diff --git a/.github/actions/perform_snapshot_tests/action.yml b/.github/actions/perform_snapshot_tests/action.yml index c33464e195bc..a5dbebeff618 100644 --- a/.github/actions/perform_snapshot_tests/action.yml +++ b/.github/actions/perform_snapshot_tests/action.yml @@ -44,6 +44,12 @@ runs: mkdir -p EcosiaTests/Results/ ./perform_snapshot_tests.sh "EcosiaTests/SnapshotTests/snapshot_configuration.json" "EcosiaTests/SnapshotTests/environment.json" "EcosiaTests/Results" "EcosiaSnapshotTests" + - name: Upload test result as artifact + uses: actions/upload-artifact@v3 + with: + name: test-result + path: 'EcosiaTests/Results/all_tests.xcresult' + - name: Process xcresult files uses: kishikawakatsumi/xcresulttool@v1 with: diff --git a/.github/actions/perform_unit_tests/action.yml b/.github/actions/perform_unit_tests/action.yml index 82d5675692e0..752363bbaf19 100644 --- a/.github/actions/perform_unit_tests/action.yml +++ b/.github/actions/perform_unit_tests/action.yml @@ -14,6 +14,7 @@ runs: shell: bash run: | bundle exec fastlane run run_tests prelaunch_simulator:true testplan:"UnitTest" + bundle exec fastlane run run_tests prelaunch_simulator:true scheme:"EcosiaFramework" testplan:"UnitTest" - name: Publish Test Report uses: mikepenz/action-junit-report@v3.7.6 diff --git a/.swiftlint.yml b/.swiftlint.yml index 6faafcca32ee..0197f7ddfd25 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -28,7 +28,8 @@ only_rules: # Only enforce these rules, ignore all others - legacy_nsgeometry_functions - mark - no_space_in_method_call - - ns_number_init_as_function_reference + # Ecosia: disabled + # - ns_number_init_as_function_reference - operator_whitespace - orphaned_doc_comment - private_over_fileprivate @@ -151,6 +152,8 @@ excluded: # paths to ignore during linting. Takes precedence over `included`. - BrowserKit/.build/ # Ecosia: Exclude Swift Packages - SourcePackages + # Ecosia; Excluding Core's EcosiaTests as we have some force unwraps + - EcosiaTests/Core included: - /Users/vagrant/git diff --git a/.tx/integration_config.yml b/.tx/integration_config.yml index 5e7314b8836d..55de8ec9506e 100644 --- a/.tx/integration_config.yml +++ b/.tx/integration_config.yml @@ -2,12 +2,12 @@ filters: - filter_type: file source_language: en - source_file: Client/Ecosia/L10N/en.lproj/Ecosia.strings + source_file: Ecosia/L10N/en.lproj/Ecosia.strings file_format: STRINGS - translation_files_expression: 'Client/Ecosia/L10N/.lproj/Ecosia.strings' + translation_files_expression: 'Ecosia/L10N/.lproj/Ecosia.strings' - filter_type: file source_language: en - source_file: Client/Ecosia/L10N/en.lproj/Plurals.stringsdict + source_file: Ecosia/L10N/en.lproj/Plurals.stringsdict file_format: STRINGSDICT - translation_files_expression: 'Client/Ecosia/L10N/.lproj/Plurals.stringsdict' + translation_files_expression: 'Ecosia/L10N/.lproj/Plurals.stringsdict' \ No newline at end of file diff --git a/BrowserKit/Sources/SiteImageView/ImageProcessing/BundleImageFetcher/BundleImageFetcher.swift b/BrowserKit/Sources/SiteImageView/ImageProcessing/BundleImageFetcher/BundleImageFetcher.swift index 227dc91e9a8c..361e3b62f463 100644 --- a/BrowserKit/Sources/SiteImageView/ImageProcessing/BundleImageFetcher/BundleImageFetcher.swift +++ b/BrowserKit/Sources/SiteImageView/ImageProcessing/BundleImageFetcher/BundleImageFetcher.swift @@ -4,8 +4,6 @@ import UIKit import Common -// Ecosia: Import Core -import Core protocol BundleImageFetcher { /// Fetches from the bundle @@ -66,7 +64,7 @@ class DefaultBundleImageFetcher: BundleImageFetcher { // MARK: - Private private func getBundleDomain(domain: ImageDomain) -> String? { - /* Ecosia: Allor Favicon to import the correct financial one + /* Ecosia: Allow Favicon to import the correct financial one OLD Implementation: return domain.bundleDomains.first(where: { bundledImages[$0] != nil }) @@ -82,17 +80,11 @@ class DefaultBundleImageFetcher: BundleImageFetcher { However, as per this class and all other dependencies being wrapped into the BrowserKit package, this would have resulted into macking a lot of changes into accessors to be able to make our own file outside of the BrowserKit context. */ - - let financialReportsURL = Environment.current.urlProvider.financialReports.absoluteString.replacingOccurrences(of: "https://", with: "") - let privacyURL = Environment.current.urlProvider.privacy.absoluteString.replacingOccurrences(of: "https://", with: "") - let urlMap = [ - financialReportsURL: "blog.ecosia.finance", - privacyURL: "privacy.ecosia" - ] - if let matchingKey = urlMap.keys.first(where: { domain.bundleDomains.contains($0) }) { - return urlMap[matchingKey] + if let ecosiaBundleDomain = EcosiaURLProvider.getBundleDomain(for: domain) { + return ecosiaBundleDomain + } else { + return domain.bundleDomains.first(where: { bundledImages[$0] != nil }) } - return domain.bundleDomains.first(where: { bundledImages[$0] != nil }) } private func retrieveBundledImages() -> [String: FormattedBundledImage] { @@ -163,3 +155,86 @@ class DefaultBundleImageFetcher: BundleImageFetcher { return UIGraphicsGetImageFromCurrentImageContext() ?? image } } +/* Ecosia + There was an annoying dependency on Core in BrowserKit as part of the BundleImageFetcher.swift that would lead to a flaky build step. + This is noticeable especially when checking out between branches without cleaning up your DerivedData. + With the advent of our EcosiaFramework, this flakiness became even more noticeable. To get a Build Successful, you had to first // comment out the Ecosia-related code as part of that file, let it build, uncomment, and build again. + This process is far from being ideal and we want as less dependencies/code changes in BrowserKit as possible. + + After spending a considerable amount of time looking for a robust solution, I realized that the best approach in this very niche scenario would be to directly implement a part of our Ecosia.Environment.swift directly in the file. + I'm conscious this is not ideal, but that's the only approach so far that guarantees the expected Favicon's workaround to work solidly. + */ +struct EcosiaURLProvider { + + // Static variables for privacy and financial reports URLs + static let privacyURL = URL(string: "https://www.ecosia.org/privacy")! + + static var financialReportsURL: URL { + let blog: URL! + + switch Language.current { + case .de: + blog = URL(string: "https://de.blog.ecosia.org/")! + case .fr: + blog = URL(string: "https://fr.blog.ecosia.org/")! + default: + blog = URL(string: "https://blog.ecosia.org/")! + } + + switch Language.current { + case .de: + return blog.appendingPathComponent("ecosia-finanzberichte-baumplanzbelege/") + case .fr: + return blog.appendingPathComponent("rapports-financiers-recus-de-plantations-arbres/") + default: + return blog.appendingPathComponent("ecosia-financial-reports-tree-planting-receipts/") + } + } + + // Utility function to create a domain part from URL (removes "https://") + private static func getURLDomain(_ url: URL) -> String { + return url.absoluteString.replacingOccurrences(of: "https://", with: "") + } + + // Static method to get the appropriate bundle domain + static func getBundleDomain(for domain: ImageDomain) -> String? { + let financialReportsDomain = getURLDomain(financialReportsURL) + let privacyDomain = getURLDomain(privacyURL) + + let urlMap: [String: String] = [ + financialReportsDomain: "blog.ecosia.finance", + privacyDomain: "privacy.ecosia" + ] + + // Check for the first matching domain + if let matchingKey = urlMap.keys.first(where: { domain.bundleDomains.contains($0) }) { + return urlMap[matchingKey] + } + + return nil + } + + public enum Language: String, Codable, CaseIterable { + case + de, + en, + fr + + public internal(set) static var current = make(for: .current) + + private static let queue = DispatchQueue(label: "\(Bundle.main.bundleIdentifier!).LanguageQueue") + static func make(for locale: Locale) -> Self { + return queue.sync { + locale.withLanguage ?? .en + } + } + } +} + +private extension Locale { + var withLanguage: EcosiaURLProvider.Language? { + languageCode.flatMap { + EcosiaURLProvider.Language(rawValue: $0.lowercased()) + } + } +} diff --git a/Client.xcodeproj/project.pbxproj b/Client.xcodeproj/project.pbxproj index c083948e7ebe..470a3e24c45a 100644 --- a/Client.xcodeproj/project.pbxproj +++ b/Client.xcodeproj/project.pbxproj @@ -27,15 +27,15 @@ 0BB5B30B1AC0AD1F0052877D /* LoginsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB5B30A1AC0AD1F0052877D /* LoginsHelper.swift */; }; 0BF0DB941A8545800039F300 /* URLBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF0DB931A8545800039F300 /* URLBarView.swift */; }; 0BF1B7E31AC60DEA00A7B407 /* InsetButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF1B7E21AC60DEA00A7B407 /* InsetButton.swift */; }; - 12147F2F2CDA3CD00009D300 /* NTPNewsletterCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12147F2E2CDA3CD00009D300 /* NTPNewsletterCardCell.swift */; }; - 12147F312CDA3CD80009D300 /* NTPNewsletterCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12147F302CDA3CD80009D300 /* NTPNewsletterCardViewModel.swift */; }; - 12147F332CDBA7230009D300 /* NewsletterCardExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12147F322CDBA7230009D300 /* NewsletterCardExperiment.swift */; }; - 126509822CD924C00011BA36 /* BrazeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 126509812CD924C00011BA36 /* BrazeService.swift */; }; + 1229357C2CE78D0A00EC1297 /* Ecosia.h in Headers */ = {isa = PBXBuildFile; fileRef = 1229356C2CE78D0A00EC1297 /* Ecosia.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1229357F2CE78D0A00EC1297 /* Ecosia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1229356A2CE78D0A00EC1297 /* Ecosia.framework */; }; + 122935802CE78D0A00EC1297 /* Ecosia.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 1229356A2CE78D0A00EC1297 /* Ecosia.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 1229358D2CE78D5D00EC1297 /* BrazeServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 126509882CDA31890011BA36 /* BrazeServiceTests.swift */; }; + 122935912CE78ED500EC1297 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 122935902CE78ED500EC1297 /* BrazeKit */; }; + 122935932CE78ED500EC1297 /* BrazeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 122935922CE78ED500EC1297 /* BrazeUI */; }; + 122935B52CE79EF900EC1297 /* SnowplowTracker in Frameworks */ = {isa = PBXBuildFile; productRef = 122935B42CE79EF900EC1297 /* SnowplowTracker */; }; 126509852CD925B40011BA36 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 126509842CD925B40011BA36 /* BrazeKit */; }; 126509872CD925B40011BA36 /* BrazeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 126509862CD925B40011BA36 /* BrazeUI */; }; - 1265098A2CDA32790011BA36 /* BrazeServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 126509882CDA31890011BA36 /* BrazeServiceTests.swift */; }; - 1285E2B52CC293CA0053506B /* AnalyticsSpyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1285E2B42CC293CA0053506B /* AnalyticsSpyTests.swift */; }; - 1285E2B72CC68BF00053506B /* APNConsent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1285E2B62CC68BF00053506B /* APNConsent.swift */; }; 158241282820698B00956B39 /* RustRemoteTabsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 158241272820698B00956B39 /* RustRemoteTabsTests.swift */; }; 15DE98FD27FCED4F00F1ECDB /* RustRemoteTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15DE98FC27FCED4F00F1ECDB /* RustRemoteTabs.swift */; }; 1B3D99F1270E89D0006E1264 /* Telemetry in Frameworks */ = {isa = PBXBuildFile; productRef = 1B3D99F0270E89D0006E1264 /* Telemetry */; }; @@ -201,148 +201,32 @@ 28E91E751B443AD5009DF274 /* SyncConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28E91E741B443AD5009DF274 /* SyncConstants.swift */; }; 28ECD9BF1BA1F19900D829DA /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = E6231C001B90A44F005ABB0D /* libz.tbd */; }; 2C0360DA2C1747E6006706F2 /* FxNimbus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0360D92C1747E6006706F2 /* FxNimbus.swift */; }; - 2C03A4152CB7C7CC00AB228B /* DispatchQueueHelper+BuildChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C03A4142CB7C7CC00AB228B /* DispatchQueueHelper+BuildChannel.swift */; }; 2C1298A52BF5EB16005AE4E4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 2C1298A42BF5EB16005AE4E4 /* PrivacyInfo.xcprivacy */; }; 2C1298A62BF5EB1E005AE4E4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 2C1298A42BF5EB16005AE4E4 /* PrivacyInfo.xcprivacy */; }; 2C1298A72BF5EB1F005AE4E4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 2C1298A42BF5EB16005AE4E4 /* PrivacyInfo.xcprivacy */; }; 2C1298A82BF5EE23005AE4E4 /* libStorage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FCAE21A1ABB51F800877008 /* libStorage.a */; }; - 2C1298AC2BF5EE3E005AE4E4 /* Core in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1298AB2BF5EE3E005AE4E4 /* Core */; }; 2C1298AF2BF602D3005AE4E4 /* DefaultSuggestedSites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394CF6CE1BAA493C00906917 /* DefaultSuggestedSites.swift */; }; - 2C16B7672CAF2441006118F8 /* UserDefaultsSeedProgressManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C16B7652CAF2425006118F8 /* UserDefaultsSeedProgressManagerTests.swift */; }; - 2C19DACF2C74C7BF00D2641C /* snapshot_configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = 2C19DACE2C74C7BF00D2641C /* snapshot_configuration.json */; }; - 2C1F23BD2B9F405E00186F55 /* Core in Frameworks */ = {isa = PBXBuildFile; productRef = 2C1F23BC2B9F405E00186F55 /* Core */; }; - 2C2349A32C57E5BC007A5894 /* EcosiaPerformanceTestHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2349A22C57E5BC007A5894 /* EcosiaPerformanceTestHistory.swift */; }; - 2C26EA142C04CAD100795552 /* EcosiaTopSitesHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C26EA132C04CAD100795552 /* EcosiaTopSitesHelperTests.swift */; }; - 2C26FAA92C8752D20055760A /* LegacyThemeManager+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189012B7A8A22006B70D7 /* LegacyThemeManager+Ecosia.swift */; }; - 2C26FAAA2C8752D20055760A /* LegacyThemeManager+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189012B7A8A22006B70D7 /* LegacyThemeManager+Ecosia.swift */; }; - 2C3BD5EB2BF6193B00E25B0D /* EcosiaThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C32B7A8A22006B70D7 /* EcosiaThemeManager.swift */; }; + 2C21011D2D0B042300CBE7EC /* AnalyticsNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C21011C2D0B042300CBE7EC /* AnalyticsNotificationSettings.swift */; }; + 2C2101212D0B04E800CBE7EC /* APNConsent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C21011E2D0B04E800CBE7EC /* APNConsent.swift */; }; + 2C2101222D0B04E800CBE7EC /* BrazeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C21011F2D0B04E800CBE7EC /* BrazeService.swift */; }; + 2C2101272D0B0D9B00CBE7EC /* MockNewsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2101242D0B0D9B00CBE7EC /* MockNewsModel.swift */; }; + 2C2101282D0B0D9B00CBE7EC /* MockWelcomeDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2101252D0B0D9B00CBE7EC /* MockWelcomeDelegate.swift */; }; + 2C2101292D0B0D9B00CBE7EC /* MockUNNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2101262D0B0D9B00CBE7EC /* MockUNNotificationSettings.swift */; }; + 2C21012B2D0B0F3700CBE7EC /* SnowplowTracker in Frameworks */ = {isa = PBXBuildFile; productRef = 2C21012A2D0B0F3700CBE7EC /* SnowplowTracker */; }; + 2C423E272D0202530043D407 /* SnapshotBaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2628E2CFDC5E900A040A7 /* SnapshotBaseTests.swift */; }; + 2C423E282D0202530043D407 /* SnapshotTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2628F2CFDC5E900A040A7 /* SnapshotTestHelper.swift */; }; + 2C423E292D02029C0043D407 /* LocaleRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2628B2CFDC5E900A040A7 /* LocaleRetriever.swift */; }; + 2C423E2A2D0203190043D407 /* MockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 281B2BE91ADF4D90002917DC /* MockProfile.swift */; }; 2C49854E206173C800893DAE /* photon-colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C49854D206173C800893DAE /* photon-colors.swift */; }; - 2C4ABD492CB58E4F00FF86F9 /* Sparkle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4ABD482CB58E4F00FF86F9 /* Sparkle.swift */; }; - 2C4D16602C76360800E89C95 /* environment.json in Resources */ = {isa = PBXBuildFile; fileRef = 2C4D165F2C76360800E89C95 /* environment.json */; }; - 2C5A5E652CB53DB7005BFE8B /* SeedCounterConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5A5E642CB53DB7005BFE8B /* SeedCounterConfig.swift */; }; - 2C5A5E672CB53DF9005BFE8B /* UserDefaultsSeedProgressManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5A5E662CB53DF9005BFE8B /* UserDefaultsSeedProgressManager.swift */; }; - 2C5B81C82C75388300B81D95 /* LocaleRetriever.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5B81C72C75388300B81D95 /* LocaleRetriever.swift */; }; - 2C6189312B7A8A22006B70D7 /* EcosiaLaunchScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188802B7A8A21006B70D7 /* EcosiaLaunchScreenView.swift */; }; - 2C6189322B7A8A22006B70D7 /* EcosiaLaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C6188812B7A8A21006B70D7 /* EcosiaLaunchScreen.xib */; }; - 2C6189332B7A8A22006B70D7 /* MMP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188832B7A8A21006B70D7 /* MMP.swift */; }; - 2C6189342B7A8A22006B70D7 /* SemanticColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188852B7A8A21006B70D7 /* SemanticColor.swift */; }; - 2C6189352B7A8A22006B70D7 /* PageActionMenuCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188872B7A8A21006B70D7 /* PageActionMenuCell.swift */; }; - 2C6189362B7A8A22006B70D7 /* PageActionsShortcutsHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188882B7A8A21006B70D7 /* PageActionsShortcutsHeader.swift */; }; - 2C6189372B7A8A22006B70D7 /* PageActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188892B7A8A21006B70D7 /* PageActionMenu.swift */; }; - 2C6189382B7A8A22006B70D7 /* EmptyBookmarksViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61888A2B7A8A21006B70D7 /* EmptyBookmarksViewDelegate.swift */; }; - 2C6189392B7A8A22006B70D7 /* EcosiaFindInPageBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61888B2B7A8A21006B70D7 /* EcosiaFindInPageBar.swift */; }; - 2C61893A2B7A8A22006B70D7 /* Ecosia.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2C61888C2B7A8A21006B70D7 /* Ecosia.xcassets */; }; - 2C61893B2B7A8A22006B70D7 /* EmptyReadingListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61888D2B7A8A21006B70D7 /* EmptyReadingListView.swift */; }; - 2C6189412B7A8A22006B70D7 /* EmptyHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188942B7A8A21006B70D7 /* EmptyHeader.swift */; }; - 2C6189422B7A8A22006B70D7 /* WhatsNewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188962B7A8A21006B70D7 /* WhatsNewCell.swift */; }; - 2C6189432B7A8A22006B70D7 /* WhatsNewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188972B7A8A21006B70D7 /* WhatsNewViewModel.swift */; }; - 2C6189442B7A8A22006B70D7 /* WhatsNewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188982B7A8A21006B70D7 /* WhatsNewViewController.swift */; }; - 2C6189452B7A8A22006B70D7 /* WhatsNewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188992B7A8A21006B70D7 /* WhatsNewItem.swift */; }; - 2C6189462B7A8A22006B70D7 /* WhatsNewDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61889B2B7A8A21006B70D7 /* WhatsNewDataProvider.swift */; }; - 2C6189472B7A8A22006B70D7 /* WhatsNewLocalDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61889C2B7A8A21006B70D7 /* WhatsNewLocalDataProvider.swift */; }; - 2C6189482B7A8A22006B70D7 /* NTPLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61889E2B7A8A21006B70D7 /* NTPLayout.swift */; }; - 2C6189492B7A8A22006B70D7 /* DefaultBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61889F2B7A8A21006B70D7 /* DefaultBrowser.swift */; }; - 2C61894A2B7A8A22006B70D7 /* CustomizableNTPSettingConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188A12B7A8A21006B70D7 /* CustomizableNTPSettingConfig.swift */; }; - 2C61894B2B7A8A22006B70D7 /* NTPCustomizationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188A22B7A8A21006B70D7 /* NTPCustomizationCell.swift */; }; - 2C61894C2B7A8A22006B70D7 /* NTPCustomizationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188A32B7A8A21006B70D7 /* NTPCustomizationCellViewModel.swift */; }; - 2C61894D2B7A8A22006B70D7 /* NTPLibraryShortcutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188A52B7A8A21006B70D7 /* NTPLibraryShortcutView.swift */; }; - 2C61894E2B7A8A22006B70D7 /* NTPLibraryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188A62B7A8A21006B70D7 /* NTPLibraryCell.swift */; }; - 2C61894F2B7A8A22006B70D7 /* NTPLibaryCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188A72B7A8A21006B70D7 /* NTPLibaryCellViewModel.swift */; }; - 2C6189502B7A8A22006B70D7 /* NTPTooltip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188A82B7A8A21006B70D7 /* NTPTooltip.swift */; }; - 2C6189512B7A8A22006B70D7 /* CircleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188A92B7A8A21006B70D7 /* CircleButton.swift */; }; - 2C6189522B7A8A22006B70D7 /* NTPTooltip.Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188AA2B7A8A21006B70D7 /* NTPTooltip.Highlight.swift */; }; - 2C6189552B7A8A22006B70D7 /* NewsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188AF2B7A8A22006B70D7 /* NewsController.swift */; }; - 2C6189562B7A8A22006B70D7 /* NTPNewsCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188B02B7A8A22006B70D7 /* NTPNewsCellViewModel.swift */; }; - 2C6189572B7A8A22006B70D7 /* NTPNewsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188B12B7A8A22006B70D7 /* NTPNewsCell.swift */; }; - 2C6189582B7A8A22006B70D7 /* NTPLogoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188B32B7A8A22006B70D7 /* NTPLogoCell.swift */; }; - 2C6189592B7A8A22006B70D7 /* NTPTooltipDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188B42B7A8A22006B70D7 /* NTPTooltipDelegate.swift */; }; - 2C61895A2B7A8A22006B70D7 /* ProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188B62B7A8A22006B70D7 /* ProgressView.swift */; }; - 2C61895B2B7A8A22006B70D7 /* NTPImpactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188B72B7A8A22006B70D7 /* NTPImpactCell.swift */; }; - 2C61895C2B7A8A22006B70D7 /* NTPImpactCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188B82B7A8A22006B70D7 /* NTPImpactCellViewModel.swift */; }; - 2C61895D2B7A8A22006B70D7 /* NTPImpactRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188B92B7A8A22006B70D7 /* NTPImpactRowView.swift */; }; - 2C61895E2B7A8A22006B70D7 /* NTPImpactDividerFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188BA2B7A8A22006B70D7 /* NTPImpactDividerFooter.swift */; }; - 2C61895F2B7A8A22006B70D7 /* ClimateImpactInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188BB2B7A8A22006B70D7 /* ClimateImpactInfo.swift */; }; - 2C6189602B7A8A22006B70D7 /* AboutEcosiaSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188BD2B7A8A22006B70D7 /* AboutEcosiaSection.swift */; }; - 2C6189612B7A8A22006B70D7 /* NTPAboutEcosiaCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188BE2B7A8A22006B70D7 /* NTPAboutEcosiaCellViewModel.swift */; }; - 2C6189622B7A8A22006B70D7 /* NTPAboutEcosiaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188BF2B7A8A22006B70D7 /* NTPAboutEcosiaCell.swift */; }; - 2C6189632B7A8A22006B70D7 /* Colours.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2C6188C02B7A8A22006B70D7 /* Colours.xcassets */; }; - 2C6189642B7A8A22006B70D7 /* EcosiaNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C12B7A8A22006B70D7 /* EcosiaNavigation.swift */; }; - 2C6189652B7A8A22006B70D7 /* EcosiaThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C32B7A8A22006B70D7 /* EcosiaThemeManager.swift */; }; - 2C6189662B7A8A22006B70D7 /* EcosiaThemeColourPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C42B7A8A22006B70D7 /* EcosiaThemeColourPalette.swift */; }; - 2C6189672B7A8A22006B70D7 /* EcosiaTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C52B7A8A22006B70D7 /* EcosiaTheme.swift */; }; - 2C6189682B7A8A22006B70D7 /* FilterController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C62B7A8A22006B70D7 /* FilterController.swift */; }; - 2C6189692B7A8A22006B70D7 /* MarketsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C72B7A8A22006B70D7 /* MarketsController.swift */; }; - 2C61896A2B7A8A22006B70D7 /* EmptyBookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C82B7A8A22006B70D7 /* EmptyBookmarksView.swift */; }; - 2C61896B2B7A8A22006B70D7 /* WelcomeTourRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188CA2B7A8A22006B70D7 /* WelcomeTourRow.swift */; }; - 2C61896C2B7A8A22006B70D7 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188CB2B7A8A22006B70D7 /* Welcome.swift */; }; - 2C61896D2B7A8A22006B70D7 /* WelcomeTour.Step.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188CC2B7A8A22006B70D7 /* WelcomeTour.Step.swift */; }; - 2C61896E2B7A8A22006B70D7 /* WelcomeTourAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188CD2B7A8A22006B70D7 /* WelcomeTourAction.swift */; }; - 2C61896F2B7A8A22006B70D7 /* WelcomeTour.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188CE2B7A8A22006B70D7 /* WelcomeTour.swift */; }; - 2C6189702B7A8A22006B70D7 /* WelcomeTourTransparent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188CF2B7A8A22006B70D7 /* WelcomeTourTransparent.swift */; }; - 2C6189712B7A8A22006B70D7 /* WelcomeTourGreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188D02B7A8A22006B70D7 /* WelcomeTourGreen.swift */; }; - 2C6189722B7A8A22006B70D7 /* WelcomeNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188D12B7A8A22006B70D7 /* WelcomeNavigation.swift */; }; - 2C6189732B7A8A22006B70D7 /* WelcomeTourProfit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188D22B7A8A22006B70D7 /* WelcomeTourProfit.swift */; }; - 2C6189742B7A8A22006B70D7 /* MultiplyImpact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188D42B7A8A22006B70D7 /* MultiplyImpact.swift */; }; - 2C6189752B7A8A22006B70D7 /* MultiplyImpactStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188D52B7A8A22006B70D7 /* MultiplyImpactStep.swift */; }; - 2C6189762B7A8A22006B70D7 /* EcosiaDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188D72B7A8A22006B70D7 /* EcosiaDebugSettings.swift */; }; - 2C6189772B7A8A22006B70D7 /* EcosiaSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188D82B7A8A22006B70D7 /* EcosiaSettings.swift */; }; - 2C6189782B7A8A22006B70D7 /* NTPCustomizationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188D92B7A8A22006B70D7 /* NTPCustomizationSettingsViewController.swift */; }; - 2C6189792B7A8A22006B70D7 /* Ecosia.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2C6188DB2B7A8A22006B70D7 /* Ecosia.strings */; }; - 2C61897A2B7A8A22006B70D7 /* Plurals.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 2C6188DD2B7A8A22006B70D7 /* Plurals.stringsdict */; }; - 2C61897D2B7A8A22006B70D7 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188E82B7A8A22006B70D7 /* String.swift */; }; - 2C61897E2B7A8A22006B70D7 /* FeatureManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188EE2B7A8A22006B70D7 /* FeatureManagement.swift */; }; - 2C61897F2B7A8A22006B70D7 /* EcosiaHomepageSectionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188F12B7A8A22006B70D7 /* EcosiaHomepageSectionType.swift */; }; - 2C6189802B7A8A22006B70D7 /* EcosiaTopSiteItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188F42B7A8A22006B70D7 /* EcosiaTopSiteItemCell.swift */; }; - 2C6189822B7A8A22006B70D7 /* BrazeIntegrationExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188F82B7A8A22006B70D7 /* BrazeIntegrationExperiment.swift */; }; - 2C6189842B7A8A22006B70D7 /* WebsiteConnectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188FB2B7A8A22006B70D7 /* WebsiteConnectionStatus.swift */; }; - 2C6189862B7A8A22006B70D7 /* BrowserViewController+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189002B7A8A22006B70D7 /* BrowserViewController+Ecosia.swift */; }; - 2C6189872B7A8A22006B70D7 /* LegacyThemeManager+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189012B7A8A22006B70D7 /* LegacyThemeManager+Ecosia.swift */; }; - 2C6189882B7A8A22006B70D7 /* UIButton+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189022B7A8A22006B70D7 /* UIButton+Ecosia.swift */; }; - 2C6189892B7A8A22006B70D7 /* UIFont+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189032B7A8A22006B70D7 /* UIFont+Ecosia.swift */; }; - 2C61898A2B7A8A22006B70D7 /* ErrorPageHandler+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189042B7A8A22006B70D7 /* ErrorPageHandler+Ecosia.swift */; }; - 2C61898B2B7A8A22006B70D7 /* SnapKit+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189052B7A8A22006B70D7 /* SnapKit+Ecosia.swift */; }; - 2C61898C2B7A8A22006B70D7 /* HomepageViewController+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189062B7A8A22006B70D7 /* HomepageViewController+Ecosia.swift */; }; - 2C61898D2B7A8A22006B70D7 /* SimpleToast+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189072B7A8A22006B70D7 /* SimpleToast+Ecosia.swift */; }; - 2C61898E2B7A8A22006B70D7 /* UIView+maskedCorners.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189082B7A8A22006B70D7 /* UIView+maskedCorners.swift */; }; - 2C61898F2B7A8A22006B70D7 /* AppInfo+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189092B7A8A22006B70D7 /* AppInfo+Ecosia.swift */; }; - 2C6189902B7A8A22006B70D7 /* NumberFormatter+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61890A2B7A8A22006B70D7 /* NumberFormatter+Ecosia.swift */; }; - 2C6189912B7A8A22006B70D7 /* URL+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61890B2B7A8A22006B70D7 /* URL+Ecosia.swift */; }; - 2C6189922B7A8A22006B70D7 /* BrowserCoordinator+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61890C2B7A8A22006B70D7 /* BrowserCoordinator+Ecosia.swift */; }; - 2C6189932B7A8A22006B70D7 /* AppSettingsTableViewController+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61890D2B7A8A22006B70D7 /* AppSettingsTableViewController+Ecosia.swift */; }; - 2C6189942B7A8A22006B70D7 /* DeviceInfo+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61890E2B7A8A22006B70D7 /* DeviceInfo+Ecosia.swift */; }; - 2C6189952B7A8A22006B70D7 /* ConnectionStatusImage+WebsiteConnectionTypeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189102B7A8A22006B70D7 /* ConnectionStatusImage+WebsiteConnectionTypeStatus.swift */; }; - 2C6189972B7A8A22006B70D7 /* ConnectionStatusImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189122B7A8A22006B70D7 /* ConnectionStatusImage.swift */; }; - 2C6189982B7A8A22006B70D7 /* BookmarksExchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189172B7A8A22006B70D7 /* BookmarksExchange.swift */; }; - 2C6189992B7A8A22006B70D7 /* markets.json in Resources */ = {isa = PBXBuildFile; fileRef = 2C6189182B7A8A22006B70D7 /* markets.json */; }; - 2C6189A22B7A8A22006B70D7 /* DefaultAppVersionInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189252B7A8A22006B70D7 /* DefaultAppVersionInfoProvider.swift */; }; - 2C6189A32B7A8A22006B70D7 /* AppVersionInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189262B7A8A22006B70D7 /* AppVersionInfoProvider.swift */; }; - 2C6189A42B7A8A22006B70D7 /* EcosiaInstallType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189282B7A8A22006B70D7 /* EcosiaInstallType.swift */; }; - 2C6189A52B7A8A22006B70D7 /* EcosiaInstallType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189292B7A8A22006B70D7 /* EcosiaInstallType+Extensions.swift */; }; - 2C6189A62B7A8A22006B70D7 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61892B2B7A8A22006B70D7 /* Version.swift */; }; - 2C6189A72B7A8A22006B70D7 /* Version+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61892C2B7A8A22006B70D7 /* Version+Extensions.swift */; }; - 2C6189A82B7A8A22006B70D7 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61892E2B7A8A22006B70D7 /* Analytics.swift */; }; - 2C6189A92B7A8A22006B70D7 /* Analytics+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61892F2B7A8A22006B70D7 /* Analytics+Configuration.swift */; }; - 2C6189AA2B7A8A22006B70D7 /* Analytics.Values.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189302B7A8A22006B70D7 /* Analytics.Values.swift */; }; - 2C6189D12B7A8D3E006B70D7 /* EcosiaThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C32B7A8A22006B70D7 /* EcosiaThemeManager.swift */; }; - 2C6189D22B7A8D69006B70D7 /* EcosiaThemeColourPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C42B7A8A22006B70D7 /* EcosiaThemeColourPalette.swift */; }; - 2C6189DD2B7B7776006B70D7 /* AppInfo+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6189092B7A8A22006B70D7 /* AppInfo+Ecosia.swift */; }; 2C6189DE2B7B78ED006B70D7 /* LegacyTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9A179A20E69A7E00B12184 /* LegacyTheme.swift */; }; - 2C6189E02B7B7916006B70D7 /* EcosiaThemeColourPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C42B7A8A22006B70D7 /* EcosiaThemeColourPalette.swift */; }; 2C6189E12B7B7922006B70D7 /* LegacyTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9A179A20E69A7E00B12184 /* LegacyTheme.swift */; }; 2C6189E22B7B7929006B70D7 /* LegacyThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9A179820E69A7E00B12184 /* LegacyThemeManager.swift */; }; 2C6189E32B7B792A006B70D7 /* LegacyThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9A179820E69A7E00B12184 /* LegacyThemeManager.swift */; }; 2C6189E42B7B7946006B70D7 /* LegacyDarkTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9A179920E69A7E00B12184 /* LegacyDarkTheme.swift */; }; 2C6189E52B7B7946006B70D7 /* LegacyDarkTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB9A179920E69A7E00B12184 /* LegacyDarkTheme.swift */; }; - 2C6189E62B7B7952006B70D7 /* SemanticColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188852B7A8A21006B70D7 /* SemanticColor.swift */; }; - 2C6189E72B7B7952006B70D7 /* SemanticColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188852B7A8A21006B70D7 /* SemanticColor.swift */; }; - 2C6189E92B7B7979006B70D7 /* EcosiaTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C52B7A8A22006B70D7 /* EcosiaTheme.swift */; }; - 2C6189EA2B7B7979006B70D7 /* EcosiaTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6188C52B7A8A22006B70D7 /* EcosiaTheme.swift */; }; 2C6189EB2B7B7AB4006B70D7 /* DispatchQueueHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83821FF1FC7961D00303C12 /* DispatchQueueHelper.swift */; }; 2C6189EC2B7B7AB4006B70D7 /* DispatchQueueHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83821FF1FC7961D00303C12 /* DispatchQueueHelper.swift */; }; 2C6189F12B7B7D5D006B70D7 /* SnowplowTracker in Frameworks */ = {isa = PBXBuildFile; productRef = 2C6189F02B7B7D5D006B70D7 /* SnowplowTracker */; }; - 2C69DA722C62175400D7F69F /* SnapshotTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C69DA712C62175400D7F69F /* SnapshotTestHelper.swift */; }; - 2C69DA752C62185A00D7F69F /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C69DA742C62185A00D7F69F /* Welcome.swift */; }; - 2C69DA772C62243300D7F69F /* NTPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C69DA762C62243300D7F69F /* NTPTests.swift */; }; 2C69DA782C62259D00D7F69F /* MockProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 281B2BE91ADF4D90002917DC /* MockProfile.swift */; }; 2C69DA792C6225AE00D7F69F /* DependencyHelperMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A70EF18295E2E1600790249 /* DependencyHelperMock.swift */; }; 2C69DA7B2C6225C400D7F69F /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 2C69DA7A2C6225C400D7F69F /* Common */; }; @@ -350,50 +234,330 @@ 2C69DA7D2C6244BE00D7F69F /* MockTabDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A475E8C29DB888E009C13FD /* MockTabDataStore.swift */; }; 2C69DA7E2C6244C400D7F69F /* MockThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AA75A622A46272000533F8D /* MockThemeManager.swift */; }; 2C69DA7F2C62458300D7F69F /* RustMozillaAppServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43BE578A278BA4D900491291 /* RustMozillaAppServices.framework */; }; - 2C69DA812C62459200D7F69F /* Core in Frameworks */ = {isa = PBXBuildFile; productRef = 2C69DA802C62459200D7F69F /* Core */; }; 2C69DA822C62459C00D7F69F /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CB1728B2C61336D008551E2 /* libz.tbd */; }; - 2C69DA842C62B44B00D7F69F /* NTPComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C69DA832C62B44B00D7F69F /* NTPComponentTests.swift */; }; - 2C69DA922C63A92D00D7F69F /* SnapshotBaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C69DA912C63A92D00D7F69F /* SnapshotBaseTests.swift */; }; - 2C69DA942C63B0C000D7F69F /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C69DA932C63B0C000D7F69F /* String+Extension.swift */; }; - 2C6B5B3E2CAAF28000F15323 /* NTPSeedCounterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6B5B3A2CAAF27F00F15323 /* NTPSeedCounterCell.swift */; }; - 2C6B5B3F2CAAF28000F15323 /* NTPSeedCounterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6B5B3B2CAAF27F00F15323 /* NTPSeedCounterViewModel.swift */; }; - 2C6B5B412CAAF3AA00F15323 /* SeedCounterNTPExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6B5B402CAAF3AA00F15323 /* SeedCounterNTPExperiment.swift */; }; 2C6C908F2C614A6C007D9B43 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 2C6C908E2C614A6C007D9B43 /* SnapshotTesting */; }; - 2C6C90902C614A76007D9B43 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD368492C5BC31700972871 /* OnboardingTests.swift */; }; - 2C728D7E2CBBDCDC00C7684B /* UnleashUserDefaultsSeedProgressManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C728D7D2CBBDCDC00C7684B /* UnleashUserDefaultsSeedProgressManagerTests.swift */; }; - 2C78374B2C1765DF00BBFFEB /* LoadingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCFB3D42C0F1EA500BEDCA0 /* LoadingScreen.swift */; }; - 2C7DBABD2C4EA37200BCD03F /* AppDelegateFeatureManagementIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7DBABB2C4EA17200BCD03F /* AppDelegateFeatureManagementIntegrationTests.swift */; }; - 2C82625A2C64BB9300E2A255 /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8262592C64BB9300E2A255 /* DeviceType.swift */; }; - 2C82625C2C6648D900E2A255 /* LocalizationOverrideTestingBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C82625B2C6648D900E2A255 /* LocalizationOverrideTestingBundle.swift */; }; - 2C82625E2C66661700E2A255 /* EcosiaMockThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C82625D2C66661700E2A255 /* EcosiaMockThemeManager.swift */; }; 2C872A552B8CD58200B318A0 /* ContileProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7A93ED2810ADF2005E7E1B /* ContileProviderTests.swift */; }; - 2C872A5A2B8CD7E000B318A0 /* MockAppVersionInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C872A532B8CD47600B318A0 /* MockAppVersionInfoProvider.swift */; }; - 2C872A5B2B8CD7E000B318A0 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE294692B7FC5A5006C22B2 /* AnalyticsTests.swift */; }; - 2C872A5E2B8CD7E000B318A0 /* EcosiaInstallTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE294672B7FC5A4006C22B2 /* EcosiaInstallTypeTests.swift */; }; - 2C872A5F2B8CD7E000B318A0 /* EcosiaNTPTooltipHighlightTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE2946C2B7FC5A5006C22B2 /* EcosiaNTPTooltipHighlightTests.swift */; }; - 2C872A602B8CD7E000B318A0 /* EcosiaPageActionMenuCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE2946D2B7FC5A5006C22B2 /* EcosiaPageActionMenuCellTests.swift */; }; - 2C872A622B8CD7E000B318A0 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE294702B7FC5A6006C22B2 /* VersionTests.swift */; }; - 2C872A632B8CD7E000B318A0 /* WhatsNewLocalDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE2946B2B7FC5A5006C22B2 /* WhatsNewLocalDataProviderTests.swift */; }; - 2C872A642B8CD7E000B318A0 /* EcosiaHomeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C872A562B8CD65100B318A0 /* EcosiaHomeViewModelTests.swift */; }; - 2C9258D92CEF97B100C6BB8D /* MockUNNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9258D82CEF97B100C6BB8D /* MockUNNotificationSettings.swift */; }; - 2C9258DB2CEFB26500C6BB8D /* AnalyticsNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9258DA2CEFB26500C6BB8D /* AnalyticsNotificationSettings.swift */; }; - 2C9A62C02CDE1F7600CDA7D1 /* MockWelcomeDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9A62BF2CDE1F7600CDA7D1 /* MockWelcomeDelegate.swift */; }; - 2C9A62C22CDE4A3B00CDA7D1 /* MockNewsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9A62C12CDE4A3B00CDA7D1 /* MockNewsModel.swift */; }; - 2CA995282CA2C06A001064CC /* NTPConfigurableNudgeCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA995272CA2C06A001064CC /* NTPConfigurableNudgeCardCell.swift */; }; - 2CA9952A2CA2C0BB001064CC /* NTPConfigurableNudgeCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA995292CA2C0BB001064CC /* NTPConfigurableNudgeCardCellViewModel.swift */; }; 2CABD7162C11C9CC00A0750F /* MozillaAppServices in Frameworks */ = {isa = PBXBuildFile; productRef = 2CABD7152C11C9CC00A0750F /* MozillaAppServices */; }; - 2CABD7282C12EF1E00A0750F /* PrivateModeButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CABD7272C12EF1E00A0750F /* PrivateModeButtonTests.swift */; }; - 2CCBB5232CAE9826006E2E10 /* ArcProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCBB5222CAE9826006E2E10 /* ArcProgressView.swift */; }; - 2CCBB5252CAEA9DF006E2E10 /* SeedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCBB5242CAEA9DF006E2E10 /* SeedProgressView.swift */; }; - 2CCBB5272CAEAD53006E2E10 /* SeedCounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCBB5262CAEAD53006E2E10 /* SeedCounterView.swift */; }; - 2CCBB5352CAF06DE006E2E10 /* SeedProgressManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCBB5342CAF06DE006E2E10 /* SeedProgressManagerProtocol.swift */; }; - 2CCBB5372CAF0E8C006E2E10 /* SeedCounterHiddenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCBB5362CAF0E8C006E2E10 /* SeedCounterHiddenSettings.swift */; }; + 2CAF5AE32D00AAEE00D3DCDD /* Bundle+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AE22D00AAEE00D3DCDD /* Bundle+Ecosia.swift */; }; + 2CAF5AE72D00B1D300D3DCDD /* EcosiaLaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF5AE42D00B1D300D3DCDD /* EcosiaLaunchScreen.xib */; }; + 2CAF5AE82D00B1D300D3DCDD /* EcosiaLaunchScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AE52D00B1D300D3DCDD /* EcosiaLaunchScreenView.swift */; }; + 2CAF5B4C2D00D16C00D3DCDD /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AEA2D00D16700D3DCDD /* Bookmark.swift */; }; + 2CAF5B4D2D00D16C00D3DCDD /* BookmarkParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AEB2D00D16700D3DCDD /* BookmarkParser.swift */; }; + 2CAF5B4E2D00D16C00D3DCDD /* BookmarkSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AEC2D00D16700D3DCDD /* BookmarkSerializer.swift */; }; + 2CAF5B4F2D00D16C00D3DCDD /* Document+Safari.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AED2D00D16700D3DCDD /* Document+Safari.swift */; }; + 2CAF5B502D00D16C00D3DCDD /* String+CssQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AEE2D00D16700D3DCDD /* String+CssQuery.swift */; }; + 2CAF5B512D00D16C00D3DCDD /* BaseRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AF02D00D16700D3DCDD /* BaseRequest.swift */; }; + 2CAF5B522D00D16C00D3DCDD /* Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AF12D00D16700D3DCDD /* Requestable.swift */; }; + 2CAF5B532D00D16C00D3DCDD /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AF32D00D16700D3DCDD /* HTTPClient.swift */; }; + 2CAF5B542D00D16C00D3DCDD /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AF42D00D16700D3DCDD /* HTTPMethod.swift */; }; + 2CAF5B552D00D16C00D3DCDD /* URLSessionHTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AF52D00D16700D3DCDD /* URLSessionHTTPClient.swift */; }; + 2CAF5B562D00D16C00D3DCDD /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AF62D00D16700D3DCDD /* URLSessionProtocol.swift */; }; + 2CAF5B572D00D16C00D3DCDD /* Cookie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AF82D00D16700D3DCDD /* Cookie.swift */; }; + 2CAF5B582D00D16C00D3DCDD /* ReferralClaimRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AF92D00D16700D3DCDD /* ReferralClaimRequest.swift */; }; + 2CAF5B592D00D16C00D3DCDD /* ReferralCreateCodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AFA2D00D16700D3DCDD /* ReferralCreateCodeRequest.swift */; }; + 2CAF5B5A2D00D16C00D3DCDD /* ReferralRefreshCodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AFB2D00D16700D3DCDD /* ReferralRefreshCodeRequest.swift */; }; + 2CAF5B5B2D00D16C00D3DCDD /* Referrals.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AFC2D00D16700D3DCDD /* Referrals.Model.swift */; }; + 2CAF5B5C2D00D16C00D3DCDD /* Referrals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AFD2D00D16700D3DCDD /* Referrals.swift */; }; + 2CAF5B5D2D00D16C00D3DCDD /* Scheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5AFF2D00D16700D3DCDD /* Scheme.swift */; }; + 2CAF5B5E2D00D16C00D3DCDD /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B002D00D16800D3DCDD /* Publisher.swift */; }; + 2CAF5B5F2D00D16C00D3DCDD /* TimestampProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B012D00D16800D3DCDD /* TimestampProvider.swift */; }; + 2CAF5B602D00D16C00D3DCDD /* SingularConversionValueRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B022D00D16800D3DCDD /* SingularConversionValueRequest.swift */; }; + 2CAF5B612D00D16C00D3DCDD /* SingularConversionValueResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B032D00D16800D3DCDD /* SingularConversionValueResponse.swift */; }; + 2CAF5B622D00D16C00D3DCDD /* SingularEventRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B042D00D16800D3DCDD /* SingularEventRequest.swift */; }; + 2CAF5B632D00D16C00D3DCDD /* SingularReponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B052D00D16800D3DCDD /* SingularReponse.swift */; }; + 2CAF5B642D00D16C00D3DCDD /* SingularService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B062D00D16800D3DCDD /* SingularService.swift */; }; + 2CAF5B652D00D16C00D3DCDD /* SingularSessionInfoSendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B072D00D16800D3DCDD /* SingularSessionInfoSendRequest.swift */; }; + 2CAF5B662D00D16C00D3DCDD /* Singular.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B092D00D16800D3DCDD /* Singular.swift */; }; + 2CAF5B672D00D16C00D3DCDD /* SingularAdNetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B0A2D00D16800D3DCDD /* SingularAdNetworkHelper.swift */; }; + 2CAF5B682D00D16C00D3DCDD /* SingularEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B0B2D00D16800D3DCDD /* SingularEvent.swift */; }; + 2CAF5B6A2D00D16C00D3DCDD /* MMPProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B0E2D00D16800D3DCDD /* MMPProvider.swift */; }; + 2CAF5B6B2D00D16C00D3DCDD /* SKAdNetworkProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B0F2D00D16800D3DCDD /* SKAdNetworkProtocol.swift */; }; + 2CAF5B6C2D00D16C00D3DCDD /* AppDeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B112D00D16800D3DCDD /* AppDeviceInfo.swift */; }; + 2CAF5B6D2D00D16C00D3DCDD /* Environment.Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B122D00D16800D3DCDD /* Environment.Auth.swift */; }; + 2CAF5B6E2D00D16C00D3DCDD /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B132D00D16800D3DCDD /* Environment.swift */; }; + 2CAF5B6F2D00D16C00D3DCDD /* URLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B142D00D16800D3DCDD /* URLProvider.swift */; }; + 2CAF5B702D00D16C00D3DCDD /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B172D00D16800D3DCDD /* TimeInterval+Extensions.swift */; }; + 2CAF5B712D00D16C00D3DCDD /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B182D00D16800D3DCDD /* History.swift */; }; + 2CAF5B722D00D16C00D3DCDD /* AppUpdateRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B192D00D16900D3DCDD /* AppUpdateRule.swift */; }; + 2CAF5B732D00D16C00D3DCDD /* DeviceRegionChangeRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B1A2D00D16900D3DCDD /* DeviceRegionChangeRule.swift */; }; + 2CAF5B742D00D16C00D3DCDD /* RefreshingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B1B2D00D16900D3DCDD /* RefreshingRule.swift */; }; + 2CAF5B752D00D16C00D3DCDD /* TimeBasedRefreshingRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B1C2D00D16900D3DCDD /* TimeBasedRefreshingRule.swift */; }; + 2CAF5B762D00D16C00D3DCDD /* Unleash.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B1F2D00D16900D3DCDD /* Unleash.Model.swift */; }; + 2CAF5B772D00D16C00D3DCDD /* Unleash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B202D00D16900D3DCDD /* Unleash.swift */; }; + 2CAF5B782D00D16C00D3DCDD /* Unleash+RefreshComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B212D00D16900D3DCDD /* Unleash+RefreshComponent.swift */; }; + 2CAF5B792D00D16C00D3DCDD /* UnleashFeatureManagementSessionInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B222D00D16900D3DCDD /* UnleashFeatureManagementSessionInitializer.swift */; }; + 2CAF5B7A2D00D16C00D3DCDD /* UnleashRefreshConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B232D00D16900D3DCDD /* UnleashRefreshConfigurator.swift */; }; + 2CAF5B7B2D00D16C00D3DCDD /* UnleashStartRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B242D00D16900D3DCDD /* UnleashStartRequest.swift */; }; + 2CAF5B7C2D00D16C00D3DCDD /* FeatureManagementSessionInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B262D00D16900D3DCDD /* FeatureManagementSessionInitializer.swift */; }; + 2CAF5B7D2D00D16C00D3DCDD /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B282D00D16900D3DCDD /* Tab.swift */; }; + 2CAF5B7E2D00D16C00D3DCDD /* Tabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B292D00D16900D3DCDD /* Tabs.swift */; }; + 2CAF5B7F2D00D16C00D3DCDD /* News.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B2B2D00D16900D3DCDD /* News.swift */; }; + 2CAF5B802D00D16C00D3DCDD /* NewsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B2C2D00D16900D3DCDD /* NewsModel.swift */; }; + 2CAF5B812D00D16C00D3DCDD /* ObjectPersister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B2E2D00D16900D3DCDD /* ObjectPersister.swift */; }; + 2CAF5B822D00D16C00D3DCDD /* Date+TimestampProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B2F2D00D16900D3DCDD /* Date+TimestampProvider.swift */; }; + 2CAF5B832D00D16C00D3DCDD /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B302D00D16900D3DCDD /* Page.swift */; }; + 2CAF5B842D00D16C00D3DCDD /* PageStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B312D00D16900D3DCDD /* PageStore.swift */; }; + 2CAF5B852D00D16C00D3DCDD /* URLRequest+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B332D00D16900D3DCDD /* URLRequest+Extensions.swift */; }; + 2CAF5B862D00D16C00D3DCDD /* Encodable+Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B342D00D16A00D3DCDD /* Encodable+Dictionary.swift */; }; + 2CAF5B872D00D16C00D3DCDD /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B352D00D16A00D3DCDD /* User.swift */; }; + 2CAF5B882D00D16C00D3DCDD /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B362D00D16A00D3DCDD /* URL+Extensions.swift */; }; + 2CAF5B892D00D16C00D3DCDD /* FinancialReports.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B372D00D16A00D3DCDD /* FinancialReports.swift */; }; + 2CAF5B8A2D00D16C00D3DCDD /* InvestmentsProjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B382D00D16A00D3DCDD /* InvestmentsProjection.swift */; }; + 2CAF5B8B2D00D16C00D3DCDD /* Statistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B392D00D16A00D3DCDD /* Statistics.swift */; }; + 2CAF5B8C2D00D16C00D3DCDD /* TreesProjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B3A2D00D16A00D3DCDD /* TreesProjection.swift */; }; + 2CAF5B8D2D00D16C00D3DCDD /* EnvironmentFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B3C2D00D16A00D3DCDD /* EnvironmentFetcher.swift */; }; + 2CAF5B8E2D00D16C00D3DCDD /* Local.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B3E2D00D16A00D3DCDD /* Local.swift */; }; + 2CAF5B8F2D00D16C00D3DCDD /* Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B3F2D00D16B00D3DCDD /* Language.swift */; }; + 2CAF5B902D00D16C00D3DCDD /* Favourites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B402D00D16B00D3DCDD /* Favourites.swift */; }; + 2CAF5B912D00D16C00D3DCDD /* List.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B412D00D16B00D3DCDD /* List.swift */; }; + 2CAF5B922D00D16C00D3DCDD /* AdultFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B422D00D16B00D3DCDD /* AdultFilter.swift */; }; + 2CAF5B932D00D16C00D3DCDD /* Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B432D00D16B00D3DCDD /* Images.swift */; }; + 2CAF5B942D00D16C00D3DCDD /* UserDefaults+ObjectPersister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B442D00D16B00D3DCDD /* UserDefaults+ObjectPersister.swift */; }; + 2CAF5B952D00D16C00D3DCDD /* SearchesCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B452D00D16B00D3DCDD /* SearchesCounter.swift */; }; + 2CAF5B962D00D16C00D3DCDD /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B462D00D16C00D3DCDD /* FileManager.swift */; }; + 2CAF5B972D00D16C00D3DCDD /* Market.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B472D00D16C00D3DCDD /* Market.swift */; }; + 2CAF5B982D00D16C00D3DCDD /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B482D00D16C00D3DCDD /* Bundle.swift */; }; + 2CAF5B992D00D16C00D3DCDD /* RegionLocatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B492D00D16C00D3DCDD /* RegionLocatable.swift */; }; + 2CAF5B9A2D00D16C00D3DCDD /* CloudFlareKeyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B4A2D00D16C00D3DCDD /* CloudFlareKeyProvider.swift */; }; + 2CAF5B9B2D00D16C00D3DCDD /* Locale+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B4B2D00D16C00D3DCDD /* Locale+Extensions.swift */; }; + 2CAF5BD52D00D1AB00D3DCDD /* User5_3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B9D2D00D1A600D3DCDD /* User5_3.swift */; }; + 2CAF5BD62D00D1AB00D3DCDD /* BookmarkFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5B9F2D00D1A600D3DCDD /* BookmarkFixtures.swift */; }; + 2CAF5BD72D00D1AB00D3DCDD /* HTTPClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BA02D00D1A600D3DCDD /* HTTPClientMock.swift */; }; + 2CAF5BD82D00D1AB00D3DCDD /* MockTimestampProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BA12D00D1A600D3DCDD /* MockTimestampProvider.swift */; }; + 2CAF5BD92D00D1AB00D3DCDD /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BA22D00D1A600D3DCDD /* MockURLSession.swift */; }; + 2CAF5BDA2D00D1AB00D3DCDD /* MockURLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BA32D00D1A600D3DCDD /* MockURLSessionProtocol.swift */; }; + 2CAF5BDB2D00D1AB00D3DCDD /* ListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BA52D00D1A600D3DCDD /* ListTests.swift */; }; + 2CAF5BDC2D00D1AB00D3DCDD /* URLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BA62D00D1A600D3DCDD /* URLTests.swift */; }; + 2CAF5BDD2D00D1AB00D3DCDD /* FeatureFlaggingSessionInitializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BA72D00D1A700D3DCDD /* FeatureFlaggingSessionInitializerTests.swift */; }; + 2CAF5BDE2D00D1AB00D3DCDD /* export_bookmark_ecosia.html in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF5BA82D00D1A700D3DCDD /* export_bookmark_ecosia.html */; }; + 2CAF5BDF2D00D1AB00D3DCDD /* import_input_bookmark_chrome.html in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF5BA92D00D1A700D3DCDD /* import_input_bookmark_chrome.html */; }; + 2CAF5BE02D00D1AB00D3DCDD /* import_input_bookmark_firefox.html in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF5BAA2D00D1A700D3DCDD /* import_input_bookmark_firefox.html */; }; + 2CAF5BE12D00D1AB00D3DCDD /* import_input_bookmark_safari.html in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF5BAB2D00D1A700D3DCDD /* import_input_bookmark_safari.html */; }; + 2CAF5BE22D00D1AB00D3DCDD /* import_output_bookmark_chrome.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF5BAC2D00D1A700D3DCDD /* import_output_bookmark_chrome.txt */; }; + 2CAF5BE32D00D1AB00D3DCDD /* import_output_bookmark_firefox.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF5BAD2D00D1A700D3DCDD /* import_output_bookmark_firefox.txt */; }; + 2CAF5BE42D00D1AB00D3DCDD /* import_output_bookmark_safari.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF5BAE2D00D1A700D3DCDD /* import_output_bookmark_safari.txt */; }; + 2CAF5BE52D00D1AB00D3DCDD /* notifications.json in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF5BB02D00D1A700D3DCDD /* notifications.json */; }; + 2CAF5BE62D00D1AB00D3DCDD /* referrals.json in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF5BB12D00D1A700D3DCDD /* referrals.json */; }; + 2CAF5BE72D00D1AB00D3DCDD /* SingularTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BB32D00D1A700D3DCDD /* SingularTests.swift */; }; + 2CAF5BE82D00D1AB00D3DCDD /* SingularServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BB42D00D1A700D3DCDD /* SingularServiceTests.swift */; }; + 2CAF5BE92D00D1AB00D3DCDD /* UpgradeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BB52D00D1A700D3DCDD /* UpgradeTests.swift */; }; + 2CAF5BEA2D00D1AB00D3DCDD /* UnleashFeatureManagementSessionInitializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BB62D00D1A700D3DCDD /* UnleashFeatureManagementSessionInitializerTests.swift */; }; + 2CAF5BEB2D00D1AB00D3DCDD /* BookmarkParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BB72D00D1A700D3DCDD /* BookmarkParserTests.swift */; }; + 2CAF5BEC2D00D1AB00D3DCDD /* NewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BB82D00D1A700D3DCDD /* NewsTests.swift */; }; + 2CAF5BED2D00D1AB00D3DCDD /* BookmarkSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BB92D00D1A800D3DCDD /* BookmarkSerializerTests.swift */; }; + 2CAF5BEE2D00D1AB00D3DCDD /* ImagesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BBA2D00D1A800D3DCDD /* ImagesTests.swift */; }; + 2CAF5BEF2D00D1AB00D3DCDD /* StatisticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BBB2D00D1A800D3DCDD /* StatisticsTests.swift */; }; + 2CAF5BF02D00D1AB00D3DCDD /* ReferralsModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BBC2D00D1A800D3DCDD /* ReferralsModelTests.swift */; }; + 2CAF5BF12D00D1AB00D3DCDD /* HistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BBD2D00D1A800D3DCDD /* HistoryTests.swift */; }; + 2CAF5BF22D00D1AB00D3DCDD /* BookmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BBE2D00D1A800D3DCDD /* BookmarkTests.swift */; }; + 2CAF5BF42D00D1AB00D3DCDD /* LocalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BC02D00D1A800D3DCDD /* LocalTests.swift */; }; + 2CAF5BF52D00D1AB00D3DCDD /* SingularAdNetworkHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BC12D00D1A900D3DCDD /* SingularAdNetworkHelperTests.swift */; }; + 2CAF5BF62D00D1AB00D3DCDD /* TabsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BC22D00D1A900D3DCDD /* TabsTests.swift */; }; + 2CAF5BF72D00D1AB00D3DCDD /* TreesProjectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BC32D00D1A900D3DCDD /* TreesProjectionTests.swift */; }; + 2CAF5BF82D00D1AB00D3DCDD /* UnleashTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BC42D00D1A900D3DCDD /* UnleashTests.swift */; }; + 2CAF5BF92D00D1AB00D3DCDD /* ProductionURLProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BC52D00D1A900D3DCDD /* ProductionURLProviderTests.swift */; }; + 2CAF5BFA2D00D1AB00D3DCDD /* StagingURLProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BC62D00D1A900D3DCDD /* StagingURLProviderTests.swift */; }; + 2CAF5BFB2D00D1AB00D3DCDD /* URLProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BC72D00D1A900D3DCDD /* URLProviderTests.swift */; }; + 2CAF5BFC2D00D1AB00D3DCDD /* UserStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BC92D00D1A900D3DCDD /* UserStateTests.swift */; }; + 2CAF5BFD2D00D1AB00D3DCDD /* SearchesCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BCA2D00D1A900D3DCDD /* SearchesCounterTests.swift */; }; + 2CAF5BFE2D00D1AB00D3DCDD /* ReferralsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BCB2D00D1AA00D3DCDD /* ReferralsTests.swift */; }; + 2CAF5BFF2D00D1AB00D3DCDD /* FinancialReportsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BCC2D00D1AA00D3DCDD /* FinancialReportsTests.swift */; }; + 2CAF5C002D00D1AB00D3DCDD /* CookieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BCD2D00D1AA00D3DCDD /* CookieTests.swift */; }; + 2CAF5C012D00D1AB00D3DCDD /* InvestmentsProjectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BCE2D00D1AA00D3DCDD /* InvestmentsProjectionTests.swift */; }; + 2CAF5C022D00D1AB00D3DCDD /* PublishersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BCF2D00D1AA00D3DCDD /* PublishersTests.swift */; }; + 2CAF5C032D00D1AB00D3DCDD /* UnleashRefreshConfiguratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BD02D00D1AA00D3DCDD /* UnleashRefreshConfiguratorTests.swift */; }; + 2CAF5C042D00D1AB00D3DCDD /* UserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BD12D00D1AA00D3DCDD /* UserTests.swift */; }; + 2CAF5C052D00D1AB00D3DCDD /* LanguageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BD22D00D1AA00D3DCDD /* LanguageTests.swift */; }; + 2CAF5C062D00D1AB00D3DCDD /* FavouritesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BD32D00D1AB00D3DCDD /* FavouritesTests.swift */; }; + 2CAF5C072D00D1AB00D3DCDD /* URLRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF5BD42D00D1AB00D3DCDD /* URLRequestTests.swift */; }; + 2CAF5C172D01B2E200D3DCDD /* Ecosia.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1229356A2CE78D0A00EC1297 /* Ecosia.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 2CAF5C1D2D01B40B00D3DCDD /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 2CAF5C1C2D01B40B00D3DCDD /* SwiftSoup */; }; + 2CAF5C1F2D01B44400D3DCDD /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 2CAF5C1E2D01B44400D3DCDD /* Sentry */; }; + 2CAF5C212D01B5F300D3DCDD /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 2CAF5C202D01B5F300D3DCDD /* Common */; }; + 2CAF5C222D01B62900D3DCDD /* Ecosia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1229356A2CE78D0A00EC1297 /* Ecosia.framework */; }; + 2CAF5C232D01C2F300D3DCDD /* NTPComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD261B92CFDC5E900A040A7 /* NTPComponentTests.swift */; }; + 2CAF5C242D01C30B00D3DCDD /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262862CFDC5E900A040A7 /* OnboardingTests.swift */; }; + 2CAF5C252D01C31900D3DCDD /* NTPTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD261BA2CFDC5E900A040A7 /* NTPTests.swift */; }; + 2CAF5C262D01C32C00D3DCDD /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD261852CFDC5E900A040A7 /* Welcome.swift */; }; + 2CAF5C272D01C33300D3DCDD /* EcosiaMockThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD261842CFDC5E900A040A7 /* EcosiaMockThemeManager.swift */; }; + 2CBF7AF42D13056300454AB4 /* BundleImageFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBF7AF32D13056300454AB4 /* BundleImageFetcherTests.swift */; }; 2CCFB3D72C0FBEE800BEDCA0 /* TabToolbarHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D815A3A724A53F3200AAB221 /* TabToolbarHelperTests.swift */; }; - 2CD48B7F2C7F7E4100A70908 /* EcosiaOverlayModeManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD48B7E2C7F7E4100A70908 /* EcosiaOverlayModeManagerTests.swift */; }; - 2CE294472B7CDD56006C22B2 /* Core in Frameworks */ = {isa = PBXBuildFile; productRef = 2CE294462B7CDD56006C22B2 /* Core */; }; - 2CE294492B7CDD78006C22B2 /* Core in Frameworks */ = {isa = PBXBuildFile; productRef = 2CE294482B7CDD78006C22B2 /* Core */; }; - 2CE2E24D2B9B1FCB00973C16 /* Core in Frameworks */ = {isa = PBXBuildFile; productRef = 2CE2E24C2B9B1FCB00973C16 /* Core */; }; - 2CF4DA632BB31970001C340A /* Core in Frameworks */ = {isa = PBXBuildFile; productRef = 2CF4DA622BB31970001C340A /* Core */; }; + 2CD263AF2CFDC5EB00A040A7 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262922CFDC5E900A040A7 /* VersionTests.swift */; }; + 2CD263B22CFDC5EB00A040A7 /* WhatsNewLocalDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262962CFDC5E900A040A7 /* WhatsNewLocalDataProviderTests.swift */; }; + 2CD263B32CFDC5EB00A040A7 /* AppDelegateFeatureManagementIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262972CFDC5EA00A040A7 /* AppDelegateFeatureManagementIntegrationTests.swift */; }; + 2CD263B42CFDC5EB00A040A7 /* EcosiaInstallTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262992CFDC5EA00A040A7 /* EcosiaInstallTypeTests.swift */; }; + 2CD263B52CFDC5EB00A040A7 /* EcosiaPerformanceTestHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2629A2CFDC5EA00A040A7 /* EcosiaPerformanceTestHistory.swift */; }; + 2CD263B62CFDC5EB00A040A7 /* MockAppVersionInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2629B2CFDC5EA00A040A7 /* MockAppVersionInfoProvider.swift */; }; + 2CD263B72CFDC5EB00A040A7 /* PrivateModeButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2629D2CFDC5EA00A040A7 /* PrivateModeButtonTests.swift */; }; + 2CD263B82CFDC5EB00A040A7 /* EcosiaNTPTooltipHighlightTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2629E2CFDC5EA00A040A7 /* EcosiaNTPTooltipHighlightTests.swift */; }; + 2CD263B92CFDC5EB00A040A7 /* EcosiaHomeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2629F2CFDC5EA00A040A7 /* EcosiaHomeViewModelTests.swift */; }; + 2CD263BA2CFDC5EB00A040A7 /* UnleashUserDefaultsSeedProgressManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262A02CFDC5EA00A040A7 /* UnleashUserDefaultsSeedProgressManagerTests.swift */; }; + 2CD263BB2CFDC5EB00A040A7 /* UserDefaultsSeedProgressManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262A12CFDC5EA00A040A7 /* UserDefaultsSeedProgressManagerTests.swift */; }; + 2CD263BC2CFDC5EB00A040A7 /* EcosiaTopSitesHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262A32CFDC5EB00A040A7 /* EcosiaTopSitesHelperTests.swift */; }; + 2CD263BD2CFDC5EB00A040A7 /* EcosiaOverlayModeManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262A42CFDC5EB00A040A7 /* EcosiaOverlayModeManagerTests.swift */; }; + 2CD263BE2CFDC5EB00A040A7 /* EcosiaPageActionMenuCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262A52CFDC5EB00A040A7 /* EcosiaPageActionMenuCellTests.swift */; }; + 2CD263BF2CFDC5EB00A040A7 /* AnalyticsSpyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262A62CFDC5EB00A040A7 /* AnalyticsSpyTests.swift */; }; + 2CD263C02CFDC5EB00A040A7 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262A72CFDC5EB00A040A7 /* AnalyticsTests.swift */; }; + 2CD2648E2CFDC76A00A040A7 /* Ecosia.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263C92CFDC76800A040A7 /* Ecosia.strings */; }; + 2CD2648F2CFDC76A00A040A7 /* Ecosia.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263CB2CFDC76800A040A7 /* Ecosia.strings */; }; + 2CD264902CFDC76A00A040A7 /* Ecosia.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263CD2CFDC76800A040A7 /* Ecosia.strings */; }; + 2CD264912CFDC76A00A040A7 /* Ecosia.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263CF2CFDC76800A040A7 /* Ecosia.strings */; }; + 2CD264922CFDC76A00A040A7 /* Ecosia.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263D12CFDC76800A040A7 /* Ecosia.strings */; }; + 2CD264932CFDC76A00A040A7 /* Ecosia.strings in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263D32CFDC76800A040A7 /* Ecosia.strings */; }; + 2CD264942CFDC76A00A040A7 /* Plurals.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263D52CFDC76800A040A7 /* Plurals.stringsdict */; }; + 2CD264952CFDC76A00A040A7 /* Plurals.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263D72CFDC76800A040A7 /* Plurals.stringsdict */; }; + 2CD264962CFDC76A00A040A7 /* Plurals.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263D92CFDC76800A040A7 /* Plurals.stringsdict */; }; + 2CD264972CFDC76A00A040A7 /* Plurals.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263DB2CFDC76800A040A7 /* Plurals.stringsdict */; }; + 2CD264982CFDC76A00A040A7 /* Plurals.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263DD2CFDC76800A040A7 /* Plurals.stringsdict */; }; + 2CD264992CFDC76A00A040A7 /* Plurals.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263DF2CFDC76800A040A7 /* Plurals.stringsdict */; }; + 2CD2649A2CFDC76A00A040A7 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263E02CFDC76800A040A7 /* String.swift */; }; + 2CD2649B2CFDC76A00A040A7 /* AppVersionInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263E22CFDC76800A040A7 /* AppVersionInfoProvider.swift */; }; + 2CD2649C2CFDC76A00A040A7 /* DefaultAppVersionInfoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263E32CFDC76800A040A7 /* DefaultAppVersionInfoProvider.swift */; }; + 2CD2649D2CFDC76A00A040A7 /* EcosiaInstallType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263E52CFDC76800A040A7 /* EcosiaInstallType.swift */; }; + 2CD2649E2CFDC76A00A040A7 /* EcosiaInstallType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263E62CFDC76800A040A7 /* EcosiaInstallType+Extensions.swift */; }; + 2CD2649F2CFDC76A00A040A7 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263E82CFDC76800A040A7 /* Version.swift */; }; + 2CD264A02CFDC76A00A040A7 /* Version+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263E92CFDC76800A040A7 /* Version+Extensions.swift */; }; + 2CD264A12CFDC76A00A040A7 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263EC2CFDC76800A040A7 /* Analytics.swift */; }; + 2CD264A22CFDC76A00A040A7 /* Analytics.Values.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263ED2CFDC76800A040A7 /* Analytics.Values.swift */; }; + 2CD264A32CFDC76A00A040A7 /* Analytics+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263EE2CFDC76800A040A7 /* Analytics+Configuration.swift */; }; + 2CD264A92CFDC76A00A040A7 /* markets.json in Resources */ = {isa = PBXBuildFile; fileRef = 2CD263F72CFDC76900A040A7 /* markets.json */; }; + 2CD264AC2CFDC76A00A040A7 /* FeatureManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD263FE2CFDC76900A040A7 /* FeatureManagement.swift */; }; + 2CD264AD2CFDC76A00A040A7 /* MMP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD264002CFDC76900A040A7 /* MMP.swift */; }; + 2CD264FF2CFDC76A00A040A7 /* FakeNimbus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD264722CFDC76A00A040A7 /* FakeNimbus.swift */; }; + 2CD265002CFDC76A00A040A7 /* FakeSentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD264732CFDC76A00A040A7 /* FakeSentry.swift */; }; + 2CD265012CFDC76A00A040A7 /* FakeTelemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD264742CFDC76A00A040A7 /* FakeTelemetry.swift */; }; + 2CD265522CFDCF0900A040A7 /* AppSettingsTableViewController+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265402CFDCF0900A040A7 /* AppSettingsTableViewController+Ecosia.swift */; }; + 2CD265532CFDCF0900A040A7 /* BrowserCoordinator+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265412CFDCF0900A040A7 /* BrowserCoordinator+Ecosia.swift */; }; + 2CD265542CFDCF0900A040A7 /* BrowserViewController+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265422CFDCF0900A040A7 /* BrowserViewController+Ecosia.swift */; }; + 2CD265562CFDCF0900A040A7 /* DispatchQueueHelper+BuildChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265442CFDCF0900A040A7 /* DispatchQueueHelper+BuildChannel.swift */; }; + 2CD265572CFDCF0900A040A7 /* ErrorPageHandler+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265452CFDCF0900A040A7 /* ErrorPageHandler+Ecosia.swift */; }; + 2CD265582CFDCF0900A040A7 /* HomepageViewController+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265462CFDCF0900A040A7 /* HomepageViewController+Ecosia.swift */; }; + 2CD265592CFDCF0900A040A7 /* LegacyThemeManager+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265472CFDCF0900A040A7 /* LegacyThemeManager+Ecosia.swift */; }; + 2CD2655A2CFDCF0900A040A7 /* NumberFormatter+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265482CFDCF0900A040A7 /* NumberFormatter+Ecosia.swift */; }; + 2CD2655B2CFDCF0900A040A7 /* SimpleToast+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265492CFDCF0900A040A7 /* SimpleToast+Ecosia.swift */; }; + 2CD2655C2CFDCF0900A040A7 /* SnapKit+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2654A2CFDCF0900A040A7 /* SnapKit+Ecosia.swift */; }; + 2CD2655D2CFDCF0900A040A7 /* UIButton+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2654B2CFDCF0900A040A7 /* UIButton+Ecosia.swift */; }; + 2CD2655E2CFDCF0900A040A7 /* UIFont+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2654C2CFDCF0900A040A7 /* UIFont+Ecosia.swift */; }; + 2CD2655F2CFDCF0900A040A7 /* UIView+maskedCorners.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2654D2CFDCF0900A040A7 /* UIView+maskedCorners.swift */; }; + 2CD265602CFDCF0900A040A7 /* URL+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2654E2CFDCF0900A040A7 /* URL+Ecosia.swift */; }; + 2CD265622CFDCF6D00A040A7 /* UIImage+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265612CFDCF6D00A040A7 /* UIImage+Ecosia.swift */; }; + 2CD265C52CFE382C00A040A7 /* MultiplyImpact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265652CFE382C00A040A7 /* MultiplyImpact.swift */; }; + 2CD265C62CFE382C00A040A7 /* MultiplyImpactStep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265662CFE382C00A040A7 /* MultiplyImpactStep.swift */; }; + 2CD265C72CFE382C00A040A7 /* AboutEcosiaSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265682CFE382C00A040A7 /* AboutEcosiaSection.swift */; }; + 2CD265C82CFE382C00A040A7 /* NTPAboutEcosiaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265692CFE382C00A040A7 /* NTPAboutEcosiaCell.swift */; }; + 2CD265C92CFE382C00A040A7 /* NTPAboutEcosiaCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2656A2CFE382C00A040A7 /* NTPAboutEcosiaCellViewModel.swift */; }; + 2CD265CB2CFE382C00A040A7 /* SeedProgressManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2656F2CFE382C00A040A7 /* SeedProgressManagerProtocol.swift */; }; + 2CD265CC2CFE382C00A040A7 /* UserDefaultsSeedProgressManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265702CFE382C00A040A7 /* UserDefaultsSeedProgressManager.swift */; }; + 2CD265CD2CFE382C00A040A7 /* ArcProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265722CFE382C00A040A7 /* ArcProgressView.swift */; }; + 2CD265CE2CFE382C00A040A7 /* NTPSeedCounterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265732CFE382C00A040A7 /* NTPSeedCounterCell.swift */; }; + 2CD265CF2CFE382C00A040A7 /* NTPSeedCounterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265742CFE382C00A040A7 /* NTPSeedCounterViewModel.swift */; }; + 2CD265D02CFE382C00A040A7 /* SeedCounterConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265752CFE382C00A040A7 /* SeedCounterConfig.swift */; }; + 2CD265D12CFE382C00A040A7 /* SeedCounterHiddenSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265762CFE382C00A040A7 /* SeedCounterHiddenSettings.swift */; }; + 2CD265D22CFE382C00A040A7 /* SeedCounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265772CFE382C00A040A7 /* SeedCounterView.swift */; }; + 2CD265D32CFE382C00A040A7 /* SeedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265782CFE382C00A040A7 /* SeedProgressView.swift */; }; + 2CD265D42CFE382C00A040A7 /* Sparkle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265792CFE382C00A040A7 /* Sparkle.swift */; }; + 2CD265D52CFE382C00A040A7 /* CustomizableNTPSettingConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2657B2CFE382C00A040A7 /* CustomizableNTPSettingConfig.swift */; }; + 2CD265D62CFE382C00A040A7 /* NTPCustomizationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2657C2CFE382C00A040A7 /* NTPCustomizationCell.swift */; }; + 2CD265D72CFE382C00A040A7 /* NTPCustomizationCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2657D2CFE382C00A040A7 /* NTPCustomizationCellViewModel.swift */; }; + 2CD265D82CFE382C00A040A7 /* ClimateImpactInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2657F2CFE382C00A040A7 /* ClimateImpactInfo.swift */; }; + 2CD265D92CFE382C00A040A7 /* NTPImpactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265802CFE382C00A040A7 /* NTPImpactCell.swift */; }; + 2CD265DA2CFE382C00A040A7 /* NTPImpactCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265812CFE382C00A040A7 /* NTPImpactCellViewModel.swift */; }; + 2CD265DB2CFE382C00A040A7 /* NTPImpactDividerFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265822CFE382C00A040A7 /* NTPImpactDividerFooter.swift */; }; + 2CD265DC2CFE382C00A040A7 /* NTPImpactRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265832CFE382C00A040A7 /* NTPImpactRowView.swift */; }; + 2CD265DD2CFE382C00A040A7 /* ProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265842CFE382C00A040A7 /* ProgressView.swift */; }; + 2CD265DE2CFE382C00A040A7 /* NTPLibaryCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265862CFE382C00A040A7 /* NTPLibaryCellViewModel.swift */; }; + 2CD265DF2CFE382C00A040A7 /* NTPLibraryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265872CFE382C00A040A7 /* NTPLibraryCell.swift */; }; + 2CD265E02CFE382C00A040A7 /* NTPLibraryShortcutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265882CFE382C00A040A7 /* NTPLibraryShortcutView.swift */; }; + 2CD265E12CFE382C00A040A7 /* NTPLogoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2658A2CFE382C00A040A7 /* NTPLogoCell.swift */; }; + 2CD265E22CFE382C00A040A7 /* NewsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2658C2CFE382C00A040A7 /* NewsController.swift */; }; + 2CD265E32CFE382C00A040A7 /* NTPNewsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2658D2CFE382C00A040A7 /* NTPNewsCell.swift */; }; + 2CD265E42CFE382C00A040A7 /* NTPNewsCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2658E2CFE382C00A040A7 /* NTPNewsCellViewModel.swift */; }; + 2CD265E52CFE382C00A040A7 /* NTPNewsletterCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265902CFE382C00A040A7 /* NTPNewsletterCardCell.swift */; }; + 2CD265E62CFE382C00A040A7 /* NTPNewsletterCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265912CFE382C00A040A7 /* NTPNewsletterCardViewModel.swift */; }; + 2CD265E72CFE382C00A040A7 /* NTPConfigurableNudgeCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265932CFE382C00A040A7 /* NTPConfigurableNudgeCardCell.swift */; }; + 2CD265E82CFE382C00A040A7 /* NTPConfigurableNudgeCardCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265942CFE382C00A040A7 /* NTPConfigurableNudgeCardCellViewModel.swift */; }; + 2CD265E92CFE382C00A040A7 /* CircleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265962CFE382C00A040A7 /* CircleButton.swift */; }; + 2CD265EA2CFE382C00A040A7 /* DefaultBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265972CFE382C00A040A7 /* DefaultBrowser.swift */; }; + 2CD265EB2CFE382C00A040A7 /* NTPLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265982CFE382C00A040A7 /* NTPLayout.swift */; }; + 2CD265EC2CFE382C00A040A7 /* NTPTooltip.Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265992CFE382C00A040A7 /* NTPTooltip.Highlight.swift */; }; + 2CD265ED2CFE382C00A040A7 /* NTPTooltip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2659A2CFE382C00A040A7 /* NTPTooltip.swift */; }; + 2CD265EE2CFE382C00A040A7 /* NTPTooltipDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2659B2CFE382C00A040A7 /* NTPTooltipDelegate.swift */; }; + 2CD265EF2CFE382C00A040A7 /* Welcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2659D2CFE382C00A040A7 /* Welcome.swift */; }; + 2CD265F02CFE382C00A040A7 /* WelcomeNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2659E2CFE382C00A040A7 /* WelcomeNavigation.swift */; }; + 2CD265F12CFE382C00A040A7 /* WelcomeTour.Step.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2659F2CFE382C00A040A7 /* WelcomeTour.Step.swift */; }; + 2CD265F22CFE382C00A040A7 /* WelcomeTour.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265A02CFE382C00A040A7 /* WelcomeTour.swift */; }; + 2CD265F32CFE382C00A040A7 /* WelcomeTourAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265A12CFE382C00A040A7 /* WelcomeTourAction.swift */; }; + 2CD265F42CFE382C00A040A7 /* WelcomeTourGreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265A22CFE382C00A040A7 /* WelcomeTourGreen.swift */; }; + 2CD265F52CFE382C00A040A7 /* WelcomeTourProfit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265A32CFE382C00A040A7 /* WelcomeTourProfit.swift */; }; + 2CD265F62CFE382C00A040A7 /* WelcomeTourRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265A42CFE382C00A040A7 /* WelcomeTourRow.swift */; }; + 2CD265F72CFE382C00A040A7 /* WelcomeTourTransparent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265A52CFE382C00A040A7 /* WelcomeTourTransparent.swift */; }; + 2CD265F82CFE382C00A040A7 /* PageActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265A72CFE382C00A040A7 /* PageActionMenu.swift */; }; + 2CD265F92CFE382C00A040A7 /* PageActionMenuCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265A82CFE382C00A040A7 /* PageActionMenuCell.swift */; }; + 2CD265FA2CFE382C00A040A7 /* PageActionsShortcutsHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265A92CFE382C00A040A7 /* PageActionsShortcutsHeader.swift */; }; + 2CD265FB2CFE382C00A040A7 /* EcosiaTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265AB2CFE382C00A040A7 /* EcosiaTheme.swift */; }; + 2CD265FC2CFE382C00A040A7 /* EcosiaThemeColourPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265AC2CFE382C00A040A7 /* EcosiaThemeColourPalette.swift */; }; + 2CD265FD2CFE382C00A040A7 /* EcosiaThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265AD2CFE382C00A040A7 /* EcosiaThemeManager.swift */; }; + 2CD265FE2CFE382C00A040A7 /* WhatsNewDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265AF2CFE382C00A040A7 /* WhatsNewDataProvider.swift */; }; + 2CD265FF2CFE382C00A040A7 /* WhatsNewLocalDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265B02CFE382C00A040A7 /* WhatsNewLocalDataProvider.swift */; }; + 2CD266002CFE382C00A040A7 /* WhatsNewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265B22CFE382C00A040A7 /* WhatsNewCell.swift */; }; + 2CD266012CFE382C00A040A7 /* WhatsNewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265B32CFE382C00A040A7 /* WhatsNewItem.swift */; }; + 2CD266022CFE382C00A040A7 /* WhatsNewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265B42CFE382C00A040A7 /* WhatsNewViewController.swift */; }; + 2CD266032CFE382C00A040A7 /* WhatsNewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265B52CFE382C00A040A7 /* WhatsNewViewModel.swift */; }; + 2CD266042CFE382C00A040A7 /* Colours.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CD265B72CFE382C00A040A7 /* Colours.xcassets */; }; + 2CD266052CFE382C00A040A7 /* Ecosia.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2CD265B82CFE382C00A040A7 /* Ecosia.xcassets */; }; + 2CD266062CFE382C00A040A7 /* EcosiaFindInPageBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265B92CFE382C00A040A7 /* EcosiaFindInPageBar.swift */; }; + 2CD266072CFE382C00A040A7 /* EcosiaNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265BA2CFE382C00A040A7 /* EcosiaNavigation.swift */; }; + 2CD266082CFE382C00A040A7 /* EmptyBookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265BB2CFE382C00A040A7 /* EmptyBookmarksView.swift */; }; + 2CD266092CFE382C00A040A7 /* EmptyBookmarksViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265BC2CFE382C00A040A7 /* EmptyBookmarksViewDelegate.swift */; }; + 2CD2660A2CFE382C00A040A7 /* EmptyHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265BD2CFE382C00A040A7 /* EmptyHeader.swift */; }; + 2CD2660B2CFE382C00A040A7 /* EmptyReadingListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265BE2CFE382C00A040A7 /* EmptyReadingListView.swift */; }; + 2CD2660C2CFE382C00A040A7 /* FilterController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265BF2CFE382C00A040A7 /* FilterController.swift */; }; + 2CD2660D2CFE382C00A040A7 /* LoadingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265C02CFE382C00A040A7 /* LoadingScreen.swift */; }; + 2CD2660E2CFE382C00A040A7 /* MarketsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265C12CFE382C00A040A7 /* MarketsController.swift */; }; + 2CD2660F2CFE382C00A040A7 /* SemanticColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265C22CFE382C00A040A7 /* SemanticColor.swift */; }; + 2CD2661A2CFE38AD00A040A7 /* EcosiaTopSiteItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD266102CFE38AD00A040A7 /* EcosiaTopSiteItemCell.swift */; }; + 2CD2661B2CFE38AD00A040A7 /* EcosiaHomepageSectionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD266132CFE38AD00A040A7 /* EcosiaHomepageSectionType.swift */; }; + 2CD2661C2CFE38AD00A040A7 /* EcosiaDebugSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD266162CFE38AD00A040A7 /* EcosiaDebugSettings.swift */; }; + 2CD2661D2CFE38AD00A040A7 /* EcosiaSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD266172CFE38AD00A040A7 /* EcosiaSettings.swift */; }; + 2CD2661E2CFE38AD00A040A7 /* NTPCustomizationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD266182CFE38AD00A040A7 /* NTPCustomizationSettingsViewController.swift */; }; + 2CD266202CFE39E300A040A7 /* EcosiaPrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2661F2CFE39E300A040A7 /* EcosiaPrimaryButton.swift */; }; + 2CD266232CFE3ADA00A040A7 /* BookmarksExchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD266212CFE3AD900A040A7 /* BookmarksExchange.swift */; }; + 2CD2662B2CFE402000A040A7 /* SeedCounterNTPExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2662A2CFE402000A040A7 /* SeedCounterNTPExperiment.swift */; }; + 2CD266332CFE403800A040A7 /* BrazeIntegrationExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2662D2CFE403800A040A7 /* BrazeIntegrationExperiment.swift */; }; + 2CD266342CFE403800A040A7 /* NewsletterCardExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2662E2CFE403800A040A7 /* NewsletterCardExperiment.swift */; }; + 2CD2663C2CFE423D00A040A7 /* AppInfo+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2663B2CFE423D00A040A7 /* AppInfo+Ecosia.swift */; }; + 2CD2663E2CFF4ED000A040A7 /* DeviceInfo+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2663D2CFF4ED000A040A7 /* DeviceInfo+Ecosia.swift */; }; + 2CD2663F2CFF4FA300A040A7 /* EcosiaTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265AB2CFE382C00A040A7 /* EcosiaTheme.swift */; }; + 2CD266402CFF4FA300A040A7 /* EcosiaTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265AB2CFE382C00A040A7 /* EcosiaTheme.swift */; }; + 2CD266412CFF4FB200A040A7 /* LegacyThemeManager+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265472CFDCF0900A040A7 /* LegacyThemeManager+Ecosia.swift */; }; + 2CD266422CFF4FB200A040A7 /* LegacyThemeManager+Ecosia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265472CFDCF0900A040A7 /* LegacyThemeManager+Ecosia.swift */; }; + 2CD266432CFF4FC300A040A7 /* SemanticColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265C22CFE382C00A040A7 /* SemanticColor.swift */; }; + 2CD266442CFF4FC400A040A7 /* SemanticColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265C22CFE382C00A040A7 /* SemanticColor.swift */; }; + 2CD266452CFF4FD100A040A7 /* EcosiaThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265AD2CFE382C00A040A7 /* EcosiaThemeManager.swift */; }; + 2CD266462CFF4FD200A040A7 /* EcosiaThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265AD2CFE382C00A040A7 /* EcosiaThemeManager.swift */; }; + 2CD266472CFF4FE800A040A7 /* EcosiaThemeColourPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265AC2CFE382C00A040A7 /* EcosiaThemeColourPalette.swift */; }; + 2CD266482CFF4FE800A040A7 /* EcosiaThemeColourPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD265AC2CFE382C00A040A7 /* EcosiaThemeColourPalette.swift */; }; + 2CD266512CFF56CB00A040A7 /* ConnectionStatusImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2664E2CFF56CB00A040A7 /* ConnectionStatusImage.swift */; }; + 2CD266522CFF56CB00A040A7 /* ConnectionStatusImage+WebsiteConnectionTypeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2664F2CFF56CB00A040A7 /* ConnectionStatusImage+WebsiteConnectionTypeStatus.swift */; }; + 2CD266552CFF56EA00A040A7 /* WebsiteConnectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD266532CFF56EA00A040A7 /* WebsiteConnectionStatus.swift */; }; + 2CD7E5F72D09DF5A0003B02B /* MockURLBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9A09D528B01FD500B6F51E /* MockURLBarView.swift */; }; + 2CD7E5F82D09DF600003B02B /* MockOverlayModeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B41A1B298B1876008BC0A2 /* MockOverlayModeManager.swift */; }; + 2CD7E5F92D09DF750003B02B /* ProfileTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA896491A250E6500C1010C /* ProfileTest.swift */; }; + 2CD7E5FA2D09DF7E0003B02B /* TopSitesHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A5BD9582878871B000FE773 /* TopSitesHelperTests.swift */; }; + 2CD7E5FB2D09DFE30003B02B /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262902CFDC5E900A040A7 /* String+Extension.swift */; }; + 2CD7E5FC2D09DFEA0003B02B /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262902CFDC5E900A040A7 /* String+Extension.swift */; }; + 2CD7E5FD2D09E19E0003B02B /* MockablePinnedSites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D5EDBF292D619000311934 /* MockablePinnedSites.swift */; }; + 2CD7E5FE2D09E1A80003B02B /* MockTabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A36AC2B2886F27F00CDC0AD /* MockTabManager.swift */; }; + 2CD7E6002D09E3A50003B02B /* DependencyHelperMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A70EF18295E2E1600790249 /* DependencyHelperMock.swift */; }; + 2CD7E6012D09E3B30003B02B /* XCTestCaseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3BA41671BD82F2200DA5457 /* XCTestCaseExtensions.swift */; }; + 2CD7E6022D09E3C20003B02B /* MockThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AA75A622A46272000533F8D /* MockThemeManager.swift */; }; + 2CD7E6032D09E3E30003B02B /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD262882CFDC5E900A040A7 /* DeviceType.swift */; }; + 2CD7E6042D09E3ED0003B02B /* LocalizationOverrideTestingBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD2628C2CFDC5E900A040A7 /* LocalizationOverrideTestingBundle.swift */; }; + 2CD7E6052D09E43B0003B02B /* MockTabDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A475E8C29DB888E009C13FD /* MockTabDataStore.swift */; }; + 2CD7E6072D09E47E0003B02B /* Bundle+EcosiaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD7E6062D09E47E0003B02B /* Bundle+EcosiaTests.swift */; }; + 2CD7E6082D09E5DF0003B02B /* RustMozillaAppServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43BE578A278BA4D900491291 /* RustMozillaAppServices.framework */; }; + 2CD7E60A2D09E8B00003B02B /* environment.json in Resources */ = {isa = PBXBuildFile; fileRef = 2CD2628A2CFDC5E900A040A7 /* environment.json */; }; + 2CD7E60B2D09E9D60003B02B /* snapshot_configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = 2CD2628D2CFDC5E900A040A7 /* snapshot_configuration.json */; }; 2F13E79B1AC0C02700D75081 /* StringExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F13E79A1AC0C02700D75081 /* StringExtensionsTests.swift */; }; 2F44FA1B1A9D426A00FD20CC /* TestHashExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F44FA1A1A9D426A00FD20CC /* TestHashExtensions.swift */; }; 2F44FB2C1A9D5D8500FD20CC /* Library.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F84B22261A09127C00AAB793 /* Library.xcassets */; }; @@ -1618,6 +1782,13 @@ remoteGlobalIDString = 047F9B2624E1FE1C00CD7DF7; remoteInfo = WidgetKitExtension; }; + 1229357D2CE78D0A00EC1297 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 122935692CE78D0A00EC1297; + remoteInfo = Ecosia; + }; 2827316A1ABC9BE700AA1954 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F84B21B61A090F8100AAB793 /* Project object */; @@ -1646,6 +1817,13 @@ remoteGlobalIDString = 2FCAE2191ABB51F800877008; remoteInfo = Storage; }; + 2C21012C2D0B0FAF00CBE7EC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F84B21BD1A090F8100AAB793; + remoteInfo = Client; + }; 2C6C90862C614A17007D9B43 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F84B21B61A090F8100AAB793 /* Project object */; @@ -1653,6 +1831,13 @@ remoteGlobalIDString = F84B21BD1A090F8100AAB793; remoteInfo = Client; }; + 2CAF5C182D01B2E200D3DCDD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F84B21B61A090F8100AAB793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 122935692CE78D0A00EC1297; + remoteInfo = Ecosia; + }; 2F11EE4F1ABCAE910083902D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F84B21B61A090F8100AAB793 /* Project object */; @@ -1827,12 +2012,13 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 4368F838279665E00013419B /* Embed Frameworks */ = { + 2CAF5C1A2D01B2E200D3DCDD /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( + 2CAF5C172D01B2E200D3DCDD /* Ecosia.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -1853,6 +2039,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 122935802CE78D0A00EC1297 /* Ecosia.framework in Copy Frameworks */, 43017ECB278E0C6700CED011 /* RustMozillaAppServices.framework in Copy Frameworks */, C82043C32523DD6A00740B71 /* Sync.framework in Copy Frameworks */, C87703D225223EA5006E38EB /* Shared.framework in Copy Frameworks */, @@ -2020,17 +2207,16 @@ 10CD44F0A402C84BB31E5474 /* gu-IN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "gu-IN"; path = "gu-IN.lproj/Intro.strings"; sourceTree = ""; }; 11F747589EB8A55A47647C93 /* bn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bn; path = bn.lproj/ClearPrivateData.strings; sourceTree = ""; }; 120F42119EB30F217AB9493E /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/FindInPage.strings; sourceTree = ""; }; - 12147F2E2CDA3CD00009D300 /* NTPNewsletterCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPNewsletterCardCell.swift; sourceTree = ""; }; - 12147F302CDA3CD80009D300 /* NTPNewsletterCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPNewsletterCardViewModel.swift; sourceTree = ""; }; - 12147F322CDBA7230009D300 /* NewsletterCardExperiment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsletterCardExperiment.swift; sourceTree = ""; }; + 1229356A2CE78D0A00EC1297 /* Ecosia.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Ecosia.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1229356C2CE78D0A00EC1297 /* Ecosia.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Ecosia.h; sourceTree = ""; }; + 122935732CE78D0A00EC1297 /* EcosiaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EcosiaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 1229359D2CE792B700EC1297 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 123045959E0F295753B4B4DB /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/Today.strings; sourceTree = ""; }; - 126509812CD924C00011BA36 /* BrazeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrazeService.swift; sourceTree = ""; }; 126509882CDA31890011BA36 /* BrazeServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrazeServiceTests.swift; sourceTree = ""; }; 12674A038346A46589A0AC0B /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = "el.lproj/Default Browser.strings"; sourceTree = ""; }; 126A40A4A5AFDFD655B0FDF4 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/ClearHistoryConfirm.strings; sourceTree = ""; }; 126F44CCB14373DC7813DE1F /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/ClearPrivateDataConfirm.strings; sourceTree = ""; }; - 1285E2B42CC293CA0053506B /* AnalyticsSpyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSpyTests.swift; sourceTree = ""; }; - 1285E2B62CC68BF00053506B /* APNConsent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNConsent.swift; sourceTree = ""; }; + 12E604442CECADDA009A7BEC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 12EA4881BFBE296298150D4A /* bn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bn; path = bn.lproj/LoginManager.strings; sourceTree = ""; }; 12F949169C30744CCC749588 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/HistoryPanel.strings; sourceTree = ""; }; 1323403C8071FC19BB79C191 /* su */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = su; path = su.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2360,15 +2546,14 @@ 2BAA411FAF1D49B9990A7720 /* sat-Olck */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sat-Olck"; path = "sat-Olck.lproj/Shared.strings"; sourceTree = ""; }; 2BAB4A40A318F5A577488909 /* mr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mr; path = mr.lproj/InfoPlist.strings; sourceTree = ""; }; 2C0360D92C1747E6006706F2 /* FxNimbus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FxNimbus.swift; sourceTree = ""; }; - 2C03A4142CB7C7CC00AB228B /* DispatchQueueHelper+BuildChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueueHelper+BuildChannel.swift"; sourceTree = ""; }; - 2C08319B2B89127200BD7134 /* Ecosia.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ecosia.entitlements; sourceTree = ""; }; - 2C08319C2B89127200BD7134 /* EcosiaBeta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = EcosiaBeta.entitlements; sourceTree = ""; }; 2C1298A42BF5EB16005AE4E4 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; - 2C16B7652CAF2425006118F8 /* UserDefaultsSeedProgressManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsSeedProgressManagerTests.swift; sourceTree = ""; }; - 2C19DACE2C74C7BF00D2641C /* snapshot_configuration.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = snapshot_configuration.json; sourceTree = ""; }; - 2C2349A22C57E5BC007A5894 /* EcosiaPerformanceTestHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EcosiaPerformanceTestHistory.swift; sourceTree = ""; }; + 2C21011C2D0B042300CBE7EC /* AnalyticsNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsNotificationSettings.swift; sourceTree = ""; }; + 2C21011E2D0B04E800CBE7EC /* APNConsent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APNConsent.swift; sourceTree = ""; }; + 2C21011F2D0B04E800CBE7EC /* BrazeService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrazeService.swift; sourceTree = ""; }; + 2C2101242D0B0D9B00CBE7EC /* MockNewsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNewsModel.swift; sourceTree = ""; }; + 2C2101252D0B0D9B00CBE7EC /* MockWelcomeDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockWelcomeDelegate.swift; sourceTree = ""; }; + 2C2101262D0B0D9B00CBE7EC /* MockUNNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockUNNotificationSettings.swift; sourceTree = ""; }; 2C26401D9CCB8C2671EA2431 /* ta */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ta; path = ta.lproj/3DTouchActions.strings; sourceTree = ""; }; - 2C26EA132C04CAD100795552 /* EcosiaTopSitesHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EcosiaTopSitesHelperTests.swift; sourceTree = ""; }; 2C2A5EF31E68469500F02659 /* PrivateBrowsingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateBrowsingTest.swift; sourceTree = ""; }; 2C2A91281FA2410D002E36BD /* HistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTests.swift; sourceTree = ""; }; 2C31A7A81E8BFB2200DAC646 /* ReadingListTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadingListTests.swift; sourceTree = ""; }; @@ -2378,162 +2563,149 @@ 2C473BCF209778900008C853 /* DownloadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTests.swift; sourceTree = ""; }; 2C49854D206173C800893DAE /* photon-colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "photon-colors.swift"; sourceTree = ""; }; 2C4A07DB20246EAD0083E320 /* DragAndDropTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragAndDropTests.swift; sourceTree = ""; }; - 2C4ABD482CB58E4F00FF86F9 /* Sparkle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sparkle.swift; sourceTree = ""; }; 2C4B6BF220349EB800A009C2 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; - 2C4D165F2C76360800E89C95 /* environment.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = environment.json; sourceTree = ""; }; - 2C5A5E642CB53DB7005BFE8B /* SeedCounterConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedCounterConfig.swift; sourceTree = ""; }; - 2C5A5E662CB53DF9005BFE8B /* UserDefaultsSeedProgressManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsSeedProgressManager.swift; sourceTree = ""; }; - 2C5B81C72C75388300B81D95 /* LocaleRetriever.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleRetriever.swift; sourceTree = ""; }; 2C6045859589979C43AF09E0 /* jv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = jv; path = jv.lproj/ClearPrivateDataConfirm.strings; sourceTree = ""; }; - 2C6188802B7A8A21006B70D7 /* EcosiaLaunchScreenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaLaunchScreenView.swift; sourceTree = ""; }; - 2C6188812B7A8A21006B70D7 /* EcosiaLaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = EcosiaLaunchScreen.xib; sourceTree = ""; }; - 2C6188832B7A8A21006B70D7 /* MMP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MMP.swift; sourceTree = ""; }; - 2C6188852B7A8A21006B70D7 /* SemanticColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SemanticColor.swift; sourceTree = ""; }; - 2C6188872B7A8A21006B70D7 /* PageActionMenuCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageActionMenuCell.swift; sourceTree = ""; }; - 2C6188882B7A8A21006B70D7 /* PageActionsShortcutsHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageActionsShortcutsHeader.swift; sourceTree = ""; }; - 2C6188892B7A8A21006B70D7 /* PageActionMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageActionMenu.swift; sourceTree = ""; }; - 2C61888A2B7A8A21006B70D7 /* EmptyBookmarksViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyBookmarksViewDelegate.swift; sourceTree = ""; }; - 2C61888B2B7A8A21006B70D7 /* EcosiaFindInPageBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaFindInPageBar.swift; sourceTree = ""; }; - 2C61888C2B7A8A21006B70D7 /* Ecosia.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Ecosia.xcassets; sourceTree = ""; }; - 2C61888D2B7A8A21006B70D7 /* EmptyReadingListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyReadingListView.swift; sourceTree = ""; }; - 2C6188942B7A8A21006B70D7 /* EmptyHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyHeader.swift; sourceTree = ""; }; - 2C6188962B7A8A21006B70D7 /* WhatsNewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewCell.swift; sourceTree = ""; }; - 2C6188972B7A8A21006B70D7 /* WhatsNewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewViewModel.swift; sourceTree = ""; }; - 2C6188982B7A8A21006B70D7 /* WhatsNewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewViewController.swift; sourceTree = ""; }; - 2C6188992B7A8A21006B70D7 /* WhatsNewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewItem.swift; sourceTree = ""; }; - 2C61889B2B7A8A21006B70D7 /* WhatsNewDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewDataProvider.swift; sourceTree = ""; }; - 2C61889C2B7A8A21006B70D7 /* WhatsNewLocalDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewLocalDataProvider.swift; sourceTree = ""; }; - 2C61889E2B7A8A21006B70D7 /* NTPLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPLayout.swift; sourceTree = ""; }; - 2C61889F2B7A8A21006B70D7 /* DefaultBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultBrowser.swift; sourceTree = ""; }; - 2C6188A12B7A8A21006B70D7 /* CustomizableNTPSettingConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizableNTPSettingConfig.swift; sourceTree = ""; }; - 2C6188A22B7A8A21006B70D7 /* NTPCustomizationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPCustomizationCell.swift; sourceTree = ""; }; - 2C6188A32B7A8A21006B70D7 /* NTPCustomizationCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPCustomizationCellViewModel.swift; sourceTree = ""; }; - 2C6188A52B7A8A21006B70D7 /* NTPLibraryShortcutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPLibraryShortcutView.swift; sourceTree = ""; }; - 2C6188A62B7A8A21006B70D7 /* NTPLibraryCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPLibraryCell.swift; sourceTree = ""; }; - 2C6188A72B7A8A21006B70D7 /* NTPLibaryCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPLibaryCellViewModel.swift; sourceTree = ""; }; - 2C6188A82B7A8A21006B70D7 /* NTPTooltip.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPTooltip.swift; sourceTree = ""; }; - 2C6188A92B7A8A21006B70D7 /* CircleButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleButton.swift; sourceTree = ""; }; - 2C6188AA2B7A8A21006B70D7 /* NTPTooltip.Highlight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPTooltip.Highlight.swift; sourceTree = ""; }; - 2C6188AF2B7A8A22006B70D7 /* NewsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsController.swift; sourceTree = ""; }; - 2C6188B02B7A8A22006B70D7 /* NTPNewsCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPNewsCellViewModel.swift; sourceTree = ""; }; - 2C6188B12B7A8A22006B70D7 /* NTPNewsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPNewsCell.swift; sourceTree = ""; }; - 2C6188B32B7A8A22006B70D7 /* NTPLogoCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPLogoCell.swift; sourceTree = ""; }; - 2C6188B42B7A8A22006B70D7 /* NTPTooltipDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPTooltipDelegate.swift; sourceTree = ""; }; - 2C6188B62B7A8A22006B70D7 /* ProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressView.swift; sourceTree = ""; }; - 2C6188B72B7A8A22006B70D7 /* NTPImpactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPImpactCell.swift; sourceTree = ""; }; - 2C6188B82B7A8A22006B70D7 /* NTPImpactCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPImpactCellViewModel.swift; sourceTree = ""; }; - 2C6188B92B7A8A22006B70D7 /* NTPImpactRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPImpactRowView.swift; sourceTree = ""; }; - 2C6188BA2B7A8A22006B70D7 /* NTPImpactDividerFooter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPImpactDividerFooter.swift; sourceTree = ""; }; - 2C6188BB2B7A8A22006B70D7 /* ClimateImpactInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClimateImpactInfo.swift; sourceTree = ""; }; - 2C6188BD2B7A8A22006B70D7 /* AboutEcosiaSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutEcosiaSection.swift; sourceTree = ""; }; - 2C6188BE2B7A8A22006B70D7 /* NTPAboutEcosiaCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPAboutEcosiaCellViewModel.swift; sourceTree = ""; }; - 2C6188BF2B7A8A22006B70D7 /* NTPAboutEcosiaCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPAboutEcosiaCell.swift; sourceTree = ""; }; - 2C6188C02B7A8A22006B70D7 /* Colours.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colours.xcassets; sourceTree = ""; }; - 2C6188C12B7A8A22006B70D7 /* EcosiaNavigation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaNavigation.swift; sourceTree = ""; }; - 2C6188C32B7A8A22006B70D7 /* EcosiaThemeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaThemeManager.swift; sourceTree = ""; }; - 2C6188C42B7A8A22006B70D7 /* EcosiaThemeColourPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaThemeColourPalette.swift; sourceTree = ""; }; - 2C6188C52B7A8A22006B70D7 /* EcosiaTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaTheme.swift; sourceTree = ""; }; - 2C6188C62B7A8A22006B70D7 /* FilterController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterController.swift; sourceTree = ""; }; - 2C6188C72B7A8A22006B70D7 /* MarketsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketsController.swift; sourceTree = ""; }; - 2C6188C82B7A8A22006B70D7 /* EmptyBookmarksView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyBookmarksView.swift; sourceTree = ""; }; - 2C6188CA2B7A8A22006B70D7 /* WelcomeTourRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTourRow.swift; sourceTree = ""; }; - 2C6188CB2B7A8A22006B70D7 /* Welcome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; - 2C6188CC2B7A8A22006B70D7 /* WelcomeTour.Step.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTour.Step.swift; sourceTree = ""; }; - 2C6188CD2B7A8A22006B70D7 /* WelcomeTourAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTourAction.swift; sourceTree = ""; }; - 2C6188CE2B7A8A22006B70D7 /* WelcomeTour.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTour.swift; sourceTree = ""; }; - 2C6188CF2B7A8A22006B70D7 /* WelcomeTourTransparent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTourTransparent.swift; sourceTree = ""; }; - 2C6188D02B7A8A22006B70D7 /* WelcomeTourGreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTourGreen.swift; sourceTree = ""; }; - 2C6188D12B7A8A22006B70D7 /* WelcomeNavigation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeNavigation.swift; sourceTree = ""; }; - 2C6188D22B7A8A22006B70D7 /* WelcomeTourProfit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTourProfit.swift; sourceTree = ""; }; - 2C6188D42B7A8A22006B70D7 /* MultiplyImpact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplyImpact.swift; sourceTree = ""; }; - 2C6188D52B7A8A22006B70D7 /* MultiplyImpactStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplyImpactStep.swift; sourceTree = ""; }; - 2C6188D72B7A8A22006B70D7 /* EcosiaDebugSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaDebugSettings.swift; sourceTree = ""; }; - 2C6188D82B7A8A22006B70D7 /* EcosiaSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaSettings.swift; sourceTree = ""; }; - 2C6188D92B7A8A22006B70D7 /* NTPCustomizationSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPCustomizationSettingsViewController.swift; sourceTree = ""; }; - 2C6188DC2B7A8A22006B70D7 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Ecosia.strings; sourceTree = ""; }; - 2C6188DE2B7A8A22006B70D7 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Plurals.stringsdict; sourceTree = ""; }; - 2C6188DF2B7A8A22006B70D7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Ecosia.strings; sourceTree = ""; }; - 2C6188E02B7A8A22006B70D7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Plurals.stringsdict; sourceTree = ""; }; - 2C6188E12B7A8A22006B70D7 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Ecosia.strings; sourceTree = ""; }; - 2C6188E22B7A8A22006B70D7 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Plurals.stringsdict; sourceTree = ""; }; - 2C6188E32B7A8A22006B70D7 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Ecosia.strings; sourceTree = ""; }; - 2C6188E42B7A8A22006B70D7 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Plurals.stringsdict; sourceTree = ""; }; - 2C6188E62B7A8A22006B70D7 /* Clean.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Clean.swift; sourceTree = ""; }; - 2C6188E72B7A8A22006B70D7 /* Validate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Validate.swift; sourceTree = ""; }; - 2C6188E82B7A8A22006B70D7 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; - 2C6188E92B7A8A22006B70D7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Ecosia.strings; sourceTree = ""; }; - 2C6188EA2B7A8A22006B70D7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Plurals.stringsdict; sourceTree = ""; }; - 2C6188EB2B7A8A22006B70D7 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Ecosia.strings; sourceTree = ""; }; - 2C6188EC2B7A8A22006B70D7 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Plurals.stringsdict; sourceTree = ""; }; - 2C6188EE2B7A8A22006B70D7 /* FeatureManagement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureManagement.swift; sourceTree = ""; }; - 2C6188F12B7A8A22006B70D7 /* EcosiaHomepageSectionType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaHomepageSectionType.swift; sourceTree = ""; }; - 2C6188F42B7A8A22006B70D7 /* EcosiaTopSiteItemCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaTopSiteItemCell.swift; sourceTree = ""; }; - 2C6188F82B7A8A22006B70D7 /* BrazeIntegrationExperiment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrazeIntegrationExperiment.swift; sourceTree = ""; }; - 2C6188FB2B7A8A22006B70D7 /* WebsiteConnectionStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebsiteConnectionStatus.swift; sourceTree = ""; }; - 2C6189002B7A8A22006B70D7 /* BrowserViewController+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+Ecosia.swift"; sourceTree = ""; }; - 2C6189012B7A8A22006B70D7 /* LegacyThemeManager+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LegacyThemeManager+Ecosia.swift"; sourceTree = ""; }; - 2C6189022B7A8A22006B70D7 /* UIButton+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Ecosia.swift"; sourceTree = ""; }; - 2C6189032B7A8A22006B70D7 /* UIFont+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIFont+Ecosia.swift"; sourceTree = ""; }; - 2C6189042B7A8A22006B70D7 /* ErrorPageHandler+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ErrorPageHandler+Ecosia.swift"; sourceTree = ""; }; - 2C6189052B7A8A22006B70D7 /* SnapKit+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SnapKit+Ecosia.swift"; sourceTree = ""; }; - 2C6189062B7A8A22006B70D7 /* HomepageViewController+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HomepageViewController+Ecosia.swift"; sourceTree = ""; }; - 2C6189072B7A8A22006B70D7 /* SimpleToast+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimpleToast+Ecosia.swift"; sourceTree = ""; }; - 2C6189082B7A8A22006B70D7 /* UIView+maskedCorners.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+maskedCorners.swift"; sourceTree = ""; }; - 2C6189092B7A8A22006B70D7 /* AppInfo+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppInfo+Ecosia.swift"; sourceTree = ""; }; - 2C61890A2B7A8A22006B70D7 /* NumberFormatter+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+Ecosia.swift"; sourceTree = ""; }; - 2C61890B2B7A8A22006B70D7 /* URL+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+Ecosia.swift"; sourceTree = ""; }; - 2C61890C2B7A8A22006B70D7 /* BrowserCoordinator+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BrowserCoordinator+Ecosia.swift"; sourceTree = ""; }; - 2C61890D2B7A8A22006B70D7 /* AppSettingsTableViewController+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppSettingsTableViewController+Ecosia.swift"; sourceTree = ""; }; - 2C61890E2B7A8A22006B70D7 /* DeviceInfo+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DeviceInfo+Ecosia.swift"; sourceTree = ""; }; - 2C6189102B7A8A22006B70D7 /* ConnectionStatusImage+WebsiteConnectionTypeStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConnectionStatusImage+WebsiteConnectionTypeStatus.swift"; sourceTree = ""; }; - 2C6189122B7A8A22006B70D7 /* ConnectionStatusImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionStatusImage.swift; sourceTree = ""; }; - 2C6189142B7A8A22006B70D7 /* EcosiaBeta.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = EcosiaBeta.entitlements; sourceTree = ""; }; - 2C6189152B7A8A22006B70D7 /* Ecosia.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Ecosia.entitlements; sourceTree = ""; }; - 2C6189172B7A8A22006B70D7 /* BookmarksExchange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksExchange.swift; sourceTree = ""; }; - 2C6189182B7A8A22006B70D7 /* markets.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = markets.json; sourceTree = ""; }; - 2C61891A2B7A8A22006B70D7 /* FakeTelemetry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeTelemetry.swift; sourceTree = ""; }; - 2C61891B2B7A8A22006B70D7 /* FakeNimbus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeNimbus.swift; sourceTree = ""; }; - 2C61891C2B7A8A22006B70D7 /* FakeSentry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeSentry.swift; sourceTree = ""; }; - 2C6189252B7A8A22006B70D7 /* DefaultAppVersionInfoProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultAppVersionInfoProvider.swift; sourceTree = ""; }; - 2C6189262B7A8A22006B70D7 /* AppVersionInfoProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppVersionInfoProvider.swift; sourceTree = ""; }; - 2C6189282B7A8A22006B70D7 /* EcosiaInstallType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaInstallType.swift; sourceTree = ""; }; - 2C6189292B7A8A22006B70D7 /* EcosiaInstallType+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EcosiaInstallType+Extensions.swift"; sourceTree = ""; }; - 2C61892B2B7A8A22006B70D7 /* Version.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; - 2C61892C2B7A8A22006B70D7 /* Version+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Version+Extensions.swift"; sourceTree = ""; }; - 2C61892E2B7A8A22006B70D7 /* Analytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; }; - 2C61892F2B7A8A22006B70D7 /* Analytics+Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Analytics+Configuration.swift"; sourceTree = ""; }; - 2C6189302B7A8A22006B70D7 /* Analytics.Values.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Analytics.Values.swift; sourceTree = ""; }; - 2C69DA712C62175400D7F69F /* SnapshotTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotTestHelper.swift; sourceTree = ""; }; - 2C69DA742C62185A00D7F69F /* Welcome.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; - 2C69DA762C62243300D7F69F /* NTPTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPTests.swift; sourceTree = ""; }; - 2C69DA832C62B44B00D7F69F /* NTPComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPComponentTests.swift; sourceTree = ""; }; - 2C69DA912C63A92D00D7F69F /* SnapshotBaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotBaseTests.swift; sourceTree = ""; }; - 2C69DA932C63B0C000D7F69F /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; - 2C6B5B3A2CAAF27F00F15323 /* NTPSeedCounterCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPSeedCounterCell.swift; sourceTree = ""; }; - 2C6B5B3B2CAAF27F00F15323 /* NTPSeedCounterViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPSeedCounterViewModel.swift; sourceTree = ""; }; - 2C6B5B402CAAF3AA00F15323 /* SeedCounterNTPExperiment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedCounterNTPExperiment.swift; sourceTree = ""; }; 2C6C90822C614A16007D9B43 /* EcosiaSnapshotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EcosiaSnapshotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2C6E44099EACF7BE5438CEB6 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Today.strings; sourceTree = ""; }; - 2C728D7D2CBBDCDC00C7684B /* UnleashUserDefaultsSeedProgressManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnleashUserDefaultsSeedProgressManagerTests.swift; sourceTree = ""; }; - 2C7DBABB2C4EA17200BCD03F /* AppDelegateFeatureManagementIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateFeatureManagementIntegrationTests.swift; sourceTree = ""; }; - 2C8262572C64424300E2A255 /* EcosiaSnapshotTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = EcosiaSnapshotTests.xctestplan; sourceTree = ""; }; - 2C8262592C64BB9300E2A255 /* DeviceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceType.swift; sourceTree = ""; }; - 2C82625B2C6648D900E2A255 /* LocalizationOverrideTestingBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationOverrideTestingBundle.swift; sourceTree = ""; }; - 2C82625D2C66661700E2A255 /* EcosiaMockThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EcosiaMockThemeManager.swift; sourceTree = ""; }; - 2C872A532B8CD47600B318A0 /* MockAppVersionInfoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppVersionInfoProvider.swift; sourceTree = ""; }; - 2C872A562B8CD65100B318A0 /* EcosiaHomeViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EcosiaHomeViewModelTests.swift; sourceTree = ""; }; + 2C75B1772D0B1A5200931F19 /* UnitTest.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTest.xctestplan; sourceTree = ""; }; 2C8C07761E7800EA00DC1237 /* FindInPageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindInPageTests.swift; sourceTree = ""; }; 2C9144B0B15218D8A0FCD538 /* es-AR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-AR"; path = "es-AR.lproj/ClearPrivateData.strings"; sourceTree = ""; }; - 2C9258D82CEF97B100C6BB8D /* MockUNNotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUNNotificationSettings.swift; sourceTree = ""; }; - 2C9258DA2CEFB26500C6BB8D /* AnalyticsNotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsNotificationSettings.swift; sourceTree = ""; }; 2C97EC701E72C80E0092EC18 /* TopTabsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTabsTest.swift; sourceTree = ""; }; - 2C9A62BF2CDE1F7600CDA7D1 /* MockWelcomeDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWelcomeDelegate.swift; sourceTree = ""; }; - 2C9A62C12CDE4A3B00CDA7D1 /* MockNewsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNewsModel.swift; sourceTree = ""; }; 2CA16FDD1E5F089100332277 /* SearchTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTest.swift; sourceTree = ""; }; - 2CA995272CA2C06A001064CC /* NTPConfigurableNudgeCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPConfigurableNudgeCardCell.swift; sourceTree = ""; }; - 2CA995292CA2C0BB001064CC /* NTPConfigurableNudgeCardCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTPConfigurableNudgeCardCellViewModel.swift; sourceTree = ""; }; - 2CABD7272C12EF1E00A0750F /* PrivateModeButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateModeButtonTests.swift; sourceTree = ""; }; 2CAE4511992E91A32AB7D7C7 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Today.strings; sourceTree = ""; }; + 2CAF5AE22D00AAEE00D3DCDD /* Bundle+Ecosia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Ecosia.swift"; sourceTree = ""; }; + 2CAF5AE42D00B1D300D3DCDD /* EcosiaLaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = EcosiaLaunchScreen.xib; sourceTree = ""; }; + 2CAF5AE52D00B1D300D3DCDD /* EcosiaLaunchScreenView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaLaunchScreenView.swift; sourceTree = ""; }; + 2CAF5AEA2D00D16700D3DCDD /* Bookmark.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bookmark.swift; sourceTree = ""; }; + 2CAF5AEB2D00D16700D3DCDD /* BookmarkParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkParser.swift; sourceTree = ""; }; + 2CAF5AEC2D00D16700D3DCDD /* BookmarkSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkSerializer.swift; sourceTree = ""; }; + 2CAF5AED2D00D16700D3DCDD /* Document+Safari.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Document+Safari.swift"; sourceTree = ""; }; + 2CAF5AEE2D00D16700D3DCDD /* String+CssQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+CssQuery.swift"; sourceTree = ""; }; + 2CAF5AF02D00D16700D3DCDD /* BaseRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseRequest.swift; sourceTree = ""; }; + 2CAF5AF12D00D16700D3DCDD /* Requestable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Requestable.swift; sourceTree = ""; }; + 2CAF5AF32D00D16700D3DCDD /* HTTPClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; + 2CAF5AF42D00D16700D3DCDD /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + 2CAF5AF52D00D16700D3DCDD /* URLSessionHTTPClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionHTTPClient.swift; sourceTree = ""; }; + 2CAF5AF62D00D16700D3DCDD /* URLSessionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionProtocol.swift; sourceTree = ""; }; + 2CAF5AF82D00D16700D3DCDD /* Cookie.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cookie.swift; sourceTree = ""; }; + 2CAF5AF92D00D16700D3DCDD /* ReferralClaimRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferralClaimRequest.swift; sourceTree = ""; }; + 2CAF5AFA2D00D16700D3DCDD /* ReferralCreateCodeRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferralCreateCodeRequest.swift; sourceTree = ""; }; + 2CAF5AFB2D00D16700D3DCDD /* ReferralRefreshCodeRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferralRefreshCodeRequest.swift; sourceTree = ""; }; + 2CAF5AFC2D00D16700D3DCDD /* Referrals.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Referrals.Model.swift; sourceTree = ""; }; + 2CAF5AFD2D00D16700D3DCDD /* Referrals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Referrals.swift; sourceTree = ""; }; + 2CAF5AFF2D00D16700D3DCDD /* Scheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scheme.swift; sourceTree = ""; }; + 2CAF5B002D00D16800D3DCDD /* Publisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = ""; }; + 2CAF5B012D00D16800D3DCDD /* TimestampProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimestampProvider.swift; sourceTree = ""; }; + 2CAF5B022D00D16800D3DCDD /* SingularConversionValueRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularConversionValueRequest.swift; sourceTree = ""; }; + 2CAF5B032D00D16800D3DCDD /* SingularConversionValueResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularConversionValueResponse.swift; sourceTree = ""; }; + 2CAF5B042D00D16800D3DCDD /* SingularEventRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularEventRequest.swift; sourceTree = ""; }; + 2CAF5B052D00D16800D3DCDD /* SingularReponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularReponse.swift; sourceTree = ""; }; + 2CAF5B062D00D16800D3DCDD /* SingularService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularService.swift; sourceTree = ""; }; + 2CAF5B072D00D16800D3DCDD /* SingularSessionInfoSendRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularSessionInfoSendRequest.swift; sourceTree = ""; }; + 2CAF5B092D00D16800D3DCDD /* Singular.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Singular.swift; sourceTree = ""; }; + 2CAF5B0A2D00D16800D3DCDD /* SingularAdNetworkHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularAdNetworkHelper.swift; sourceTree = ""; }; + 2CAF5B0B2D00D16800D3DCDD /* SingularEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularEvent.swift; sourceTree = ""; }; + 2CAF5B0E2D00D16800D3DCDD /* MMPProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MMPProvider.swift; sourceTree = ""; }; + 2CAF5B0F2D00D16800D3DCDD /* SKAdNetworkProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SKAdNetworkProtocol.swift; sourceTree = ""; }; + 2CAF5B112D00D16800D3DCDD /* AppDeviceInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDeviceInfo.swift; sourceTree = ""; }; + 2CAF5B122D00D16800D3DCDD /* Environment.Auth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Environment.Auth.swift; sourceTree = ""; }; + 2CAF5B132D00D16800D3DCDD /* Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; + 2CAF5B142D00D16800D3DCDD /* URLProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLProvider.swift; sourceTree = ""; }; + 2CAF5B172D00D16800D3DCDD /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = ""; }; + 2CAF5B182D00D16800D3DCDD /* History.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; + 2CAF5B192D00D16900D3DCDD /* AppUpdateRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppUpdateRule.swift; sourceTree = ""; }; + 2CAF5B1A2D00D16900D3DCDD /* DeviceRegionChangeRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceRegionChangeRule.swift; sourceTree = ""; }; + 2CAF5B1B2D00D16900D3DCDD /* RefreshingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshingRule.swift; sourceTree = ""; }; + 2CAF5B1C2D00D16900D3DCDD /* TimeBasedRefreshingRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeBasedRefreshingRule.swift; sourceTree = ""; }; + 2CAF5B1F2D00D16900D3DCDD /* Unleash.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Unleash.Model.swift; sourceTree = ""; }; + 2CAF5B202D00D16900D3DCDD /* Unleash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Unleash.swift; sourceTree = ""; }; + 2CAF5B212D00D16900D3DCDD /* Unleash+RefreshComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Unleash+RefreshComponent.swift"; sourceTree = ""; }; + 2CAF5B222D00D16900D3DCDD /* UnleashFeatureManagementSessionInitializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnleashFeatureManagementSessionInitializer.swift; sourceTree = ""; }; + 2CAF5B232D00D16900D3DCDD /* UnleashRefreshConfigurator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnleashRefreshConfigurator.swift; sourceTree = ""; }; + 2CAF5B242D00D16900D3DCDD /* UnleashStartRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnleashStartRequest.swift; sourceTree = ""; }; + 2CAF5B262D00D16900D3DCDD /* FeatureManagementSessionInitializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureManagementSessionInitializer.swift; sourceTree = ""; }; + 2CAF5B282D00D16900D3DCDD /* Tab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = ""; }; + 2CAF5B292D00D16900D3DCDD /* Tabs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tabs.swift; sourceTree = ""; }; + 2CAF5B2B2D00D16900D3DCDD /* News.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = News.swift; sourceTree = ""; }; + 2CAF5B2C2D00D16900D3DCDD /* NewsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsModel.swift; sourceTree = ""; }; + 2CAF5B2E2D00D16900D3DCDD /* ObjectPersister.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectPersister.swift; sourceTree = ""; }; + 2CAF5B2F2D00D16900D3DCDD /* Date+TimestampProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+TimestampProvider.swift"; sourceTree = ""; }; + 2CAF5B302D00D16900D3DCDD /* Page.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; }; + 2CAF5B312D00D16900D3DCDD /* PageStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageStore.swift; sourceTree = ""; }; + 2CAF5B332D00D16900D3DCDD /* URLRequest+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLRequest+Extensions.swift"; sourceTree = ""; }; + 2CAF5B342D00D16A00D3DCDD /* Encodable+Dictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Encodable+Dictionary.swift"; sourceTree = ""; }; + 2CAF5B352D00D16A00D3DCDD /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 2CAF5B362D00D16A00D3DCDD /* URL+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; + 2CAF5B372D00D16A00D3DCDD /* FinancialReports.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FinancialReports.swift; sourceTree = ""; }; + 2CAF5B382D00D16A00D3DCDD /* InvestmentsProjection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvestmentsProjection.swift; sourceTree = ""; }; + 2CAF5B392D00D16A00D3DCDD /* Statistics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Statistics.swift; sourceTree = ""; }; + 2CAF5B3A2D00D16A00D3DCDD /* TreesProjection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TreesProjection.swift; sourceTree = ""; }; + 2CAF5B3C2D00D16A00D3DCDD /* EnvironmentFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentFetcher.swift; sourceTree = ""; }; + 2CAF5B3E2D00D16A00D3DCDD /* Local.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Local.swift; sourceTree = ""; }; + 2CAF5B3F2D00D16B00D3DCDD /* Language.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Language.swift; sourceTree = ""; }; + 2CAF5B402D00D16B00D3DCDD /* Favourites.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Favourites.swift; sourceTree = ""; }; + 2CAF5B412D00D16B00D3DCDD /* List.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = List.swift; sourceTree = ""; }; + 2CAF5B422D00D16B00D3DCDD /* AdultFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdultFilter.swift; sourceTree = ""; }; + 2CAF5B432D00D16B00D3DCDD /* Images.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = ""; }; + 2CAF5B442D00D16B00D3DCDD /* UserDefaults+ObjectPersister.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+ObjectPersister.swift"; sourceTree = ""; }; + 2CAF5B452D00D16B00D3DCDD /* SearchesCounter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchesCounter.swift; sourceTree = ""; }; + 2CAF5B462D00D16C00D3DCDD /* FileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; + 2CAF5B472D00D16C00D3DCDD /* Market.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Market.swift; sourceTree = ""; }; + 2CAF5B482D00D16C00D3DCDD /* Bundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; + 2CAF5B492D00D16C00D3DCDD /* RegionLocatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegionLocatable.swift; sourceTree = ""; }; + 2CAF5B4A2D00D16C00D3DCDD /* CloudFlareKeyProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudFlareKeyProvider.swift; sourceTree = ""; }; + 2CAF5B4B2D00D16C00D3DCDD /* Locale+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Locale+Extensions.swift"; sourceTree = ""; }; + 2CAF5B9D2D00D1A600D3DCDD /* User5_3.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User5_3.swift; sourceTree = ""; }; + 2CAF5B9F2D00D1A600D3DCDD /* BookmarkFixtures.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkFixtures.swift; sourceTree = ""; }; + 2CAF5BA02D00D1A600D3DCDD /* HTTPClientMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPClientMock.swift; sourceTree = ""; }; + 2CAF5BA12D00D1A600D3DCDD /* MockTimestampProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockTimestampProvider.swift; sourceTree = ""; }; + 2CAF5BA22D00D1A600D3DCDD /* MockURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; + 2CAF5BA32D00D1A600D3DCDD /* MockURLSessionProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSessionProtocol.swift; sourceTree = ""; }; + 2CAF5BA52D00D1A600D3DCDD /* ListTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListTests.swift; sourceTree = ""; }; + 2CAF5BA62D00D1A600D3DCDD /* URLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLTests.swift; sourceTree = ""; }; + 2CAF5BA72D00D1A700D3DCDD /* FeatureFlaggingSessionInitializerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureFlaggingSessionInitializerTests.swift; sourceTree = ""; }; + 2CAF5BA82D00D1A700D3DCDD /* export_bookmark_ecosia.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = export_bookmark_ecosia.html; sourceTree = ""; }; + 2CAF5BA92D00D1A700D3DCDD /* import_input_bookmark_chrome.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = import_input_bookmark_chrome.html; sourceTree = ""; }; + 2CAF5BAA2D00D1A700D3DCDD /* import_input_bookmark_firefox.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = import_input_bookmark_firefox.html; sourceTree = ""; }; + 2CAF5BAB2D00D1A700D3DCDD /* import_input_bookmark_safari.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = import_input_bookmark_safari.html; sourceTree = ""; }; + 2CAF5BAC2D00D1A700D3DCDD /* import_output_bookmark_chrome.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = import_output_bookmark_chrome.txt; sourceTree = ""; }; + 2CAF5BAD2D00D1A700D3DCDD /* import_output_bookmark_firefox.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = import_output_bookmark_firefox.txt; sourceTree = ""; }; + 2CAF5BAE2D00D1A700D3DCDD /* import_output_bookmark_safari.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = import_output_bookmark_safari.txt; sourceTree = ""; }; + 2CAF5BB02D00D1A700D3DCDD /* notifications.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = notifications.json; sourceTree = ""; }; + 2CAF5BB12D00D1A700D3DCDD /* referrals.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = referrals.json; sourceTree = ""; }; + 2CAF5BB32D00D1A700D3DCDD /* SingularTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularTests.swift; sourceTree = ""; }; + 2CAF5BB42D00D1A700D3DCDD /* SingularServiceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularServiceTests.swift; sourceTree = ""; }; + 2CAF5BB52D00D1A700D3DCDD /* UpgradeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpgradeTests.swift; sourceTree = ""; }; + 2CAF5BB62D00D1A700D3DCDD /* UnleashFeatureManagementSessionInitializerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnleashFeatureManagementSessionInitializerTests.swift; sourceTree = ""; }; + 2CAF5BB72D00D1A700D3DCDD /* BookmarkParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkParserTests.swift; sourceTree = ""; }; + 2CAF5BB82D00D1A700D3DCDD /* NewsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsTests.swift; sourceTree = ""; }; + 2CAF5BB92D00D1A800D3DCDD /* BookmarkSerializerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkSerializerTests.swift; sourceTree = ""; }; + 2CAF5BBA2D00D1A800D3DCDD /* ImagesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagesTests.swift; sourceTree = ""; }; + 2CAF5BBB2D00D1A800D3DCDD /* StatisticsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatisticsTests.swift; sourceTree = ""; }; + 2CAF5BBC2D00D1A800D3DCDD /* ReferralsModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferralsModelTests.swift; sourceTree = ""; }; + 2CAF5BBD2D00D1A800D3DCDD /* HistoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryTests.swift; sourceTree = ""; }; + 2CAF5BBE2D00D1A800D3DCDD /* BookmarkTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkTests.swift; sourceTree = ""; }; + 2CAF5BBF2D00D1A800D3DCDD /* SnapshotsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotsTests.swift; sourceTree = ""; }; + 2CAF5BC02D00D1A800D3DCDD /* LocalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalTests.swift; sourceTree = ""; }; + 2CAF5BC12D00D1A900D3DCDD /* SingularAdNetworkHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingularAdNetworkHelperTests.swift; sourceTree = ""; }; + 2CAF5BC22D00D1A900D3DCDD /* TabsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabsTests.swift; sourceTree = ""; }; + 2CAF5BC32D00D1A900D3DCDD /* TreesProjectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TreesProjectionTests.swift; sourceTree = ""; }; + 2CAF5BC42D00D1A900D3DCDD /* UnleashTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnleashTests.swift; sourceTree = ""; }; + 2CAF5BC52D00D1A900D3DCDD /* ProductionURLProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductionURLProviderTests.swift; sourceTree = ""; }; + 2CAF5BC62D00D1A900D3DCDD /* StagingURLProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StagingURLProviderTests.swift; sourceTree = ""; }; + 2CAF5BC72D00D1A900D3DCDD /* URLProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLProviderTests.swift; sourceTree = ""; }; + 2CAF5BC92D00D1A900D3DCDD /* UserStateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserStateTests.swift; sourceTree = ""; }; + 2CAF5BCA2D00D1A900D3DCDD /* SearchesCounterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchesCounterTests.swift; sourceTree = ""; }; + 2CAF5BCB2D00D1AA00D3DCDD /* ReferralsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferralsTests.swift; sourceTree = ""; }; + 2CAF5BCC2D00D1AA00D3DCDD /* FinancialReportsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FinancialReportsTests.swift; sourceTree = ""; }; + 2CAF5BCD2D00D1AA00D3DCDD /* CookieTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CookieTests.swift; sourceTree = ""; }; + 2CAF5BCE2D00D1AA00D3DCDD /* InvestmentsProjectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvestmentsProjectionTests.swift; sourceTree = ""; }; + 2CAF5BCF2D00D1AA00D3DCDD /* PublishersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublishersTests.swift; sourceTree = ""; }; + 2CAF5BD02D00D1AA00D3DCDD /* UnleashRefreshConfiguratorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnleashRefreshConfiguratorTests.swift; sourceTree = ""; }; + 2CAF5BD12D00D1AA00D3DCDD /* UserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserTests.swift; sourceTree = ""; }; + 2CAF5BD22D00D1AA00D3DCDD /* LanguageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageTests.swift; sourceTree = ""; }; + 2CAF5BD32D00D1AB00D3DCDD /* FavouritesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavouritesTests.swift; sourceTree = ""; }; + 2CAF5BD42D00D1AB00D3DCDD /* URLRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestTests.swift; sourceTree = ""; }; 2CB1728B2C61336D008551E2 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.5.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; 2CB1A6591FDEA8B60084E96D /* NewTabSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabSettings.swift; sourceTree = ""; }; 2CB56E3E1E926BFB00AF7586 /* ToolbarTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarTest.swift; sourceTree = ""; }; @@ -2557,24 +2729,180 @@ 2CBCAB0C2B88EEE40080AD68 /* Ecosia.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Ecosia.xcconfig; sourceTree = ""; }; 2CBCAB0D2B88EEE40080AD68 /* EcosiaDebug.WidgetKit.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = EcosiaDebug.WidgetKit.xcconfig; sourceTree = ""; }; 2CBCAB0E2B88EEE40080AD68 /* EcosiaDebug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = EcosiaDebug.xcconfig; sourceTree = ""; }; + 2CBF7AF32D13056300454AB4 /* BundleImageFetcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleImageFetcherTests.swift; sourceTree = ""; }; 2CC1B3EF1E9B861400814EEC /* DomainAutocompleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainAutocompleteTests.swift; sourceTree = ""; }; 2CCB296620A99C9500121DD8 /* LoginsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginsTests.swift; sourceTree = ""; }; - 2CCBB5222CAE9826006E2E10 /* ArcProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArcProgressView.swift; sourceTree = ""; }; - 2CCBB5242CAEA9DF006E2E10 /* SeedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedProgressView.swift; sourceTree = ""; }; - 2CCBB5262CAEAD53006E2E10 /* SeedCounterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedCounterView.swift; sourceTree = ""; }; - 2CCBB5342CAF06DE006E2E10 /* SeedProgressManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedProgressManagerProtocol.swift; sourceTree = ""; }; - 2CCBB5362CAF0E8C006E2E10 /* SeedCounterHiddenSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedCounterHiddenSettings.swift; sourceTree = ""; }; 2CCF17522105E4FD00705AE5 /* DisplaySettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaySettingsTests.swift; sourceTree = ""; }; - 2CCFB3D42C0F1EA500BEDCA0 /* LoadingScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingScreen.swift; sourceTree = ""; }; - 2CD368492C5BC31700972871 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; - 2CD48B7E2C7F7E4100A70908 /* EcosiaOverlayModeManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EcosiaOverlayModeManagerTests.swift; sourceTree = ""; }; + 2CD261842CFDC5E900A040A7 /* EcosiaMockThemeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaMockThemeManager.swift; sourceTree = ""; }; + 2CD261852CFDC5E900A040A7 /* Welcome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; + 2CD261B92CFDC5E900A040A7 /* NTPComponentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPComponentTests.swift; sourceTree = ""; }; + 2CD261BA2CFDC5E900A040A7 /* NTPTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPTests.swift; sourceTree = ""; }; + 2CD262862CFDC5E900A040A7 /* OnboardingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; + 2CD262882CFDC5E900A040A7 /* DeviceType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceType.swift; sourceTree = ""; }; + 2CD262892CFDC5E900A040A7 /* SnapshotTests.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SnapshotTests.xctestplan; sourceTree = ""; }; + 2CD2628A2CFDC5E900A040A7 /* environment.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = environment.json; sourceTree = ""; }; + 2CD2628B2CFDC5E900A040A7 /* LocaleRetriever.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocaleRetriever.swift; sourceTree = ""; }; + 2CD2628C2CFDC5E900A040A7 /* LocalizationOverrideTestingBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationOverrideTestingBundle.swift; sourceTree = ""; }; + 2CD2628D2CFDC5E900A040A7 /* snapshot_configuration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = snapshot_configuration.json; sourceTree = ""; }; + 2CD2628E2CFDC5E900A040A7 /* SnapshotBaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotBaseTests.swift; sourceTree = ""; }; + 2CD2628F2CFDC5E900A040A7 /* SnapshotTestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotTestHelper.swift; sourceTree = ""; }; + 2CD262902CFDC5E900A040A7 /* String+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; + 2CD262922CFDC5E900A040A7 /* VersionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = ""; }; + 2CD262962CFDC5E900A040A7 /* WhatsNewLocalDataProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewLocalDataProviderTests.swift; sourceTree = ""; }; + 2CD262972CFDC5EA00A040A7 /* AppDelegateFeatureManagementIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegateFeatureManagementIntegrationTests.swift; sourceTree = ""; }; + 2CD262992CFDC5EA00A040A7 /* EcosiaInstallTypeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaInstallTypeTests.swift; sourceTree = ""; }; + 2CD2629A2CFDC5EA00A040A7 /* EcosiaPerformanceTestHistory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaPerformanceTestHistory.swift; sourceTree = ""; }; + 2CD2629B2CFDC5EA00A040A7 /* MockAppVersionInfoProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockAppVersionInfoProvider.swift; sourceTree = ""; }; + 2CD2629D2CFDC5EA00A040A7 /* PrivateModeButtonTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateModeButtonTests.swift; sourceTree = ""; }; + 2CD2629E2CFDC5EA00A040A7 /* EcosiaNTPTooltipHighlightTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaNTPTooltipHighlightTests.swift; sourceTree = ""; }; + 2CD2629F2CFDC5EA00A040A7 /* EcosiaHomeViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaHomeViewModelTests.swift; sourceTree = ""; }; + 2CD262A02CFDC5EA00A040A7 /* UnleashUserDefaultsSeedProgressManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnleashUserDefaultsSeedProgressManagerTests.swift; sourceTree = ""; }; + 2CD262A12CFDC5EA00A040A7 /* UserDefaultsSeedProgressManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsSeedProgressManagerTests.swift; sourceTree = ""; }; + 2CD262A32CFDC5EB00A040A7 /* EcosiaTopSitesHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaTopSitesHelperTests.swift; sourceTree = ""; }; + 2CD262A42CFDC5EB00A040A7 /* EcosiaOverlayModeManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaOverlayModeManagerTests.swift; sourceTree = ""; }; + 2CD262A52CFDC5EB00A040A7 /* EcosiaPageActionMenuCellTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaPageActionMenuCellTests.swift; sourceTree = ""; }; + 2CD262A62CFDC5EB00A040A7 /* AnalyticsSpyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsSpyTests.swift; sourceTree = ""; }; + 2CD262A72CFDC5EB00A040A7 /* AnalyticsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = ""; }; + 2CD263C52CFDC76800A040A7 /* Clean.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Clean.swift; sourceTree = ""; }; + 2CD263C62CFDC76800A040A7 /* Validate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Validate.swift; sourceTree = ""; }; + 2CD263C82CFDC76800A040A7 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Ecosia.strings; sourceTree = ""; }; + 2CD263CA2CFDC76800A040A7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Ecosia.strings; sourceTree = ""; }; + 2CD263CC2CFDC76800A040A7 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Ecosia.strings; sourceTree = ""; }; + 2CD263CE2CFDC76800A040A7 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Ecosia.strings; sourceTree = ""; }; + 2CD263D02CFDC76800A040A7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Ecosia.strings; sourceTree = ""; }; + 2CD263D22CFDC76800A040A7 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Ecosia.strings; sourceTree = ""; }; + 2CD263D42CFDC76800A040A7 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Plurals.stringsdict; sourceTree = ""; }; + 2CD263D62CFDC76800A040A7 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Plurals.stringsdict; sourceTree = ""; }; + 2CD263D82CFDC76800A040A7 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Plurals.stringsdict; sourceTree = ""; }; + 2CD263DA2CFDC76800A040A7 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Plurals.stringsdict; sourceTree = ""; }; + 2CD263DC2CFDC76800A040A7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Plurals.stringsdict; sourceTree = ""; }; + 2CD263DE2CFDC76800A040A7 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Plurals.stringsdict; sourceTree = ""; }; + 2CD263E02CFDC76800A040A7 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + 2CD263E22CFDC76800A040A7 /* AppVersionInfoProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppVersionInfoProvider.swift; sourceTree = ""; }; + 2CD263E32CFDC76800A040A7 /* DefaultAppVersionInfoProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultAppVersionInfoProvider.swift; sourceTree = ""; }; + 2CD263E52CFDC76800A040A7 /* EcosiaInstallType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaInstallType.swift; sourceTree = ""; }; + 2CD263E62CFDC76800A040A7 /* EcosiaInstallType+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EcosiaInstallType+Extensions.swift"; sourceTree = ""; }; + 2CD263E82CFDC76800A040A7 /* Version.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; + 2CD263E92CFDC76800A040A7 /* Version+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Version+Extensions.swift"; sourceTree = ""; }; + 2CD263EC2CFDC76800A040A7 /* Analytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; }; + 2CD263ED2CFDC76800A040A7 /* Analytics.Values.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Analytics.Values.swift; sourceTree = ""; }; + 2CD263EE2CFDC76800A040A7 /* Analytics+Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Analytics+Configuration.swift"; sourceTree = ""; }; + 2CD263F72CFDC76900A040A7 /* markets.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = markets.json; sourceTree = ""; }; + 2CD263FE2CFDC76900A040A7 /* FeatureManagement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeatureManagement.swift; sourceTree = ""; }; + 2CD264002CFDC76900A040A7 /* MMP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MMP.swift; sourceTree = ""; }; + 2CD264022CFDC76900A040A7 /* Ecosia.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Ecosia.entitlements; sourceTree = ""; }; + 2CD264032CFDC76900A040A7 /* EcosiaBeta.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = EcosiaBeta.entitlements; sourceTree = ""; }; + 2CD264052CFDC76900A040A7 /* Ecosia.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Ecosia.entitlements; sourceTree = ""; }; + 2CD264062CFDC76900A040A7 /* EcosiaBeta.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = EcosiaBeta.entitlements; sourceTree = ""; }; + 2CD264722CFDC76A00A040A7 /* FakeNimbus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeNimbus.swift; sourceTree = ""; }; + 2CD264732CFDC76A00A040A7 /* FakeSentry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeSentry.swift; sourceTree = ""; }; + 2CD264742CFDC76A00A040A7 /* FakeTelemetry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeTelemetry.swift; sourceTree = ""; }; + 2CD265402CFDCF0900A040A7 /* AppSettingsTableViewController+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppSettingsTableViewController+Ecosia.swift"; sourceTree = ""; }; + 2CD265412CFDCF0900A040A7 /* BrowserCoordinator+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BrowserCoordinator+Ecosia.swift"; sourceTree = ""; }; + 2CD265422CFDCF0900A040A7 /* BrowserViewController+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BrowserViewController+Ecosia.swift"; sourceTree = ""; }; + 2CD265442CFDCF0900A040A7 /* DispatchQueueHelper+BuildChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DispatchQueueHelper+BuildChannel.swift"; sourceTree = ""; }; + 2CD265452CFDCF0900A040A7 /* ErrorPageHandler+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ErrorPageHandler+Ecosia.swift"; sourceTree = ""; }; + 2CD265462CFDCF0900A040A7 /* HomepageViewController+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HomepageViewController+Ecosia.swift"; sourceTree = ""; }; + 2CD265472CFDCF0900A040A7 /* LegacyThemeManager+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "LegacyThemeManager+Ecosia.swift"; sourceTree = ""; }; + 2CD265482CFDCF0900A040A7 /* NumberFormatter+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+Ecosia.swift"; sourceTree = ""; }; + 2CD265492CFDCF0900A040A7 /* SimpleToast+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SimpleToast+Ecosia.swift"; sourceTree = ""; }; + 2CD2654A2CFDCF0900A040A7 /* SnapKit+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SnapKit+Ecosia.swift"; sourceTree = ""; }; + 2CD2654B2CFDCF0900A040A7 /* UIButton+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Ecosia.swift"; sourceTree = ""; }; + 2CD2654C2CFDCF0900A040A7 /* UIFont+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIFont+Ecosia.swift"; sourceTree = ""; }; + 2CD2654D2CFDCF0900A040A7 /* UIView+maskedCorners.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+maskedCorners.swift"; sourceTree = ""; }; + 2CD2654E2CFDCF0900A040A7 /* URL+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+Ecosia.swift"; sourceTree = ""; }; + 2CD265612CFDCF6D00A040A7 /* UIImage+Ecosia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Ecosia.swift"; sourceTree = ""; }; + 2CD265652CFE382C00A040A7 /* MultiplyImpact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplyImpact.swift; sourceTree = ""; }; + 2CD265662CFE382C00A040A7 /* MultiplyImpactStep.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplyImpactStep.swift; sourceTree = ""; }; + 2CD265682CFE382C00A040A7 /* AboutEcosiaSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutEcosiaSection.swift; sourceTree = ""; }; + 2CD265692CFE382C00A040A7 /* NTPAboutEcosiaCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPAboutEcosiaCell.swift; sourceTree = ""; }; + 2CD2656A2CFE382C00A040A7 /* NTPAboutEcosiaCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPAboutEcosiaCellViewModel.swift; sourceTree = ""; }; + 2CD2656F2CFE382C00A040A7 /* SeedProgressManagerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedProgressManagerProtocol.swift; sourceTree = ""; }; + 2CD265702CFE382C00A040A7 /* UserDefaultsSeedProgressManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsSeedProgressManager.swift; sourceTree = ""; }; + 2CD265722CFE382C00A040A7 /* ArcProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArcProgressView.swift; sourceTree = ""; }; + 2CD265732CFE382C00A040A7 /* NTPSeedCounterCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPSeedCounterCell.swift; sourceTree = ""; }; + 2CD265742CFE382C00A040A7 /* NTPSeedCounterViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPSeedCounterViewModel.swift; sourceTree = ""; }; + 2CD265752CFE382C00A040A7 /* SeedCounterConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedCounterConfig.swift; sourceTree = ""; }; + 2CD265762CFE382C00A040A7 /* SeedCounterHiddenSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedCounterHiddenSettings.swift; sourceTree = ""; }; + 2CD265772CFE382C00A040A7 /* SeedCounterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedCounterView.swift; sourceTree = ""; }; + 2CD265782CFE382C00A040A7 /* SeedProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedProgressView.swift; sourceTree = ""; }; + 2CD265792CFE382C00A040A7 /* Sparkle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sparkle.swift; sourceTree = ""; }; + 2CD2657B2CFE382C00A040A7 /* CustomizableNTPSettingConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizableNTPSettingConfig.swift; sourceTree = ""; }; + 2CD2657C2CFE382C00A040A7 /* NTPCustomizationCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPCustomizationCell.swift; sourceTree = ""; }; + 2CD2657D2CFE382C00A040A7 /* NTPCustomizationCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPCustomizationCellViewModel.swift; sourceTree = ""; }; + 2CD2657F2CFE382C00A040A7 /* ClimateImpactInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClimateImpactInfo.swift; sourceTree = ""; }; + 2CD265802CFE382C00A040A7 /* NTPImpactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPImpactCell.swift; sourceTree = ""; }; + 2CD265812CFE382C00A040A7 /* NTPImpactCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPImpactCellViewModel.swift; sourceTree = ""; }; + 2CD265822CFE382C00A040A7 /* NTPImpactDividerFooter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPImpactDividerFooter.swift; sourceTree = ""; }; + 2CD265832CFE382C00A040A7 /* NTPImpactRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPImpactRowView.swift; sourceTree = ""; }; + 2CD265842CFE382C00A040A7 /* ProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressView.swift; sourceTree = ""; }; + 2CD265862CFE382C00A040A7 /* NTPLibaryCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPLibaryCellViewModel.swift; sourceTree = ""; }; + 2CD265872CFE382C00A040A7 /* NTPLibraryCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPLibraryCell.swift; sourceTree = ""; }; + 2CD265882CFE382C00A040A7 /* NTPLibraryShortcutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPLibraryShortcutView.swift; sourceTree = ""; }; + 2CD2658A2CFE382C00A040A7 /* NTPLogoCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPLogoCell.swift; sourceTree = ""; }; + 2CD2658C2CFE382C00A040A7 /* NewsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsController.swift; sourceTree = ""; }; + 2CD2658D2CFE382C00A040A7 /* NTPNewsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPNewsCell.swift; sourceTree = ""; }; + 2CD2658E2CFE382C00A040A7 /* NTPNewsCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPNewsCellViewModel.swift; sourceTree = ""; }; + 2CD265902CFE382C00A040A7 /* NTPNewsletterCardCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPNewsletterCardCell.swift; sourceTree = ""; }; + 2CD265912CFE382C00A040A7 /* NTPNewsletterCardViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPNewsletterCardViewModel.swift; sourceTree = ""; }; + 2CD265932CFE382C00A040A7 /* NTPConfigurableNudgeCardCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPConfigurableNudgeCardCell.swift; sourceTree = ""; }; + 2CD265942CFE382C00A040A7 /* NTPConfigurableNudgeCardCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPConfigurableNudgeCardCellViewModel.swift; sourceTree = ""; }; + 2CD265962CFE382C00A040A7 /* CircleButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleButton.swift; sourceTree = ""; }; + 2CD265972CFE382C00A040A7 /* DefaultBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultBrowser.swift; sourceTree = ""; }; + 2CD265982CFE382C00A040A7 /* NTPLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPLayout.swift; sourceTree = ""; }; + 2CD265992CFE382C00A040A7 /* NTPTooltip.Highlight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPTooltip.Highlight.swift; sourceTree = ""; }; + 2CD2659A2CFE382C00A040A7 /* NTPTooltip.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPTooltip.swift; sourceTree = ""; }; + 2CD2659B2CFE382C00A040A7 /* NTPTooltipDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPTooltipDelegate.swift; sourceTree = ""; }; + 2CD2659D2CFE382C00A040A7 /* Welcome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Welcome.swift; sourceTree = ""; }; + 2CD2659E2CFE382C00A040A7 /* WelcomeNavigation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeNavigation.swift; sourceTree = ""; }; + 2CD2659F2CFE382C00A040A7 /* WelcomeTour.Step.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTour.Step.swift; sourceTree = ""; }; + 2CD265A02CFE382C00A040A7 /* WelcomeTour.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTour.swift; sourceTree = ""; }; + 2CD265A12CFE382C00A040A7 /* WelcomeTourAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTourAction.swift; sourceTree = ""; }; + 2CD265A22CFE382C00A040A7 /* WelcomeTourGreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTourGreen.swift; sourceTree = ""; }; + 2CD265A32CFE382C00A040A7 /* WelcomeTourProfit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTourProfit.swift; sourceTree = ""; }; + 2CD265A42CFE382C00A040A7 /* WelcomeTourRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTourRow.swift; sourceTree = ""; }; + 2CD265A52CFE382C00A040A7 /* WelcomeTourTransparent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeTourTransparent.swift; sourceTree = ""; }; + 2CD265A72CFE382C00A040A7 /* PageActionMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageActionMenu.swift; sourceTree = ""; }; + 2CD265A82CFE382C00A040A7 /* PageActionMenuCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageActionMenuCell.swift; sourceTree = ""; }; + 2CD265A92CFE382C00A040A7 /* PageActionsShortcutsHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageActionsShortcutsHeader.swift; sourceTree = ""; }; + 2CD265AB2CFE382C00A040A7 /* EcosiaTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaTheme.swift; sourceTree = ""; }; + 2CD265AC2CFE382C00A040A7 /* EcosiaThemeColourPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaThemeColourPalette.swift; sourceTree = ""; }; + 2CD265AD2CFE382C00A040A7 /* EcosiaThemeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaThemeManager.swift; sourceTree = ""; }; + 2CD265AF2CFE382C00A040A7 /* WhatsNewDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewDataProvider.swift; sourceTree = ""; }; + 2CD265B02CFE382C00A040A7 /* WhatsNewLocalDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewLocalDataProvider.swift; sourceTree = ""; }; + 2CD265B22CFE382C00A040A7 /* WhatsNewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewCell.swift; sourceTree = ""; }; + 2CD265B32CFE382C00A040A7 /* WhatsNewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewItem.swift; sourceTree = ""; }; + 2CD265B42CFE382C00A040A7 /* WhatsNewViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewViewController.swift; sourceTree = ""; }; + 2CD265B52CFE382C00A040A7 /* WhatsNewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewViewModel.swift; sourceTree = ""; }; + 2CD265B72CFE382C00A040A7 /* Colours.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colours.xcassets; sourceTree = ""; }; + 2CD265B82CFE382C00A040A7 /* Ecosia.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Ecosia.xcassets; sourceTree = ""; }; + 2CD265B92CFE382C00A040A7 /* EcosiaFindInPageBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaFindInPageBar.swift; sourceTree = ""; }; + 2CD265BA2CFE382C00A040A7 /* EcosiaNavigation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaNavigation.swift; sourceTree = ""; }; + 2CD265BB2CFE382C00A040A7 /* EmptyBookmarksView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyBookmarksView.swift; sourceTree = ""; }; + 2CD265BC2CFE382C00A040A7 /* EmptyBookmarksViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyBookmarksViewDelegate.swift; sourceTree = ""; }; + 2CD265BD2CFE382C00A040A7 /* EmptyHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyHeader.swift; sourceTree = ""; }; + 2CD265BE2CFE382C00A040A7 /* EmptyReadingListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyReadingListView.swift; sourceTree = ""; }; + 2CD265BF2CFE382C00A040A7 /* FilterController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterController.swift; sourceTree = ""; }; + 2CD265C02CFE382C00A040A7 /* LoadingScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingScreen.swift; sourceTree = ""; }; + 2CD265C12CFE382C00A040A7 /* MarketsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketsController.swift; sourceTree = ""; }; + 2CD265C22CFE382C00A040A7 /* SemanticColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SemanticColor.swift; sourceTree = ""; }; + 2CD266102CFE38AD00A040A7 /* EcosiaTopSiteItemCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaTopSiteItemCell.swift; sourceTree = ""; }; + 2CD266132CFE38AD00A040A7 /* EcosiaHomepageSectionType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaHomepageSectionType.swift; sourceTree = ""; }; + 2CD266162CFE38AD00A040A7 /* EcosiaDebugSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaDebugSettings.swift; sourceTree = ""; }; + 2CD266172CFE38AD00A040A7 /* EcosiaSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaSettings.swift; sourceTree = ""; }; + 2CD266182CFE38AD00A040A7 /* NTPCustomizationSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NTPCustomizationSettingsViewController.swift; sourceTree = ""; }; + 2CD2661F2CFE39E300A040A7 /* EcosiaPrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EcosiaPrimaryButton.swift; sourceTree = ""; }; + 2CD266212CFE3AD900A040A7 /* BookmarksExchange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksExchange.swift; sourceTree = ""; }; + 2CD2662A2CFE402000A040A7 /* SeedCounterNTPExperiment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedCounterNTPExperiment.swift; sourceTree = ""; }; + 2CD2662D2CFE403800A040A7 /* BrazeIntegrationExperiment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrazeIntegrationExperiment.swift; sourceTree = ""; }; + 2CD2662E2CFE403800A040A7 /* NewsletterCardExperiment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsletterCardExperiment.swift; sourceTree = ""; }; + 2CD2663B2CFE423D00A040A7 /* AppInfo+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AppInfo+Ecosia.swift"; sourceTree = ""; }; + 2CD2663D2CFF4ED000A040A7 /* DeviceInfo+Ecosia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DeviceInfo+Ecosia.swift"; sourceTree = ""; }; + 2CD2664E2CFF56CB00A040A7 /* ConnectionStatusImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionStatusImage.swift; sourceTree = ""; }; + 2CD2664F2CFF56CB00A040A7 /* ConnectionStatusImage+WebsiteConnectionTypeStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConnectionStatusImage+WebsiteConnectionTypeStatus.swift"; sourceTree = ""; }; + 2CD266532CFF56EA00A040A7 /* WebsiteConnectionStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebsiteConnectionStatus.swift; sourceTree = ""; }; + 2CD7E6062D09E47E0003B02B /* Bundle+EcosiaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+EcosiaTests.swift"; sourceTree = ""; }; 2CE294442B7CDD05006C22B2 /* CoreAudioTypes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudioTypes.framework; path = System/Library/Frameworks/CoreAudioTypes.framework; sourceTree = SDKROOT; }; - 2CE294672B7FC5A4006C22B2 /* EcosiaInstallTypeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaInstallTypeTests.swift; sourceTree = ""; }; - 2CE294692B7FC5A5006C22B2 /* AnalyticsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = ""; }; - 2CE2946B2B7FC5A5006C22B2 /* WhatsNewLocalDataProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatsNewLocalDataProviderTests.swift; sourceTree = ""; }; - 2CE2946C2B7FC5A5006C22B2 /* EcosiaNTPTooltipHighlightTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaNTPTooltipHighlightTests.swift; sourceTree = ""; }; - 2CE2946D2B7FC5A5006C22B2 /* EcosiaPageActionMenuCellTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EcosiaPageActionMenuCellTests.swift; sourceTree = ""; }; - 2CE294702B7FC5A6006C22B2 /* VersionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = ""; }; 2CEA6F781E93E3A600D4100E /* SearchSettingsUITest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchSettingsUITest.swift; sourceTree = ""; }; 2CEB48402BE0EE2600498471 /* Production.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; }; 2CEDADA120207EC400223A89 /* SyncFAUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncFAUITests.swift; sourceTree = ""; }; @@ -7746,7 +8074,6 @@ 43A878FE27AC39D30071C372 /* RustMozillaAppServices.framework in Frameworks */, 5A87148A292EA1520039A5BD /* Fuzi in Frameworks */, 433F87D02788ECDD00693368 /* GCDWebServers in Frameworks */, - 2CE294492B7CDD78006C22B2 /* Core in Frameworks */, 5A70EF10295DFD4900790249 /* Common in Frameworks */, 5A68F0AB2AF2E5E00089AC62 /* TabDataStore in Frameworks */, C8CC4F8725253E79003FDE1F /* WidgetKit.framework in Frameworks */, @@ -7756,6 +8083,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 122935672CE78D0A00EC1297 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2CAF5C1F2D01B44400D3DCDD /* Sentry in Frameworks */, + 122935B52CE79EF900EC1297 /* SnowplowTracker in Frameworks */, + 2CAF5C1D2D01B40B00D3DCDD /* SwiftSoup in Frameworks */, + 122935932CE78ED500EC1297 /* BrazeUI in Frameworks */, + 122935912CE78ED500EC1297 /* BrazeKit in Frameworks */, + 2CAF5C212D01B5F300D3DCDD /* Common in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 122935702CE78D0A00EC1297 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2C21012B2D0B0F3700CBE7EC /* SnowplowTracker in Frameworks */, + 2CD7E6082D09E5DF0003B02B /* RustMozillaAppServices.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 2827315A1ABC9BE600AA1954 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -7765,7 +8114,6 @@ C820439A2523DC4500740B71 /* libStorage.a in Frameworks */, 5A70EF12295DFD6400790249 /* Common in Frameworks */, D09A0CDC1FAA24CC009A0273 /* libAccount.a in Frameworks */, - 2C1298AC2BF5EE3E005AE4E4 /* Core in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7785,7 +8133,6 @@ 43A878FD27AB4A110071C372 /* RustMozillaAppServices.framework in Frameworks */, 8A88815C2B2103AD009635AE /* WebEngine in Frameworks */, 8A88815A2B20FFE0009635AE /* GCDWebServers in Frameworks */, - 2CE2E24D2B9B1FCB00973C16 /* Core in Frameworks */, 5A9FF8492942454600DF9FBB /* Common in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -7800,6 +8147,7 @@ 2C6189F12B7B7D5D006B70D7 /* SnowplowTracker in Frameworks */, 43BE580E278BABCF00491291 /* RustMozillaAppServices.framework in Frameworks */, C82043852523DBF600740B71 /* Sync.framework in Frameworks */, + 1229357F2CE78D0A00EC1297 /* Ecosia.framework in Frameworks */, 5A37861929A2C337006B3A34 /* Sentry in Frameworks */, 8AF2D0FC2A5F272A00C7DD69 /* ComponentLibrary in Frameworks */, 5A871490292EA3910039A5BD /* SiteImageView in Frameworks */, @@ -7819,7 +8167,6 @@ 435C85F02788F4D00072B526 /* Glean in Frameworks */, 433F87CE2788EAB600693368 /* GCDWebServers in Frameworks */, 5A06135A29D6052E008F3D38 /* TabDataStore in Frameworks */, - 2CE294472B7CDD56006C22B2 /* Core in Frameworks */, 216A0D762A40E7AB008077BA /* Redux in Frameworks */, 7B8A47F61D01D3B400C07734 /* PassKit.framework in Frameworks */, 5A8FD0EC293A7D5E00333AA7 /* SnapKit in Frameworks */, @@ -7832,7 +8179,6 @@ files = ( 2C69DA822C62459C00D7F69F /* libz.tbd in Frameworks */, 2C69DA7F2C62458300D7F69F /* RustMozillaAppServices.framework in Frameworks */, - 2C69DA812C62459200D7F69F /* Core in Frameworks */, 2C6C908F2C614A6C007D9B43 /* SnapshotTesting in Frameworks */, 2C69DA7B2C6225C400D7F69F /* Common in Frameworks */, ); @@ -7860,6 +8206,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2CAF5C222D01B62900D3DCDD /* Ecosia.framework in Frameworks */, 43BE5809278BA9D700491291 /* RustMozillaAppServices.framework in Frameworks */, 8A88815E2B21071E009635AE /* GCDWebServers in Frameworks */, 5A37861429A2BFB9006B3A34 /* Common in Frameworks */, @@ -7912,7 +8259,6 @@ buildActionMask = 2147483647; files = ( 43F93C2827A8683E009833D9 /* RustMozillaAppServices.framework in Frameworks */, - 2C1F23BD2B9F405E00186F55 /* Core in Frameworks */, 8A8BAE142B21110000D774EB /* GCDWebServers in Frameworks */, E6231C081B90A71E005ABB0D /* libz.tbd in Frameworks */, ); @@ -7928,7 +8274,6 @@ C82043AF2523DC9600740B71 /* Sync.framework in Frameworks */, 2C1298A82BF5EE23005AE4E4 /* libStorage.a in Frameworks */, 5A70EF14295DFD7C00790249 /* Common in Frameworks */, - 2CF4DA632BB31970001C340A /* Core in Frameworks */, C877039625222FDC006E38EB /* Shared.framework in Frameworks */, 0B75AEA91AC20FB20015E5DC /* ImageIO.framework in Frameworks */, ); @@ -7964,31 +8309,52 @@ path = SearchQuickLinksMedium; sourceTree = ""; }; - 12147F2D2CDA3CB40009D300 /* Newsletter */ = { - isa = PBXGroup; - children = ( - 12147F2E2CDA3CD00009D300 /* NTPNewsletterCardCell.swift */, - 12147F302CDA3CD80009D300 /* NTPNewsletterCardViewModel.swift */, - ); - path = Newsletter; - sourceTree = ""; - }; - 126509802CD9249C0011BA36 /* Braze */ = { + 1229356B2CE78D0A00EC1297 /* Ecosia */ = { isa = PBXGroup; children = ( - 1285E2B62CC68BF00053506B /* APNConsent.swift */, - 126509812CD924C00011BA36 /* BrazeService.swift */, + 2C2101202D0B04E800CBE7EC /* Braze */, + 2CAF5AE92D00D14300D3DCDD /* Core */, + 2CD2663A2CFE421C00A040A7 /* Extensions */, + 2CD266312CFE403800A040A7 /* Experiments */, + 2CD263EF2CFDC76800A040A7 /* Analytics */, + 2CD264082CFDC76900A040A7 /* Auth */, + 2CD264072CFDC76900A040A7 /* Entitlements */, + 2CD264752CFDC76A00A040A7 /* Fake */, + 2CD263FF2CFDC76900A040A7 /* FeatureManagement */, + 2CD263EB2CFDC76800A040A7 /* Helpers */, + 2CD263E12CFDC76800A040A7 /* L10N */, + 2CD263F72CFDC76900A040A7 /* markets.json */, + 1229356C2CE78D0A00EC1297 /* Ecosia.h */, + 1229359D2CE792B700EC1297 /* README.md */, + 12E604442CECADDA009A7BEC /* Info.plist */, ); - path = Braze; + path = Ecosia; sourceTree = ""; }; - 1285E2B32CC293A20053506B /* Analytics */ = { - isa = PBXGroup; - children = ( - 2CE294692B7FC5A5006C22B2 /* AnalyticsTests.swift */, - 1285E2B42CC293CA0053506B /* AnalyticsSpyTests.swift */, + 122935792CE78D0A00EC1297 /* EcosiaTests */ = { + isa = PBXGroup; + children = ( + 2C75B1772D0B1A5200931F19 /* UnitTest.xctestplan */, + 2CAF5B9C2D00D18300D3DCDD /* Core */, + 2CD262A82CFDC5EB00A040A7 /* Analytics */, + 2CD262A22CFDC5EA00A040A7 /* ClimateImpactCounter */, + 2CD262982CFDC5EA00A040A7 /* IntegrationTests */, + 2CD2629C2CFDC5EA00A040A7 /* Mocks */, + 2CD262912CFDC5E900A040A7 /* SnapshotTests */, + 2CD2629F2CFDC5EA00A040A7 /* EcosiaHomeViewModelTests.swift */, + 2CD262992CFDC5EA00A040A7 /* EcosiaInstallTypeTests.swift */, + 2CD2629E2CFDC5EA00A040A7 /* EcosiaNTPTooltipHighlightTests.swift */, + 2CD262A42CFDC5EB00A040A7 /* EcosiaOverlayModeManagerTests.swift */, + 2CD262A52CFDC5EB00A040A7 /* EcosiaPageActionMenuCellTests.swift */, + 2CD2629A2CFDC5EA00A040A7 /* EcosiaPerformanceTestHistory.swift */, + 2CD262A32CFDC5EB00A040A7 /* EcosiaTopSitesHelperTests.swift */, + 2CD2629D2CFDC5EA00A040A7 /* PrivateModeButtonTests.swift */, + 2CD262922CFDC5E900A040A7 /* VersionTests.swift */, + 2CD262962CFDC5E900A040A7 /* WhatsNewLocalDataProviderTests.swift */, + 126509882CDA31890011BA36 /* BrazeServiceTests.swift */, + 2CBF7AF32D13056300454AB4 /* BundleImageFetcherTests.swift */, ); - path = Analytics; + path = EcosiaTests; sourceTree = ""; }; 1D7B78952ADF324E0011E9F2 /* Event Queue */ = { @@ -8397,714 +8763,1073 @@ path = Extensions; sourceTree = ""; }; - 2C08319A2B89124000BD7134 /* AppExtensions */ = { + 2C2101202D0B04E800CBE7EC /* Braze */ = { isa = PBXGroup; children = ( - 2C08319B2B89127200BD7134 /* Ecosia.entitlements */, - 2C08319C2B89127200BD7134 /* EcosiaBeta.entitlements */, + 2C21011E2D0B04E800CBE7EC /* APNConsent.swift */, + 2C21011F2D0B04E800CBE7EC /* BrazeService.swift */, ); - path = AppExtensions; + path = Braze; sourceTree = ""; }; - 2C16B7642CAF2417006118F8 /* ClimateImpactCounter */ = { + 2CABD71A2C11E07300A0750F /* PersistedGenerated */ = { isa = PBXGroup; children = ( - 2C16B7652CAF2425006118F8 /* UserDefaultsSeedProgressManagerTests.swift */, - 2C728D7D2CBBDCDC00C7684B /* UnleashUserDefaultsSeedProgressManagerTests.swift */, + 2C0360D92C1747E6006706F2 /* FxNimbus.swift */, ); - path = ClimateImpactCounter; + path = PersistedGenerated; sourceTree = ""; }; - 2C5A5E682CB53E05005BFE8B /* SeedProgressManager */ = { + 2CAF5AE62D00B1D300D3DCDD /* LaunchScreen */ = { isa = PBXGroup; children = ( - 2CCBB5342CAF06DE006E2E10 /* SeedProgressManagerProtocol.swift */, - 2C5A5E662CB53DF9005BFE8B /* UserDefaultsSeedProgressManager.swift */, + 2CAF5AE42D00B1D300D3DCDD /* EcosiaLaunchScreen.xib */, + 2CAF5AE52D00B1D300D3DCDD /* EcosiaLaunchScreenView.swift */, ); - path = SeedProgressManager; + path = LaunchScreen; sourceTree = ""; }; - 2C61887E2B7A8A21006B70D7 /* Ecosia */ = { - isa = PBXGroup; - children = ( - 2C61887F2B7A8A21006B70D7 /* LaunchScreen */, - 2C6188822B7A8A21006B70D7 /* MMP */, - 2C6188842B7A8A21006B70D7 /* UI */, - 2C6188D62B7A8A22006B70D7 /* Settings */, - 2C6188DA2B7A8A22006B70D7 /* L10N */, - 2C6188ED2B7A8A22006B70D7 /* FeatureManagement */, - 2C6188EF2B7A8A22006B70D7 /* Frontend */, - 2C6188F52B7A8A22006B70D7 /* Experiments */, - 2C6188FA2B7A8A22006B70D7 /* Network */, - 2C6188FC2B7A8A22006B70D7 /* BrowserKitExtensions */, - 2C6188FF2B7A8A22006B70D7 /* Extensions */, - 2C6189132B7A8A22006B70D7 /* Entitlements */, - 2C6189162B7A8A22006B70D7 /* Bookmarks */, - 2C6189182B7A8A22006B70D7 /* markets.json */, - 2C6189192B7A8A22006B70D7 /* Fake */, - 2C6189232B7A8A22006B70D7 /* Helpers */, - 2C61892D2B7A8A22006B70D7 /* Analytics */, - 126509802CD9249C0011BA36 /* Braze */, + 2CAF5AE92D00D14300D3DCDD /* Core */ = { + isa = PBXGroup; + children = ( + 2CAF5B422D00D16B00D3DCDD /* AdultFilter.swift */, + 2CAF5B112D00D16800D3DCDD /* AppDeviceInfo.swift */, + 2CAF5B3D2D00D16A00D3DCDD /* Auth */, + 2CAF5AEF2D00D16700D3DCDD /* Bookmarks */, + 2CAF5B482D00D16C00D3DCDD /* Bundle.swift */, + 2CAF5B4A2D00D16C00D3DCDD /* CloudFlareKeyProvider.swift */, + 2CAF5AF82D00D16700D3DCDD /* Cookie.swift */, + 2CAF5B2F2D00D16900D3DCDD /* Date+TimestampProvider.swift */, + 2CAF5B342D00D16A00D3DCDD /* Encodable+Dictionary.swift */, + 2CAF5B152D00D16800D3DCDD /* Environment */, + 2CAF5B3C2D00D16A00D3DCDD /* EnvironmentFetcher.swift */, + 2CAF5B402D00D16B00D3DCDD /* Favourites.swift */, + 2CAF5B272D00D16900D3DCDD /* FeatureManagement */, + 2CAF5B462D00D16C00D3DCDD /* FileManager.swift */, + 2CAF5B182D00D16800D3DCDD /* History.swift */, + 2CAF5AF72D00D16700D3DCDD /* HTTPClient */, + 2CAF5B432D00D16B00D3DCDD /* Images.swift */, + 2CAF5B3F2D00D16B00D3DCDD /* Language.swift */, + 2CAF5B412D00D16B00D3DCDD /* List.swift */, + 2CAF5B3E2D00D16A00D3DCDD /* Local.swift */, + 2CAF5B4B2D00D16C00D3DCDD /* Locale+Extensions.swift */, + 2CAF5B472D00D16C00D3DCDD /* Market.swift */, + 2CAF5B102D00D16800D3DCDD /* MMP */, + 2CAF5B2D2D00D16900D3DCDD /* News */, + 2CAF5B2E2D00D16900D3DCDD /* ObjectPersister.swift */, + 2CAF5B162D00D16800D3DCDD /* OptInReminder */, + 2CAF5B322D00D16900D3DCDD /* Pages */, + 2CAF5B002D00D16800D3DCDD /* Publisher.swift */, + 2CAF5AFE2D00D16700D3DCDD /* Referrals */, + 2CAF5B492D00D16C00D3DCDD /* RegionLocatable.swift */, + 2CAF5AFF2D00D16700D3DCDD /* Scheme.swift */, + 2CAF5B452D00D16B00D3DCDD /* SearchesCounter.swift */, + 2CAF5B3B2D00D16A00D3DCDD /* Statistics */, + 2CAF5B2A2D00D16900D3DCDD /* Tabs */, + 2CAF5B172D00D16800D3DCDD /* TimeInterval+Extensions.swift */, + 2CAF5B012D00D16800D3DCDD /* TimestampProvider.swift */, + 2CAF5B362D00D16A00D3DCDD /* URL+Extensions.swift */, + 2CAF5B332D00D16900D3DCDD /* URLRequest+Extensions.swift */, + 2CAF5B352D00D16A00D3DCDD /* User.swift */, + 2CAF5B442D00D16B00D3DCDD /* UserDefaults+ObjectPersister.swift */, + ); + path = Core; + sourceTree = ""; + }; + 2CAF5AEF2D00D16700D3DCDD /* Bookmarks */ = { + isa = PBXGroup; + children = ( + 2CAF5AEA2D00D16700D3DCDD /* Bookmark.swift */, + 2CAF5AEB2D00D16700D3DCDD /* BookmarkParser.swift */, + 2CAF5AEC2D00D16700D3DCDD /* BookmarkSerializer.swift */, + 2CAF5AED2D00D16700D3DCDD /* Document+Safari.swift */, + 2CAF5AEE2D00D16700D3DCDD /* String+CssQuery.swift */, ); - path = Ecosia; + path = Bookmarks; sourceTree = ""; }; - 2C61887F2B7A8A21006B70D7 /* LaunchScreen */ = { + 2CAF5AF22D00D16700D3DCDD /* Requestable */ = { isa = PBXGroup; children = ( - 2C6188802B7A8A21006B70D7 /* EcosiaLaunchScreenView.swift */, - 2C6188812B7A8A21006B70D7 /* EcosiaLaunchScreen.xib */, + 2CAF5AF02D00D16700D3DCDD /* BaseRequest.swift */, + 2CAF5AF12D00D16700D3DCDD /* Requestable.swift */, ); - path = LaunchScreen; + path = Requestable; sourceTree = ""; }; - 2C6188822B7A8A21006B70D7 /* MMP */ = { + 2CAF5AF72D00D16700D3DCDD /* HTTPClient */ = { isa = PBXGroup; children = ( - 2C6188832B7A8A21006B70D7 /* MMP.swift */, + 2CAF5AF22D00D16700D3DCDD /* Requestable */, + 2CAF5AF32D00D16700D3DCDD /* HTTPClient.swift */, + 2CAF5AF42D00D16700D3DCDD /* HTTPMethod.swift */, + 2CAF5AF52D00D16700D3DCDD /* URLSessionHTTPClient.swift */, + 2CAF5AF62D00D16700D3DCDD /* URLSessionProtocol.swift */, ); - path = MMP; + path = HTTPClient; sourceTree = ""; }; - 2C6188842B7A8A21006B70D7 /* UI */ = { - isa = PBXGroup; - children = ( - 2CCFB3D42C0F1EA500BEDCA0 /* LoadingScreen.swift */, - 2C6188852B7A8A21006B70D7 /* SemanticColor.swift */, - 2C6188862B7A8A21006B70D7 /* PageAction */, - 2C61888A2B7A8A21006B70D7 /* EmptyBookmarksViewDelegate.swift */, - 2C61888B2B7A8A21006B70D7 /* EcosiaFindInPageBar.swift */, - 2C61888C2B7A8A21006B70D7 /* Ecosia.xcassets */, - 2C61888D2B7A8A21006B70D7 /* EmptyReadingListView.swift */, - 2C6188942B7A8A21006B70D7 /* EmptyHeader.swift */, - 2C6188952B7A8A21006B70D7 /* WhatsNew */, - 2C61889D2B7A8A21006B70D7 /* NTP */, - 2C6188C02B7A8A22006B70D7 /* Colours.xcassets */, - 2C6188C12B7A8A22006B70D7 /* EcosiaNavigation.swift */, - 2C6188C22B7A8A22006B70D7 /* Theme */, - 2C6188C62B7A8A22006B70D7 /* FilterController.swift */, - 2C6188C72B7A8A22006B70D7 /* MarketsController.swift */, - 2C6188C82B7A8A22006B70D7 /* EmptyBookmarksView.swift */, - 2C6188C92B7A8A22006B70D7 /* Onboarding */, - 2C6188D32B7A8A22006B70D7 /* MultiplyImpact */, + 2CAF5AFE2D00D16700D3DCDD /* Referrals */ = { + isa = PBXGroup; + children = ( + 2CAF5AF92D00D16700D3DCDD /* ReferralClaimRequest.swift */, + 2CAF5AFA2D00D16700D3DCDD /* ReferralCreateCodeRequest.swift */, + 2CAF5AFB2D00D16700D3DCDD /* ReferralRefreshCodeRequest.swift */, + 2CAF5AFC2D00D16700D3DCDD /* Referrals.Model.swift */, + 2CAF5AFD2D00D16700D3DCDD /* Referrals.swift */, ); - path = UI; + path = Referrals; sourceTree = ""; }; - 2C6188862B7A8A21006B70D7 /* PageAction */ = { + 2CAF5B082D00D16800D3DCDD /* Service */ = { isa = PBXGroup; children = ( - 2C6188872B7A8A21006B70D7 /* PageActionMenuCell.swift */, - 2C6188882B7A8A21006B70D7 /* PageActionsShortcutsHeader.swift */, - 2C6188892B7A8A21006B70D7 /* PageActionMenu.swift */, + 2CAF5B022D00D16800D3DCDD /* SingularConversionValueRequest.swift */, + 2CAF5B032D00D16800D3DCDD /* SingularConversionValueResponse.swift */, + 2CAF5B042D00D16800D3DCDD /* SingularEventRequest.swift */, + 2CAF5B052D00D16800D3DCDD /* SingularReponse.swift */, + 2CAF5B062D00D16800D3DCDD /* SingularService.swift */, + 2CAF5B072D00D16800D3DCDD /* SingularSessionInfoSendRequest.swift */, ); - path = PageAction; + path = Service; sourceTree = ""; }; - 2C6188952B7A8A21006B70D7 /* WhatsNew */ = { + 2CAF5B0C2D00D16800D3DCDD /* Singular */ = { isa = PBXGroup; children = ( - 2C6188962B7A8A21006B70D7 /* WhatsNewCell.swift */, - 2C6188972B7A8A21006B70D7 /* WhatsNewViewModel.swift */, - 2C6188982B7A8A21006B70D7 /* WhatsNewViewController.swift */, - 2C6188992B7A8A21006B70D7 /* WhatsNewItem.swift */, - 2C61889A2B7A8A21006B70D7 /* DataProvider */, + 2CAF5B082D00D16800D3DCDD /* Service */, + 2CAF5B092D00D16800D3DCDD /* Singular.swift */, + 2CAF5B0A2D00D16800D3DCDD /* SingularAdNetworkHelper.swift */, + 2CAF5B0B2D00D16800D3DCDD /* SingularEvent.swift */, ); - path = WhatsNew; + path = Singular; sourceTree = ""; }; - 2C61889A2B7A8A21006B70D7 /* DataProvider */ = { + 2CAF5B102D00D16800D3DCDD /* MMP */ = { isa = PBXGroup; children = ( - 2C61889B2B7A8A21006B70D7 /* WhatsNewDataProvider.swift */, - 2C61889C2B7A8A21006B70D7 /* WhatsNewLocalDataProvider.swift */, + 2CD264002CFDC76900A040A7 /* MMP.swift */, + 2CAF5B0C2D00D16800D3DCDD /* Singular */, + 2CAF5B0E2D00D16800D3DCDD /* MMPProvider.swift */, + 2CAF5B0F2D00D16800D3DCDD /* SKAdNetworkProtocol.swift */, ); - path = DataProvider; + path = MMP; sourceTree = ""; }; - 2C61889D2B7A8A21006B70D7 /* NTP */ = { + 2CAF5B152D00D16800D3DCDD /* Environment */ = { isa = PBXGroup; children = ( - 2C6B5B3C2CAAF27F00F15323 /* ClimateImpactCounter */, - 2CA995262CA2C053001064CC /* NudgeCards */, - 2C61889E2B7A8A21006B70D7 /* NTPLayout.swift */, - 2C61889F2B7A8A21006B70D7 /* DefaultBrowser.swift */, - 2C6188A02B7A8A21006B70D7 /* Customization */, - 2C6188A42B7A8A21006B70D7 /* Library */, - 2C6188A82B7A8A21006B70D7 /* NTPTooltip.swift */, - 2C6188A92B7A8A21006B70D7 /* CircleButton.swift */, - 2C6188AA2B7A8A21006B70D7 /* NTPTooltip.Highlight.swift */, - 2C6188AE2B7A8A22006B70D7 /* News */, - 2C6188B22B7A8A22006B70D7 /* Logo */, - 2C6188B42B7A8A22006B70D7 /* NTPTooltipDelegate.swift */, - 2C6188B52B7A8A22006B70D7 /* Impact */, - 2C6188BC2B7A8A22006B70D7 /* AboutEcosia */, + 2CAF5B122D00D16800D3DCDD /* Environment.Auth.swift */, + 2CAF5B132D00D16800D3DCDD /* Environment.swift */, + 2CAF5B142D00D16800D3DCDD /* URLProvider.swift */, ); - path = NTP; + path = Environment; sourceTree = ""; }; - 2C6188A02B7A8A21006B70D7 /* Customization */ = { + 2CAF5B162D00D16800D3DCDD /* OptInReminder */ = { isa = PBXGroup; children = ( - 2C6188A12B7A8A21006B70D7 /* CustomizableNTPSettingConfig.swift */, - 2C6188A22B7A8A21006B70D7 /* NTPCustomizationCell.swift */, - 2C6188A32B7A8A21006B70D7 /* NTPCustomizationCellViewModel.swift */, ); - path = Customization; + path = OptInReminder; sourceTree = ""; }; - 2C6188A42B7A8A21006B70D7 /* Library */ = { + 2CAF5B1D2D00D16900D3DCDD /* RefreshingRule */ = { isa = PBXGroup; children = ( - 2C6188A52B7A8A21006B70D7 /* NTPLibraryShortcutView.swift */, - 2C6188A62B7A8A21006B70D7 /* NTPLibraryCell.swift */, - 2C6188A72B7A8A21006B70D7 /* NTPLibaryCellViewModel.swift */, + 2CAF5B192D00D16900D3DCDD /* AppUpdateRule.swift */, + 2CAF5B1A2D00D16900D3DCDD /* DeviceRegionChangeRule.swift */, + 2CAF5B1B2D00D16900D3DCDD /* RefreshingRule.swift */, + 2CAF5B1C2D00D16900D3DCDD /* TimeBasedRefreshingRule.swift */, ); - path = Library; + path = RefreshingRule; sourceTree = ""; }; - 2C6188AE2B7A8A22006B70D7 /* News */ = { + 2CAF5B1E2D00D16900D3DCDD /* RefreshingComponent */ = { isa = PBXGroup; children = ( - 2C6188AF2B7A8A22006B70D7 /* NewsController.swift */, - 2C6188B02B7A8A22006B70D7 /* NTPNewsCellViewModel.swift */, - 2C6188B12B7A8A22006B70D7 /* NTPNewsCell.swift */, + 2CAF5B1D2D00D16900D3DCDD /* RefreshingRule */, ); - path = News; + path = RefreshingComponent; sourceTree = ""; }; - 2C6188B22B7A8A22006B70D7 /* Logo */ = { + 2CAF5B252D00D16900D3DCDD /* Unleash */ = { isa = PBXGroup; children = ( - 2C6188B32B7A8A22006B70D7 /* NTPLogoCell.swift */, + 2CAF5B1E2D00D16900D3DCDD /* RefreshingComponent */, + 2CAF5B1F2D00D16900D3DCDD /* Unleash.Model.swift */, + 2CAF5B202D00D16900D3DCDD /* Unleash.swift */, + 2CAF5B212D00D16900D3DCDD /* Unleash+RefreshComponent.swift */, + 2CAF5B222D00D16900D3DCDD /* UnleashFeatureManagementSessionInitializer.swift */, + 2CAF5B232D00D16900D3DCDD /* UnleashRefreshConfigurator.swift */, + 2CAF5B242D00D16900D3DCDD /* UnleashStartRequest.swift */, ); - path = Logo; + path = Unleash; sourceTree = ""; }; - 2C6188B52B7A8A22006B70D7 /* Impact */ = { + 2CAF5B272D00D16900D3DCDD /* FeatureManagement */ = { isa = PBXGroup; children = ( - 2C6188B62B7A8A22006B70D7 /* ProgressView.swift */, - 2C6188B72B7A8A22006B70D7 /* NTPImpactCell.swift */, - 2C6188B82B7A8A22006B70D7 /* NTPImpactCellViewModel.swift */, - 2C6188B92B7A8A22006B70D7 /* NTPImpactRowView.swift */, - 2C6188BA2B7A8A22006B70D7 /* NTPImpactDividerFooter.swift */, - 2C6188BB2B7A8A22006B70D7 /* ClimateImpactInfo.swift */, + 2CAF5B252D00D16900D3DCDD /* Unleash */, + 2CAF5B262D00D16900D3DCDD /* FeatureManagementSessionInitializer.swift */, ); - path = Impact; + path = FeatureManagement; sourceTree = ""; }; - 2C6188BC2B7A8A22006B70D7 /* AboutEcosia */ = { + 2CAF5B2A2D00D16900D3DCDD /* Tabs */ = { isa = PBXGroup; children = ( - 2C6188BD2B7A8A22006B70D7 /* AboutEcosiaSection.swift */, - 2C6188BE2B7A8A22006B70D7 /* NTPAboutEcosiaCellViewModel.swift */, - 2C6188BF2B7A8A22006B70D7 /* NTPAboutEcosiaCell.swift */, + 2CAF5B282D00D16900D3DCDD /* Tab.swift */, + 2CAF5B292D00D16900D3DCDD /* Tabs.swift */, ); - path = AboutEcosia; + path = Tabs; sourceTree = ""; }; - 2C6188C22B7A8A22006B70D7 /* Theme */ = { + 2CAF5B2D2D00D16900D3DCDD /* News */ = { isa = PBXGroup; children = ( - 2C6188C32B7A8A22006B70D7 /* EcosiaThemeManager.swift */, - 2C6188C42B7A8A22006B70D7 /* EcosiaThemeColourPalette.swift */, - 2C6188C52B7A8A22006B70D7 /* EcosiaTheme.swift */, + 2CAF5B2B2D00D16900D3DCDD /* News.swift */, + 2CAF5B2C2D00D16900D3DCDD /* NewsModel.swift */, ); - path = Theme; + path = News; sourceTree = ""; }; - 2C6188C92B7A8A22006B70D7 /* Onboarding */ = { + 2CAF5B322D00D16900D3DCDD /* Pages */ = { isa = PBXGroup; children = ( - 2C6188CA2B7A8A22006B70D7 /* WelcomeTourRow.swift */, - 2C6188CB2B7A8A22006B70D7 /* Welcome.swift */, - 2C6188CC2B7A8A22006B70D7 /* WelcomeTour.Step.swift */, - 2C6188CD2B7A8A22006B70D7 /* WelcomeTourAction.swift */, - 2C6188CE2B7A8A22006B70D7 /* WelcomeTour.swift */, - 2C6188CF2B7A8A22006B70D7 /* WelcomeTourTransparent.swift */, - 2C6188D02B7A8A22006B70D7 /* WelcomeTourGreen.swift */, - 2C6188D12B7A8A22006B70D7 /* WelcomeNavigation.swift */, - 2C6188D22B7A8A22006B70D7 /* WelcomeTourProfit.swift */, + 2CAF5B302D00D16900D3DCDD /* Page.swift */, + 2CAF5B312D00D16900D3DCDD /* PageStore.swift */, ); - path = Onboarding; + path = Pages; sourceTree = ""; }; - 2C6188D32B7A8A22006B70D7 /* MultiplyImpact */ = { + 2CAF5B3B2D00D16A00D3DCDD /* Statistics */ = { isa = PBXGroup; children = ( - 2C6188D42B7A8A22006B70D7 /* MultiplyImpact.swift */, - 2C6188D52B7A8A22006B70D7 /* MultiplyImpactStep.swift */, + 2CAF5B372D00D16A00D3DCDD /* FinancialReports.swift */, + 2CAF5B382D00D16A00D3DCDD /* InvestmentsProjection.swift */, + 2CAF5B392D00D16A00D3DCDD /* Statistics.swift */, + 2CAF5B3A2D00D16A00D3DCDD /* TreesProjection.swift */, ); - path = MultiplyImpact; + path = Statistics; sourceTree = ""; }; - 2C6188D62B7A8A22006B70D7 /* Settings */ = { + 2CAF5B3D2D00D16A00D3DCDD /* Auth */ = { isa = PBXGroup; children = ( - 2C6188D72B7A8A22006B70D7 /* EcosiaDebugSettings.swift */, - 2C6188D82B7A8A22006B70D7 /* EcosiaSettings.swift */, - 2C6188D92B7A8A22006B70D7 /* NTPCustomizationSettingsViewController.swift */, ); - path = Settings; + path = Auth; sourceTree = ""; }; - 2C6188DA2B7A8A22006B70D7 /* L10N */ = { + 2CAF5B9C2D00D18300D3DCDD /* Core */ = { isa = PBXGroup; children = ( - 2C6188DB2B7A8A22006B70D7 /* Ecosia.strings */, - 2C6188DD2B7A8A22006B70D7 /* Plurals.stringsdict */, - 2C6188E52B7A8A22006B70D7 /* Scripts */, - 2C6188E82B7A8A22006B70D7 /* String.swift */, + 2CAF5BB72D00D1A700D3DCDD /* BookmarkParserTests.swift */, + 2CAF5BB92D00D1A800D3DCDD /* BookmarkSerializerTests.swift */, + 2CAF5BBE2D00D1A800D3DCDD /* BookmarkTests.swift */, + 2CAF5BCD2D00D1AA00D3DCDD /* CookieTests.swift */, + 2CAF5BD32D00D1AB00D3DCDD /* FavouritesTests.swift */, + 2CAF5BA72D00D1A700D3DCDD /* FeatureFlaggingSessionInitializerTests.swift */, + 2CAF5BCC2D00D1AA00D3DCDD /* FinancialReportsTests.swift */, + 2CAF5BBD2D00D1A800D3DCDD /* HistoryTests.swift */, + 2CAF5BBA2D00D1A800D3DCDD /* ImagesTests.swift */, + 2CAF5BCE2D00D1AA00D3DCDD /* InvestmentsProjectionTests.swift */, + 2CAF5BD22D00D1AA00D3DCDD /* LanguageTests.swift */, + 2CAF5BA52D00D1A600D3DCDD /* ListTests.swift */, + 2CAF5BC02D00D1A800D3DCDD /* LocalTests.swift */, + 2CAF5BB82D00D1A700D3DCDD /* NewsTests.swift */, + 2CAF5BCF2D00D1AA00D3DCDD /* PublishersTests.swift */, + 2CAF5BBC2D00D1A800D3DCDD /* ReferralsModelTests.swift */, + 2CAF5BCB2D00D1AA00D3DCDD /* ReferralsTests.swift */, + 2CAF5BB22D00D1A700D3DCDD /* Resources */, + 2CAF5BCA2D00D1A900D3DCDD /* SearchesCounterTests.swift */, + 2CAF5BC12D00D1A900D3DCDD /* SingularAdNetworkHelperTests.swift */, + 2CAF5BB42D00D1A700D3DCDD /* SingularServiceTests.swift */, + 2CAF5BB32D00D1A700D3DCDD /* SingularTests.swift */, + 2CAF5BBF2D00D1A800D3DCDD /* SnapshotsTests.swift */, + 2CAF5BBB2D00D1A800D3DCDD /* StatisticsTests.swift */, + 2CAF5BC22D00D1A900D3DCDD /* TabsTests.swift */, + 2CAF5BA42D00D1A600D3DCDD /* Tools */, + 2CAF5BC32D00D1A900D3DCDD /* TreesProjectionTests.swift */, + 2CAF5BB62D00D1A700D3DCDD /* UnleashFeatureManagementSessionInitializerTests.swift */, + 2CAF5BD02D00D1AA00D3DCDD /* UnleashRefreshConfiguratorTests.swift */, + 2CAF5BC42D00D1A900D3DCDD /* UnleashTests.swift */, + 2CAF5BB52D00D1A700D3DCDD /* UpgradeTests.swift */, + 2CAF5BC82D00D1A900D3DCDD /* URLProviderDependantTests */, + 2CAF5BD42D00D1AB00D3DCDD /* URLRequestTests.swift */, + 2CAF5BA62D00D1A600D3DCDD /* URLTests.swift */, + 2CAF5BC92D00D1A900D3DCDD /* UserStateTests.swift */, + 2CAF5BD12D00D1AA00D3DCDD /* UserTests.swift */, + 2CAF5B9E2D00D1A600D3DCDD /* Versions */, + 2CD7E6062D09E47E0003B02B /* Bundle+EcosiaTests.swift */, ); - path = L10N; + path = Core; sourceTree = ""; }; - 2C6188E52B7A8A22006B70D7 /* Scripts */ = { + 2CAF5B9E2D00D1A600D3DCDD /* Versions */ = { isa = PBXGroup; children = ( - 2C6188E62B7A8A22006B70D7 /* Clean.swift */, - 2C6188E72B7A8A22006B70D7 /* Validate.swift */, + 2CAF5B9D2D00D1A600D3DCDD /* User5_3.swift */, ); - path = Scripts; + path = Versions; sourceTree = ""; }; - 2C6188ED2B7A8A22006B70D7 /* FeatureManagement */ = { + 2CAF5BA42D00D1A600D3DCDD /* Tools */ = { isa = PBXGroup; children = ( - 2C6188EE2B7A8A22006B70D7 /* FeatureManagement.swift */, + 2CAF5B9F2D00D1A600D3DCDD /* BookmarkFixtures.swift */, + 2CAF5BA02D00D1A600D3DCDD /* HTTPClientMock.swift */, + 2CAF5BA12D00D1A600D3DCDD /* MockTimestampProvider.swift */, + 2CAF5BA22D00D1A600D3DCDD /* MockURLSession.swift */, + 2CAF5BA32D00D1A600D3DCDD /* MockURLSessionProtocol.swift */, ); - path = FeatureManagement; + path = Tools; sourceTree = ""; }; - 2C6188EF2B7A8A22006B70D7 /* Frontend */ = { + 2CAF5BAF2D00D1A700D3DCDD /* Bookmarks */ = { isa = PBXGroup; children = ( - 2C6188F02B7A8A22006B70D7 /* Home */, + 2CAF5BA82D00D1A700D3DCDD /* export_bookmark_ecosia.html */, + 2CAF5BA92D00D1A700D3DCDD /* import_input_bookmark_chrome.html */, + 2CAF5BAA2D00D1A700D3DCDD /* import_input_bookmark_firefox.html */, + 2CAF5BAB2D00D1A700D3DCDD /* import_input_bookmark_safari.html */, + 2CAF5BAC2D00D1A700D3DCDD /* import_output_bookmark_chrome.txt */, + 2CAF5BAD2D00D1A700D3DCDD /* import_output_bookmark_firefox.txt */, + 2CAF5BAE2D00D1A700D3DCDD /* import_output_bookmark_safari.txt */, ); - path = Frontend; + path = Bookmarks; sourceTree = ""; }; - 2C6188F02B7A8A22006B70D7 /* Home */ = { + 2CAF5BB22D00D1A700D3DCDD /* Resources */ = { isa = PBXGroup; children = ( - 2C6188F12B7A8A22006B70D7 /* EcosiaHomepageSectionType.swift */, - 2C6188F22B7A8A22006B70D7 /* TopSites */, + 2CAF5BAF2D00D1A700D3DCDD /* Bookmarks */, + 2CAF5BB02D00D1A700D3DCDD /* notifications.json */, + 2CAF5BB12D00D1A700D3DCDD /* referrals.json */, ); - path = Home; + path = Resources; sourceTree = ""; }; - 2C6188F22B7A8A22006B70D7 /* TopSites */ = { + 2CAF5BC82D00D1A900D3DCDD /* URLProviderDependantTests */ = { isa = PBXGroup; children = ( - 2C6188F32B7A8A22006B70D7 /* Cell */, + 2CAF5BC52D00D1A900D3DCDD /* ProductionURLProviderTests.swift */, + 2CAF5BC62D00D1A900D3DCDD /* StagingURLProviderTests.swift */, + 2CAF5BC72D00D1A900D3DCDD /* URLProviderTests.swift */, ); - path = TopSites; + path = URLProviderDependantTests; sourceTree = ""; }; - 2C6188F32B7A8A22006B70D7 /* Cell */ = { + 2CBCAAFA2B88EEE40080AD68 /* Configuration */ = { isa = PBXGroup; children = ( - 2C6188F42B7A8A22006B70D7 /* EcosiaTopSiteItemCell.swift */, + 2CBCAB0C2B88EEE40080AD68 /* Ecosia.xcconfig */, + 2CBCAAFB2B88EEE40080AD68 /* Ecosia.WidgetKit.xcconfig */, + 2CBCAAFD2B88EEE40080AD68 /* Ecosia.ShareTo.xcconfig */, + 2CBCAB0E2B88EEE40080AD68 /* EcosiaDebug.xcconfig */, + 2CBCAB0D2B88EEE40080AD68 /* EcosiaDebug.WidgetKit.xcconfig */, + 2CBCAB072B88EEE40080AD68 /* EcosiaDebug.ShareTo.xcconfig */, + 2CBCAB0B2B88EEE40080AD68 /* EcosiaBeta.xcconfig */, + 2CBCAB042B88EEE40080AD68 /* EcosiaBeta.WidgetKit.xcconfig */, + 2CBCAAFC2B88EEE40080AD68 /* EcosiaBeta.ShareTo.xcconfig */, + 2CBCAAFE2B88EEE40080AD68 /* EcosiaBetaDebug.xcconfig */, + 2CBCAB092B88EEE40080AD68 /* EcosiaBetaDebug.ShareTo.xcconfig */, + 2CBCAB022B88EEE40080AD68 /* EcosiaBetaDebug.WidgetKit.xcconfig */, + 2CBCAB002B88EEE40080AD68 /* Common.xcconfig */, + 2CBCAB032B88EEE40080AD68 /* Debug.xcconfig */, + 2CBCAB082B88EEE40080AD68 /* Staging.xcconfig */, + 2CBCAB062B88EEE40080AD68 /* Release.xcconfig */, + 2CEB48402BE0EE2600498471 /* Production.xcconfig */, + 2CBCAB0A2B88EEE40080AD68 /* Fennec.enterprise.xcconfig */, + 2CBCAAFF2B88EEE40080AD68 /* Firefox.xcconfig */, + 2CBCAB052B88EEE40080AD68 /* FirefoxBeta.xcconfig */, + 2CBCAB012B88EEE40080AD68 /* Fennec.xcconfig */, ); - path = Cell; + path = Configuration; sourceTree = ""; }; - 2C6188F52B7A8A22006B70D7 /* Experiments */ = { + 2CD261862CFDC5E900A040A7 /* Mocks */ = { isa = PBXGroup; children = ( - 2C6188F62B7A8A22006B70D7 /* Unleash */, + 2CD261842CFDC5E900A040A7 /* EcosiaMockThemeManager.swift */, + 2CD261852CFDC5E900A040A7 /* Welcome.swift */, ); - path = Experiments; + path = Mocks; sourceTree = ""; }; - 2C6188F62B7A8A22006B70D7 /* Unleash */ = { + 2CD261BB2CFDC5E900A040A7 /* NTP */ = { isa = PBXGroup; children = ( - 2C6188F82B7A8A22006B70D7 /* BrazeIntegrationExperiment.swift */, - 2C6B5B402CAAF3AA00F15323 /* SeedCounterNTPExperiment.swift */, - 12147F322CDBA7230009D300 /* NewsletterCardExperiment.swift */, + 2CD261B92CFDC5E900A040A7 /* NTPComponentTests.swift */, + 2CD261BA2CFDC5E900A040A7 /* NTPTests.swift */, ); - path = Unleash; + path = NTP; sourceTree = ""; }; - 2C6188FA2B7A8A22006B70D7 /* Network */ = { + 2CD262872CFDC5E900A040A7 /* Onboarding */ = { isa = PBXGroup; children = ( - 2C6188FB2B7A8A22006B70D7 /* WebsiteConnectionStatus.swift */, + 2CD262862CFDC5E900A040A7 /* OnboardingTests.swift */, ); - path = Network; + path = Onboarding; sourceTree = ""; }; - 2C6188FC2B7A8A22006B70D7 /* BrowserKitExtensions */ = { + 2CD262912CFDC5E900A040A7 /* SnapshotTests */ = { isa = PBXGroup; children = ( + 2CD261862CFDC5E900A040A7 /* Mocks */, + 2CD261BB2CFDC5E900A040A7 /* NTP */, + 2CD262872CFDC5E900A040A7 /* Onboarding */, + 2CD262882CFDC5E900A040A7 /* DeviceType.swift */, + 2CD262892CFDC5E900A040A7 /* SnapshotTests.xctestplan */, + 2CD2628A2CFDC5E900A040A7 /* environment.json */, + 2CD2628B2CFDC5E900A040A7 /* LocaleRetriever.swift */, + 2CD2628C2CFDC5E900A040A7 /* LocalizationOverrideTestingBundle.swift */, + 2CD2628D2CFDC5E900A040A7 /* snapshot_configuration.json */, + 2CD2628E2CFDC5E900A040A7 /* SnapshotBaseTests.swift */, + 2CD2628F2CFDC5E900A040A7 /* SnapshotTestHelper.swift */, + 2CD262902CFDC5E900A040A7 /* String+Extension.swift */, ); - path = BrowserKitExtensions; + path = SnapshotTests; sourceTree = ""; }; - 2C6188FF2B7A8A22006B70D7 /* Extensions */ = { + 2CD262982CFDC5EA00A040A7 /* IntegrationTests */ = { isa = PBXGroup; children = ( - 2C6189002B7A8A22006B70D7 /* BrowserViewController+Ecosia.swift */, - 2C6189012B7A8A22006B70D7 /* LegacyThemeManager+Ecosia.swift */, - 2C6189022B7A8A22006B70D7 /* UIButton+Ecosia.swift */, - 2C6189032B7A8A22006B70D7 /* UIFont+Ecosia.swift */, - 2C6189042B7A8A22006B70D7 /* ErrorPageHandler+Ecosia.swift */, - 2C6189052B7A8A22006B70D7 /* SnapKit+Ecosia.swift */, - 2C6189062B7A8A22006B70D7 /* HomepageViewController+Ecosia.swift */, - 2C6189072B7A8A22006B70D7 /* SimpleToast+Ecosia.swift */, - 2C6189082B7A8A22006B70D7 /* UIView+maskedCorners.swift */, - 2C6189092B7A8A22006B70D7 /* AppInfo+Ecosia.swift */, - 2C61890A2B7A8A22006B70D7 /* NumberFormatter+Ecosia.swift */, - 2C61890B2B7A8A22006B70D7 /* URL+Ecosia.swift */, - 2C61890C2B7A8A22006B70D7 /* BrowserCoordinator+Ecosia.swift */, - 2C61890D2B7A8A22006B70D7 /* AppSettingsTableViewController+Ecosia.swift */, - 2C61890E2B7A8A22006B70D7 /* DeviceInfo+Ecosia.swift */, - 2C61890F2B7A8A22006B70D7 /* Toolbar+URLBar */, - 2C03A4142CB7C7CC00AB228B /* DispatchQueueHelper+BuildChannel.swift */, + 2CD262972CFDC5EA00A040A7 /* AppDelegateFeatureManagementIntegrationTests.swift */, ); - path = Extensions; + path = IntegrationTests; sourceTree = ""; }; - 2C61890F2B7A8A22006B70D7 /* Toolbar+URLBar */ = { + 2CD2629C2CFDC5EA00A040A7 /* Mocks */ = { isa = PBXGroup; children = ( - 2C6189102B7A8A22006B70D7 /* ConnectionStatusImage+WebsiteConnectionTypeStatus.swift */, - 2C6189122B7A8A22006B70D7 /* ConnectionStatusImage.swift */, + 2C2101242D0B0D9B00CBE7EC /* MockNewsModel.swift */, + 2C2101262D0B0D9B00CBE7EC /* MockUNNotificationSettings.swift */, + 2C2101252D0B0D9B00CBE7EC /* MockWelcomeDelegate.swift */, + 2CD2629B2CFDC5EA00A040A7 /* MockAppVersionInfoProvider.swift */, ); - path = "Toolbar+URLBar"; + path = Mocks; sourceTree = ""; }; - 2C6189132B7A8A22006B70D7 /* Entitlements */ = { + 2CD262A22CFDC5EA00A040A7 /* ClimateImpactCounter */ = { isa = PBXGroup; children = ( - 2C08319A2B89124000BD7134 /* AppExtensions */, - 2C6189142B7A8A22006B70D7 /* EcosiaBeta.entitlements */, - 2C6189152B7A8A22006B70D7 /* Ecosia.entitlements */, + 2CD262A02CFDC5EA00A040A7 /* UnleashUserDefaultsSeedProgressManagerTests.swift */, + 2CD262A12CFDC5EA00A040A7 /* UserDefaultsSeedProgressManagerTests.swift */, ); - path = Entitlements; + path = ClimateImpactCounter; sourceTree = ""; }; - 2C6189162B7A8A22006B70D7 /* Bookmarks */ = { + 2CD262A82CFDC5EB00A040A7 /* Analytics */ = { isa = PBXGroup; children = ( - 2C6189172B7A8A22006B70D7 /* BookmarksExchange.swift */, + 2CD262A62CFDC5EB00A040A7 /* AnalyticsSpyTests.swift */, + 2CD262A72CFDC5EB00A040A7 /* AnalyticsTests.swift */, ); - path = Bookmarks; + path = Analytics; sourceTree = ""; }; - 2C6189192B7A8A22006B70D7 /* Fake */ = { + 2CD263C72CFDC76800A040A7 /* Scripts */ = { isa = PBXGroup; children = ( - 2C61891A2B7A8A22006B70D7 /* FakeTelemetry.swift */, - 2C61891B2B7A8A22006B70D7 /* FakeNimbus.swift */, - 2C61891C2B7A8A22006B70D7 /* FakeSentry.swift */, + 2CD263C52CFDC76800A040A7 /* Clean.swift */, + 2CD263C62CFDC76800A040A7 /* Validate.swift */, ); - path = Fake; + path = Scripts; sourceTree = ""; }; - 2C6189232B7A8A22006B70D7 /* Helpers */ = { + 2CD263E12CFDC76800A040A7 /* L10N */ = { isa = PBXGroup; children = ( - 2C6189242B7A8A22006B70D7 /* AppInfoProvider */, - 2C6189272B7A8A22006B70D7 /* EcosiaInstallType */, - 2C61892A2B7A8A22006B70D7 /* Version */, + 2CD263C72CFDC76800A040A7 /* Scripts */, + 2CD263C92CFDC76800A040A7 /* Ecosia.strings */, + 2CD263CB2CFDC76800A040A7 /* Ecosia.strings */, + 2CD263CD2CFDC76800A040A7 /* Ecosia.strings */, + 2CD263CF2CFDC76800A040A7 /* Ecosia.strings */, + 2CD263D12CFDC76800A040A7 /* Ecosia.strings */, + 2CD263D32CFDC76800A040A7 /* Ecosia.strings */, + 2CD263D52CFDC76800A040A7 /* Plurals.stringsdict */, + 2CD263D72CFDC76800A040A7 /* Plurals.stringsdict */, + 2CD263D92CFDC76800A040A7 /* Plurals.stringsdict */, + 2CD263DB2CFDC76800A040A7 /* Plurals.stringsdict */, + 2CD263DD2CFDC76800A040A7 /* Plurals.stringsdict */, + 2CD263DF2CFDC76800A040A7 /* Plurals.stringsdict */, + 2CD263E02CFDC76800A040A7 /* String.swift */, ); - path = Helpers; + path = L10N; sourceTree = ""; }; - 2C6189242B7A8A22006B70D7 /* AppInfoProvider */ = { + 2CD263E42CFDC76800A040A7 /* AppInfoProvider */ = { isa = PBXGroup; children = ( - 2C6189252B7A8A22006B70D7 /* DefaultAppVersionInfoProvider.swift */, - 2C6189262B7A8A22006B70D7 /* AppVersionInfoProvider.swift */, + 2CD263E22CFDC76800A040A7 /* AppVersionInfoProvider.swift */, + 2CD263E32CFDC76800A040A7 /* DefaultAppVersionInfoProvider.swift */, ); path = AppInfoProvider; sourceTree = ""; }; - 2C6189272B7A8A22006B70D7 /* EcosiaInstallType */ = { + 2CD263E72CFDC76800A040A7 /* EcosiaInstallType */ = { isa = PBXGroup; children = ( - 2C6189282B7A8A22006B70D7 /* EcosiaInstallType.swift */, - 2C6189292B7A8A22006B70D7 /* EcosiaInstallType+Extensions.swift */, + 2CD263E52CFDC76800A040A7 /* EcosiaInstallType.swift */, + 2CD263E62CFDC76800A040A7 /* EcosiaInstallType+Extensions.swift */, ); path = EcosiaInstallType; sourceTree = ""; }; - 2C61892A2B7A8A22006B70D7 /* Version */ = { + 2CD263EA2CFDC76800A040A7 /* Version */ = { isa = PBXGroup; children = ( - 2C61892B2B7A8A22006B70D7 /* Version.swift */, - 2C61892C2B7A8A22006B70D7 /* Version+Extensions.swift */, + 2CD263E82CFDC76800A040A7 /* Version.swift */, + 2CD263E92CFDC76800A040A7 /* Version+Extensions.swift */, ); path = Version; sourceTree = ""; }; - 2C61892D2B7A8A22006B70D7 /* Analytics */ = { + 2CD263EB2CFDC76800A040A7 /* Helpers */ = { + isa = PBXGroup; + children = ( + 2CD263E42CFDC76800A040A7 /* AppInfoProvider */, + 2CD263E72CFDC76800A040A7 /* EcosiaInstallType */, + 2CD263EA2CFDC76800A040A7 /* Version */, + ); + path = Helpers; + sourceTree = ""; + }; + 2CD263EF2CFDC76800A040A7 /* Analytics */ = { isa = PBXGroup; children = ( - 2C61892E2B7A8A22006B70D7 /* Analytics.swift */, - 2C61892F2B7A8A22006B70D7 /* Analytics+Configuration.swift */, - 2C6189302B7A8A22006B70D7 /* Analytics.Values.swift */, - 2C9258DA2CEFB26500C6BB8D /* AnalyticsNotificationSettings.swift */, + 2C21011C2D0B042300CBE7EC /* AnalyticsNotificationSettings.swift */, + 2CD263EC2CFDC76800A040A7 /* Analytics.swift */, + 2CD263ED2CFDC76800A040A7 /* Analytics.Values.swift */, + 2CD263EE2CFDC76800A040A7 /* Analytics+Configuration.swift */, ); path = Analytics; sourceTree = ""; }; - 2C69DA732C62185000D7F69F /* Mocks */ = { + 2CD263FF2CFDC76900A040A7 /* FeatureManagement */ = { isa = PBXGroup; children = ( - 2C69DA742C62185A00D7F69F /* Welcome.swift */, - 2C82625D2C66661700E2A255 /* EcosiaMockThemeManager.swift */, + 2CD263FE2CFDC76900A040A7 /* FeatureManagement.swift */, ); - path = Mocks; + path = FeatureManagement; sourceTree = ""; }; - 2C69DA872C63A53C00D7F69F /* NTP */ = { + 2CD264042CFDC76900A040A7 /* AppExtensions */ = { isa = PBXGroup; children = ( - 2C69DA762C62243300D7F69F /* NTPTests.swift */, - 2C69DA832C62B44B00D7F69F /* NTPComponentTests.swift */, + 2CD264022CFDC76900A040A7 /* Ecosia.entitlements */, + 2CD264032CFDC76900A040A7 /* EcosiaBeta.entitlements */, ); - path = NTP; + path = AppExtensions; sourceTree = ""; }; - 2C69DA882C63A54100D7F69F /* Onboarding */ = { + 2CD264072CFDC76900A040A7 /* Entitlements */ = { isa = PBXGroup; children = ( - 2CD368492C5BC31700972871 /* OnboardingTests.swift */, + 2CD264042CFDC76900A040A7 /* AppExtensions */, + 2CD264052CFDC76900A040A7 /* Ecosia.entitlements */, + 2CD264062CFDC76900A040A7 /* EcosiaBeta.entitlements */, ); - path = Onboarding; + path = Entitlements; sourceTree = ""; }; - 2C6B5B3C2CAAF27F00F15323 /* ClimateImpactCounter */ = { + 2CD264082CFDC76900A040A7 /* Auth */ = { isa = PBXGroup; children = ( - 2C4ABD482CB58E4F00FF86F9 /* Sparkle.swift */, - 2C6B5B3A2CAAF27F00F15323 /* NTPSeedCounterCell.swift */, - 2C6B5B3B2CAAF27F00F15323 /* NTPSeedCounterViewModel.swift */, - 2CCBB5222CAE9826006E2E10 /* ArcProgressView.swift */, - 2CCBB5242CAEA9DF006E2E10 /* SeedProgressView.swift */, - 2CCBB5262CAEAD53006E2E10 /* SeedCounterView.swift */, - 2CCBB5362CAF0E8C006E2E10 /* SeedCounterHiddenSettings.swift */, - 2C5A5E682CB53E05005BFE8B /* SeedProgressManager */, - 2C5A5E642CB53DB7005BFE8B /* SeedCounterConfig.swift */, ); - path = ClimateImpactCounter; + path = Auth; sourceTree = ""; }; - 2C7DBABA2C4EA14C00BCD03F /* IntegrationTests */ = { + 2CD264752CFDC76A00A040A7 /* Fake */ = { isa = PBXGroup; children = ( - 2C7DBABB2C4EA17200BCD03F /* AppDelegateFeatureManagementIntegrationTests.swift */, + 2CD264722CFDC76A00A040A7 /* FakeNimbus.swift */, + 2CD264732CFDC76A00A040A7 /* FakeSentry.swift */, + 2CD264742CFDC76A00A040A7 /* FakeTelemetry.swift */, ); - path = IntegrationTests; + path = Fake; sourceTree = ""; }; - 2C872A522B8CD45F00B318A0 /* Mocks */ = { + 2CD265162CFDCB8A00A040A7 /* Ecosia */ = { isa = PBXGroup; children = ( - 2C872A532B8CD47600B318A0 /* MockAppVersionInfoProvider.swift */, - 2C9A62BF2CDE1F7600CDA7D1 /* MockWelcomeDelegate.swift */, - 2C9A62C12CDE4A3B00CDA7D1 /* MockNewsModel.swift */, - 2C9258D82CEF97B100C6BB8D /* MockUNNotificationSettings.swift */, + 2CD266542CFF56EA00A040A7 /* Network */, + 2CD266242CFE3E8500A040A7 /* Experiments */, + 2CD266222CFE3AD900A040A7 /* Bookmarks */, + 2CD266152CFE38AD00A040A7 /* Frontend */, + 2CD266192CFE38AD00A040A7 /* Settings */, + 2CD265C32CFE382C00A040A7 /* UI */, + 2CD2654F2CFDCF0900A040A7 /* Extensions */, ); - path = Mocks; + path = Ecosia; sourceTree = ""; }; - 2CA995262CA2C053001064CC /* NudgeCards */ = { + 2CD2654F2CFDCF0900A040A7 /* Extensions */ = { isa = PBXGroup; children = ( - 12147F2D2CDA3CB40009D300 /* Newsletter */, - 2CA995272CA2C06A001064CC /* NTPConfigurableNudgeCardCell.swift */, - 2CA995292CA2C0BB001064CC /* NTPConfigurableNudgeCardCellViewModel.swift */, + 2CD266502CFF56CB00A040A7 /* Toolbar+URLBar */, + 2CD265402CFDCF0900A040A7 /* AppSettingsTableViewController+Ecosia.swift */, + 2CD265412CFDCF0900A040A7 /* BrowserCoordinator+Ecosia.swift */, + 2CD265422CFDCF0900A040A7 /* BrowserViewController+Ecosia.swift */, + 2CD265442CFDCF0900A040A7 /* DispatchQueueHelper+BuildChannel.swift */, + 2CD265452CFDCF0900A040A7 /* ErrorPageHandler+Ecosia.swift */, + 2CD265462CFDCF0900A040A7 /* HomepageViewController+Ecosia.swift */, + 2CD265472CFDCF0900A040A7 /* LegacyThemeManager+Ecosia.swift */, + 2CD265482CFDCF0900A040A7 /* NumberFormatter+Ecosia.swift */, + 2CD265492CFDCF0900A040A7 /* SimpleToast+Ecosia.swift */, + 2CD2654A2CFDCF0900A040A7 /* SnapKit+Ecosia.swift */, + 2CD2654B2CFDCF0900A040A7 /* UIButton+Ecosia.swift */, + 2CD2654C2CFDCF0900A040A7 /* UIFont+Ecosia.swift */, + 2CD2654D2CFDCF0900A040A7 /* UIView+maskedCorners.swift */, + 2CD2654E2CFDCF0900A040A7 /* URL+Ecosia.swift */, + 2CD265612CFDCF6D00A040A7 /* UIImage+Ecosia.swift */, ); - path = NudgeCards; + path = Extensions; sourceTree = ""; }; - 2CABD71A2C11E07300A0750F /* PersistedGenerated */ = { + 2CD265672CFE382C00A040A7 /* MultiplyImpact */ = { isa = PBXGroup; children = ( - 2C0360D92C1747E6006706F2 /* FxNimbus.swift */, + 2CD265652CFE382C00A040A7 /* MultiplyImpact.swift */, + 2CD265662CFE382C00A040A7 /* MultiplyImpactStep.swift */, ); - path = PersistedGenerated; + path = MultiplyImpact; sourceTree = ""; }; - 2CBCAAFA2B88EEE40080AD68 /* Configuration */ = { + 2CD2656B2CFE382C00A040A7 /* AboutEcosia */ = { isa = PBXGroup; children = ( - 2CBCAB0C2B88EEE40080AD68 /* Ecosia.xcconfig */, - 2CBCAAFB2B88EEE40080AD68 /* Ecosia.WidgetKit.xcconfig */, - 2CBCAAFD2B88EEE40080AD68 /* Ecosia.ShareTo.xcconfig */, - 2CBCAB0E2B88EEE40080AD68 /* EcosiaDebug.xcconfig */, - 2CBCAB0D2B88EEE40080AD68 /* EcosiaDebug.WidgetKit.xcconfig */, - 2CBCAB072B88EEE40080AD68 /* EcosiaDebug.ShareTo.xcconfig */, - 2CBCAB0B2B88EEE40080AD68 /* EcosiaBeta.xcconfig */, - 2CBCAB042B88EEE40080AD68 /* EcosiaBeta.WidgetKit.xcconfig */, - 2CBCAAFC2B88EEE40080AD68 /* EcosiaBeta.ShareTo.xcconfig */, - 2CBCAAFE2B88EEE40080AD68 /* EcosiaBetaDebug.xcconfig */, - 2CBCAB092B88EEE40080AD68 /* EcosiaBetaDebug.ShareTo.xcconfig */, - 2CBCAB022B88EEE40080AD68 /* EcosiaBetaDebug.WidgetKit.xcconfig */, - 2CBCAB002B88EEE40080AD68 /* Common.xcconfig */, - 2CBCAB032B88EEE40080AD68 /* Debug.xcconfig */, - 2CBCAB082B88EEE40080AD68 /* Staging.xcconfig */, - 2CBCAB062B88EEE40080AD68 /* Release.xcconfig */, - 2CEB48402BE0EE2600498471 /* Production.xcconfig */, - 2CBCAB0A2B88EEE40080AD68 /* Fennec.enterprise.xcconfig */, - 2CBCAAFF2B88EEE40080AD68 /* Firefox.xcconfig */, - 2CBCAB052B88EEE40080AD68 /* FirefoxBeta.xcconfig */, - 2CBCAB012B88EEE40080AD68 /* Fennec.xcconfig */, + 2CD265682CFE382C00A040A7 /* AboutEcosiaSection.swift */, + 2CD265692CFE382C00A040A7 /* NTPAboutEcosiaCell.swift */, + 2CD2656A2CFE382C00A040A7 /* NTPAboutEcosiaCellViewModel.swift */, ); - path = Configuration; + path = AboutEcosia; sourceTree = ""; }; - 2CD3683F2C5BA30A00972871 /* SnapshotTests */ = { + 2CD2656C2CFE382C00A040A7 /* Account */ = { isa = PBXGroup; children = ( - 2C8262572C64424300E2A255 /* EcosiaSnapshotTests.xctestplan */, - 2C69DA882C63A54100D7F69F /* Onboarding */, - 2C69DA872C63A53C00D7F69F /* NTP */, - 2C69DA732C62185000D7F69F /* Mocks */, - 2C69DA712C62175400D7F69F /* SnapshotTestHelper.swift */, - 2C69DA912C63A92D00D7F69F /* SnapshotBaseTests.swift */, - 2C69DA932C63B0C000D7F69F /* String+Extension.swift */, - 2C8262592C64BB9300E2A255 /* DeviceType.swift */, - 2C82625B2C6648D900E2A255 /* LocalizationOverrideTestingBundle.swift */, - 2C5B81C72C75388300B81D95 /* LocaleRetriever.swift */, - 2C19DACE2C74C7BF00D2641C /* snapshot_configuration.json */, - 2C4D165F2C76360800E89C95 /* environment.json */, ); - path = SnapshotTests; + path = Account; sourceTree = ""; }; - 2CE2945C2B7FC51E006C22B2 /* EcosiaTests */ = { - isa = PBXGroup; - children = ( - 1285E2B32CC293A20053506B /* Analytics */, - 2C16B7642CAF2417006118F8 /* ClimateImpactCounter */, - 2CD3683F2C5BA30A00972871 /* SnapshotTests */, - 2C7DBABA2C4EA14C00BCD03F /* IntegrationTests */, - 2C872A522B8CD45F00B318A0 /* Mocks */, - 2CE294672B7FC5A4006C22B2 /* EcosiaInstallTypeTests.swift */, - 2CE2946C2B7FC5A5006C22B2 /* EcosiaNTPTooltipHighlightTests.swift */, - 2CE2946D2B7FC5A5006C22B2 /* EcosiaPageActionMenuCellTests.swift */, - 2CE294702B7FC5A6006C22B2 /* VersionTests.swift */, - 2CE2946B2B7FC5A5006C22B2 /* WhatsNewLocalDataProviderTests.swift */, - 2C872A562B8CD65100B318A0 /* EcosiaHomeViewModelTests.swift */, - 2C26EA132C04CAD100795552 /* EcosiaTopSitesHelperTests.swift */, - 2CABD7272C12EF1E00A0750F /* PrivateModeButtonTests.swift */, - 2C2349A22C57E5BC007A5894 /* EcosiaPerformanceTestHistory.swift */, - 2CD48B7E2C7F7E4100A70908 /* EcosiaOverlayModeManagerTests.swift */, - 126509882CDA31890011BA36 /* BrazeServiceTests.swift */, + 2CD265712CFE382C00A040A7 /* SeedProgressManager */ = { + isa = PBXGroup; + children = ( + 2CD2656F2CFE382C00A040A7 /* SeedProgressManagerProtocol.swift */, + 2CD265702CFE382C00A040A7 /* UserDefaultsSeedProgressManager.swift */, ); - path = EcosiaTests; + path = SeedProgressManager; sourceTree = ""; }; - 2F44FC551A9E83E200FD20CC /* Settings */ = { + 2CD2657A2CFE382C00A040A7 /* ClimateImpactCounter */ = { isa = PBXGroup; children = ( - 216A0D772A40E83F008077BA /* ThemeSettings */, - 8A3EF7ED2A2FCEC900796E3A /* Main */, - EBB895322193FFF400EB91A0 /* ContentBlockerSettingViewController.swift */, - 7B4980A71CE363ED0017547C /* Settings.xcassets */, - 8D8251721F4DE67E00780643 /* AdvancedAccountSettingViewController.swift */, - 0B62EFD11AD63CD100ACB9CD /* Clearables.swift */, - D3E8EEE71B97A87A001900FB /* ClearPrivateDataTableViewController.swift */, - 3B39EDCA1E16E1AA00EF029F /* CustomSearchViewController.swift */, - BCFF93F32AAF9879005B5B71 /* FirefoxSuggestSettingsViewController.swift */, - DFACBF83277B9B36003D5F41 /* HomepageSettings */, - C8656D76270F858900E199EA /* TabsSettingsViewControler.swift */, - C82F4C2A29AE2DF0005BD116 /* NotificationsSettingsViewController.swift */, - D81E45121F82C56C004EFFBA /* NewTabContentSettingsViewController.swift */, - 2F44FCCA1A9E972E00FD20CC /* SearchEnginePicker.swift */, - 2F44FCC61A9E8CF500FD20CC /* SearchSettingsTableViewController.swift */, - 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */, - D04CD717215EBD85004FF5B0 /* SettingsLoadingView.swift */, - 2F44FC711A9E840300FD20CC /* SettingsNavigationController.swift */, - 2F44FCC41A9E85E900FD20CC /* SettingsTableViewController.swift */, - F85C7F0D2711C555004BDBA4 /* SettingsViewController.swift */, - D821E9052141B71C00452C55 /* SiriSettingsViewController.swift */, - CEFA977D1FAA6B490016F365 /* SyncContentSettingsViewController.swift */, - 8AE1E1CE27B191160024C45E /* SearchBar */, - C22753422A3CA25100B9C0D1 /* WebsiteDataManagement */, - 211046CC2A7D842A00A7309F /* TPAccessoryInfo.swift */, + 2CD265712CFE382C00A040A7 /* SeedProgressManager */, + 2CD265722CFE382C00A040A7 /* ArcProgressView.swift */, + 2CD265732CFE382C00A040A7 /* NTPSeedCounterCell.swift */, + 2CD265742CFE382C00A040A7 /* NTPSeedCounterViewModel.swift */, + 2CD265752CFE382C00A040A7 /* SeedCounterConfig.swift */, + 2CD265762CFE382C00A040A7 /* SeedCounterHiddenSettings.swift */, + 2CD265772CFE382C00A040A7 /* SeedCounterView.swift */, + 2CD265782CFE382C00A040A7 /* SeedProgressView.swift */, + 2CD265792CFE382C00A040A7 /* Sparkle.swift */, ); - path = Settings; + path = ClimateImpactCounter; sourceTree = ""; }; - 2FA435FC1ABB83B4008031D1 /* Account */ = { + 2CD2657E2CFE382C00A040A7 /* Customization */ = { isa = PBXGroup; children = ( - 2F14E1391ABB890800FF98DB /* Account-Bridging-Header.h */, - 3905B4D41E8E7A6B0027D953 /* FxAPushMessageHandler.swift */, - 2FA435FD1ABB83B4008031D1 /* Supporting Files */, + 2CD2657B2CFE382C00A040A7 /* CustomizableNTPSettingConfig.swift */, + 2CD2657C2CFE382C00A040A7 /* NTPCustomizationCell.swift */, + 2CD2657D2CFE382C00A040A7 /* NTPCustomizationCellViewModel.swift */, ); - path = Account; + path = Customization; sourceTree = ""; }; - 2FA435FD1ABB83B4008031D1 /* Supporting Files */ = { + 2CD265852CFE382C00A040A7 /* Impact */ = { isa = PBXGroup; children = ( - 2FA435FE1ABB83B4008031D1 /* Info.plist */, + 2CD2657F2CFE382C00A040A7 /* ClimateImpactInfo.swift */, + 2CD265802CFE382C00A040A7 /* NTPImpactCell.swift */, + 2CD265812CFE382C00A040A7 /* NTPImpactCellViewModel.swift */, + 2CD265822CFE382C00A040A7 /* NTPImpactDividerFooter.swift */, + 2CD265832CFE382C00A040A7 /* NTPImpactRowView.swift */, + 2CD265842CFE382C00A040A7 /* ProgressView.swift */, ); - name = "Supporting Files"; + path = Impact; sourceTree = ""; }; - 2FA4360B1ABB83B4008031D1 /* AccountTests */ = { + 2CD265892CFE382C00A040A7 /* Library */ = { isa = PBXGroup; children = ( - 39B0646D1E7ADA4B000BE173 /* Push */, - 2FA4360C1ABB83B4008031D1 /* Supporting Files */, + 2CD265862CFE382C00A040A7 /* NTPLibaryCellViewModel.swift */, + 2CD265872CFE382C00A040A7 /* NTPLibraryCell.swift */, + 2CD265882CFE382C00A040A7 /* NTPLibraryShortcutView.swift */, ); - path = AccountTests; + path = Library; sourceTree = ""; }; - 2FA4360C1ABB83B4008031D1 /* Supporting Files */ = { + 2CD2658B2CFE382C00A040A7 /* Logo */ = { isa = PBXGroup; children = ( - 2FA4360D1ABB83B4008031D1 /* Info.plist */, + 2CD2658A2CFE382C00A040A7 /* NTPLogoCell.swift */, ); - name = "Supporting Files"; + path = Logo; sourceTree = ""; }; - 2FCAE21B1ABB51F800877008 /* Storage */ = { + 2CD2658F2CFE382C00A040A7 /* News */ = { isa = PBXGroup; children = ( - 45D5EDCB292D839700311934 /* PinnedSites.swift */, - 45CC573D28AD8B64006D55AA /* Generated */, - 45CC573828AD8881006D55AA /* metrics.yaml */, - 74B195431CF503FC007F36EF /* RecentlyClosedTabs.swift */, - 2FCAE33D1ABB5F1800877008 /* Storage-Bridging-Header.h */, - D37DE2821CA2047500A5EC69 /* CertStore.swift */, - 28C4AB711AD42D4300D9ACE3 /* Clients.swift */, - 2FCAE2411ABB531100877008 /* Cursor.swift */, - 28302E3F1AF0747800521E2E /* DatabaseError.swift */, - 394CF6CE1BAA493C00906917 /* DefaultSuggestedSites.swift */, - D3BF8CBA1B7425570007AFE6 /* DiskImageStore.swift */, - E65075C11E37F956006961AC /* ExtensionUtils.swift */, - 2FCAE2431ABB531100877008 /* FileAccessor.swift */, - E677F0531D94247300ECF1FB /* Metadata.swift */, - 285D3B671B4380B70035FD22 /* Queue.swift */, - E6FF6AC91D873CFF0070C294 /* PageMetadata.swift */, - D076971E206AC60900FACFD8 /* ReadingList.swift */, - 2FCAE2471ABB531100877008 /* RemoteTabs.swift */, - 2829D39F1C2F0AD400DCF931 /* Sharing.swift */, - 2FCAE2481ABB531100877008 /* Site.swift */, - 0B54BD181B698B7C004C822C /* SuggestedSites.swift */, - 2FCAE25C1ABB531100877008 /* Visit.swift */, - D057B2AC22022617000614E0 /* Rust */, - 2FCAE2491ABB531100877008 /* SQL */, - 2FCAE25A1ABB531100877008 /* ThirdParty */, - BD6CC84129CDDA3400546A5D /* ZoomLevelStore.swift */, + 2CD2658C2CFE382C00A040A7 /* NewsController.swift */, + 2CD2658D2CFE382C00A040A7 /* NTPNewsCell.swift */, + 2CD2658E2CFE382C00A040A7 /* NTPNewsCellViewModel.swift */, ); - path = Storage; + path = News; sourceTree = ""; }; - 2FCAE22A1ABB51F800877008 /* StorageTests */ = { + 2CD265922CFE382C00A040A7 /* Newsletter */ = { isa = PBXGroup; children = ( - F8AAC1B5296637B7000BCDEC /* RustAutofillTests.swift */, + 2CD265902CFE382C00A040A7 /* NTPNewsletterCardCell.swift */, + 2CD265912CFE382C00A040A7 /* NTPNewsletterCardViewModel.swift */, + ); + path = Newsletter; + sourceTree = ""; + }; + 2CD265952CFE382C00A040A7 /* NudgeCards */ = { + isa = PBXGroup; + children = ( + 2CD265922CFE382C00A040A7 /* Newsletter */, + 2CD265932CFE382C00A040A7 /* NTPConfigurableNudgeCardCell.swift */, + 2CD265942CFE382C00A040A7 /* NTPConfigurableNudgeCardCellViewModel.swift */, + ); + path = NudgeCards; + sourceTree = ""; + }; + 2CD2659C2CFE382C00A040A7 /* NTP */ = { + isa = PBXGroup; + children = ( + 2CD2656B2CFE382C00A040A7 /* AboutEcosia */, + 2CD2656C2CFE382C00A040A7 /* Account */, + 2CD2657A2CFE382C00A040A7 /* ClimateImpactCounter */, + 2CD2657E2CFE382C00A040A7 /* Customization */, + 2CD265852CFE382C00A040A7 /* Impact */, + 2CD265892CFE382C00A040A7 /* Library */, + 2CD2658B2CFE382C00A040A7 /* Logo */, + 2CD2658F2CFE382C00A040A7 /* News */, + 2CD265952CFE382C00A040A7 /* NudgeCards */, + 2CD265962CFE382C00A040A7 /* CircleButton.swift */, + 2CD265972CFE382C00A040A7 /* DefaultBrowser.swift */, + 2CD265982CFE382C00A040A7 /* NTPLayout.swift */, + 2CD265992CFE382C00A040A7 /* NTPTooltip.Highlight.swift */, + 2CD2659A2CFE382C00A040A7 /* NTPTooltip.swift */, + 2CD2659B2CFE382C00A040A7 /* NTPTooltipDelegate.swift */, + ); + path = NTP; + sourceTree = ""; + }; + 2CD265A62CFE382C00A040A7 /* Onboarding */ = { + isa = PBXGroup; + children = ( + 2CD2659D2CFE382C00A040A7 /* Welcome.swift */, + 2CD2659E2CFE382C00A040A7 /* WelcomeNavigation.swift */, + 2CD2659F2CFE382C00A040A7 /* WelcomeTour.Step.swift */, + 2CD265A02CFE382C00A040A7 /* WelcomeTour.swift */, + 2CD265A12CFE382C00A040A7 /* WelcomeTourAction.swift */, + 2CD265A22CFE382C00A040A7 /* WelcomeTourGreen.swift */, + 2CD265A32CFE382C00A040A7 /* WelcomeTourProfit.swift */, + 2CD265A42CFE382C00A040A7 /* WelcomeTourRow.swift */, + 2CD265A52CFE382C00A040A7 /* WelcomeTourTransparent.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; + 2CD265AA2CFE382C00A040A7 /* PageAction */ = { + isa = PBXGroup; + children = ( + 2CD265A72CFE382C00A040A7 /* PageActionMenu.swift */, + 2CD265A82CFE382C00A040A7 /* PageActionMenuCell.swift */, + 2CD265A92CFE382C00A040A7 /* PageActionsShortcutsHeader.swift */, + ); + path = PageAction; + sourceTree = ""; + }; + 2CD265AE2CFE382C00A040A7 /* Theme */ = { + isa = PBXGroup; + children = ( + 2CD265AB2CFE382C00A040A7 /* EcosiaTheme.swift */, + 2CD265AC2CFE382C00A040A7 /* EcosiaThemeColourPalette.swift */, + 2CD265AD2CFE382C00A040A7 /* EcosiaThemeManager.swift */, + ); + path = Theme; + sourceTree = ""; + }; + 2CD265B12CFE382C00A040A7 /* DataProvider */ = { + isa = PBXGroup; + children = ( + 2CD265AF2CFE382C00A040A7 /* WhatsNewDataProvider.swift */, + 2CD265B02CFE382C00A040A7 /* WhatsNewLocalDataProvider.swift */, + ); + path = DataProvider; + sourceTree = ""; + }; + 2CD265B62CFE382C00A040A7 /* WhatsNew */ = { + isa = PBXGroup; + children = ( + 2CD265B12CFE382C00A040A7 /* DataProvider */, + 2CD265B22CFE382C00A040A7 /* WhatsNewCell.swift */, + 2CD265B32CFE382C00A040A7 /* WhatsNewItem.swift */, + 2CD265B42CFE382C00A040A7 /* WhatsNewViewController.swift */, + 2CD265B52CFE382C00A040A7 /* WhatsNewViewModel.swift */, + ); + path = WhatsNew; + sourceTree = ""; + }; + 2CD265C32CFE382C00A040A7 /* UI */ = { + isa = PBXGroup; + children = ( + 2CAF5AE62D00B1D300D3DCDD /* LaunchScreen */, + 2CD265672CFE382C00A040A7 /* MultiplyImpact */, + 2CD2659C2CFE382C00A040A7 /* NTP */, + 2CD265A62CFE382C00A040A7 /* Onboarding */, + 2CD265AA2CFE382C00A040A7 /* PageAction */, + 2CD265AE2CFE382C00A040A7 /* Theme */, + 2CD265B62CFE382C00A040A7 /* WhatsNew */, + 2CD265B72CFE382C00A040A7 /* Colours.xcassets */, + 2CD265B82CFE382C00A040A7 /* Ecosia.xcassets */, + 2CD265B92CFE382C00A040A7 /* EcosiaFindInPageBar.swift */, + 2CD265BA2CFE382C00A040A7 /* EcosiaNavigation.swift */, + 2CD265BB2CFE382C00A040A7 /* EmptyBookmarksView.swift */, + 2CD265BC2CFE382C00A040A7 /* EmptyBookmarksViewDelegate.swift */, + 2CD265BD2CFE382C00A040A7 /* EmptyHeader.swift */, + 2CD265BE2CFE382C00A040A7 /* EmptyReadingListView.swift */, + 2CD265BF2CFE382C00A040A7 /* FilterController.swift */, + 2CD265C02CFE382C00A040A7 /* LoadingScreen.swift */, + 2CD265C12CFE382C00A040A7 /* MarketsController.swift */, + 2CD265C22CFE382C00A040A7 /* SemanticColor.swift */, + 2CD2661F2CFE39E300A040A7 /* EcosiaPrimaryButton.swift */, + ); + path = UI; + sourceTree = ""; + }; + 2CD266112CFE38AD00A040A7 /* Cell */ = { + isa = PBXGroup; + children = ( + 2CD266102CFE38AD00A040A7 /* EcosiaTopSiteItemCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; + 2CD266122CFE38AD00A040A7 /* TopSites */ = { + isa = PBXGroup; + children = ( + 2CD266112CFE38AD00A040A7 /* Cell */, + ); + path = TopSites; + sourceTree = ""; + }; + 2CD266142CFE38AD00A040A7 /* Home */ = { + isa = PBXGroup; + children = ( + 2CD266122CFE38AD00A040A7 /* TopSites */, + 2CD266132CFE38AD00A040A7 /* EcosiaHomepageSectionType.swift */, + ); + path = Home; + sourceTree = ""; + }; + 2CD266152CFE38AD00A040A7 /* Frontend */ = { + isa = PBXGroup; + children = ( + 2CD266142CFE38AD00A040A7 /* Home */, + ); + path = Frontend; + sourceTree = ""; + }; + 2CD266192CFE38AD00A040A7 /* Settings */ = { + isa = PBXGroup; + children = ( + 2CD266162CFE38AD00A040A7 /* EcosiaDebugSettings.swift */, + 2CD266172CFE38AD00A040A7 /* EcosiaSettings.swift */, + 2CD266182CFE38AD00A040A7 /* NTPCustomizationSettingsViewController.swift */, + ); + path = Settings; + sourceTree = ""; + }; + 2CD266222CFE3AD900A040A7 /* Bookmarks */ = { + isa = PBXGroup; + children = ( + 2CD266212CFE3AD900A040A7 /* BookmarksExchange.swift */, + ); + path = Bookmarks; + sourceTree = ""; + }; + 2CD266242CFE3E8500A040A7 /* Experiments */ = { + isa = PBXGroup; + children = ( + 2CD266252CFE3E8D00A040A7 /* Unleash */, + ); + path = Experiments; + sourceTree = ""; + }; + 2CD266252CFE3E8D00A040A7 /* Unleash */ = { + isa = PBXGroup; + children = ( + 2CD2662A2CFE402000A040A7 /* SeedCounterNTPExperiment.swift */, + ); + path = Unleash; + sourceTree = ""; + }; + 2CD266302CFE403800A040A7 /* Unleash */ = { + isa = PBXGroup; + children = ( + 2CD2662D2CFE403800A040A7 /* BrazeIntegrationExperiment.swift */, + 2CD2662E2CFE403800A040A7 /* NewsletterCardExperiment.swift */, + ); + path = Unleash; + sourceTree = ""; + }; + 2CD266312CFE403800A040A7 /* Experiments */ = { + isa = PBXGroup; + children = ( + 2CD266302CFE403800A040A7 /* Unleash */, + ); + path = Experiments; + sourceTree = ""; + }; + 2CD2663A2CFE421C00A040A7 /* Extensions */ = { + isa = PBXGroup; + children = ( + 2CD2663D2CFF4ED000A040A7 /* DeviceInfo+Ecosia.swift */, + 2CD2663B2CFE423D00A040A7 /* AppInfo+Ecosia.swift */, + 2CAF5AE22D00AAEE00D3DCDD /* Bundle+Ecosia.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 2CD266502CFF56CB00A040A7 /* Toolbar+URLBar */ = { + isa = PBXGroup; + children = ( + 2CD2664E2CFF56CB00A040A7 /* ConnectionStatusImage.swift */, + 2CD2664F2CFF56CB00A040A7 /* ConnectionStatusImage+WebsiteConnectionTypeStatus.swift */, + ); + path = "Toolbar+URLBar"; + sourceTree = ""; + }; + 2CD266542CFF56EA00A040A7 /* Network */ = { + isa = PBXGroup; + children = ( + 2CD266532CFF56EA00A040A7 /* WebsiteConnectionStatus.swift */, + ); + path = Network; + sourceTree = ""; + }; + 2F44FC551A9E83E200FD20CC /* Settings */ = { + isa = PBXGroup; + children = ( + 216A0D772A40E83F008077BA /* ThemeSettings */, + 8A3EF7ED2A2FCEC900796E3A /* Main */, + EBB895322193FFF400EB91A0 /* ContentBlockerSettingViewController.swift */, + 7B4980A71CE363ED0017547C /* Settings.xcassets */, + 8D8251721F4DE67E00780643 /* AdvancedAccountSettingViewController.swift */, + 0B62EFD11AD63CD100ACB9CD /* Clearables.swift */, + D3E8EEE71B97A87A001900FB /* ClearPrivateDataTableViewController.swift */, + 3B39EDCA1E16E1AA00EF029F /* CustomSearchViewController.swift */, + BCFF93F32AAF9879005B5B71 /* FirefoxSuggestSettingsViewController.swift */, + DFACBF83277B9B36003D5F41 /* HomepageSettings */, + C8656D76270F858900E199EA /* TabsSettingsViewControler.swift */, + C82F4C2A29AE2DF0005BD116 /* NotificationsSettingsViewController.swift */, + D81E45121F82C56C004EFFBA /* NewTabContentSettingsViewController.swift */, + 2F44FCCA1A9E972E00FD20CC /* SearchEnginePicker.swift */, + 2F44FCC61A9E8CF500FD20CC /* SearchSettingsTableViewController.swift */, + 74E36D771B71323500D69DA1 /* SettingsContentViewController.swift */, + D04CD717215EBD85004FF5B0 /* SettingsLoadingView.swift */, + 2F44FC711A9E840300FD20CC /* SettingsNavigationController.swift */, + 2F44FCC41A9E85E900FD20CC /* SettingsTableViewController.swift */, + F85C7F0D2711C555004BDBA4 /* SettingsViewController.swift */, + D821E9052141B71C00452C55 /* SiriSettingsViewController.swift */, + CEFA977D1FAA6B490016F365 /* SyncContentSettingsViewController.swift */, + 8AE1E1CE27B191160024C45E /* SearchBar */, + C22753422A3CA25100B9C0D1 /* WebsiteDataManagement */, + 211046CC2A7D842A00A7309F /* TPAccessoryInfo.swift */, + ); + path = Settings; + sourceTree = ""; + }; + 2FA435FC1ABB83B4008031D1 /* Account */ = { + isa = PBXGroup; + children = ( + 2F14E1391ABB890800FF98DB /* Account-Bridging-Header.h */, + 3905B4D41E8E7A6B0027D953 /* FxAPushMessageHandler.swift */, + 2FA435FD1ABB83B4008031D1 /* Supporting Files */, + ); + path = Account; + sourceTree = ""; + }; + 2FA435FD1ABB83B4008031D1 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 2FA435FE1ABB83B4008031D1 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 2FA4360B1ABB83B4008031D1 /* AccountTests */ = { + isa = PBXGroup; + children = ( + 39B0646D1E7ADA4B000BE173 /* Push */, + 2FA4360C1ABB83B4008031D1 /* Supporting Files */, + ); + path = AccountTests; + sourceTree = ""; + }; + 2FA4360C1ABB83B4008031D1 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 2FA4360D1ABB83B4008031D1 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 2FCAE21B1ABB51F800877008 /* Storage */ = { + isa = PBXGroup; + children = ( + 45D5EDCB292D839700311934 /* PinnedSites.swift */, + 45CC573D28AD8B64006D55AA /* Generated */, + 45CC573828AD8881006D55AA /* metrics.yaml */, + 74B195431CF503FC007F36EF /* RecentlyClosedTabs.swift */, + 2FCAE33D1ABB5F1800877008 /* Storage-Bridging-Header.h */, + D37DE2821CA2047500A5EC69 /* CertStore.swift */, + 28C4AB711AD42D4300D9ACE3 /* Clients.swift */, + 2FCAE2411ABB531100877008 /* Cursor.swift */, + 28302E3F1AF0747800521E2E /* DatabaseError.swift */, + 394CF6CE1BAA493C00906917 /* DefaultSuggestedSites.swift */, + D3BF8CBA1B7425570007AFE6 /* DiskImageStore.swift */, + E65075C11E37F956006961AC /* ExtensionUtils.swift */, + 2FCAE2431ABB531100877008 /* FileAccessor.swift */, + E677F0531D94247300ECF1FB /* Metadata.swift */, + 285D3B671B4380B70035FD22 /* Queue.swift */, + E6FF6AC91D873CFF0070C294 /* PageMetadata.swift */, + D076971E206AC60900FACFD8 /* ReadingList.swift */, + 2FCAE2471ABB531100877008 /* RemoteTabs.swift */, + 2829D39F1C2F0AD400DCF931 /* Sharing.swift */, + 2FCAE2481ABB531100877008 /* Site.swift */, + 0B54BD181B698B7C004C822C /* SuggestedSites.swift */, + 2FCAE25C1ABB531100877008 /* Visit.swift */, + D057B2AC22022617000614E0 /* Rust */, + 2FCAE2491ABB531100877008 /* SQL */, + 2FCAE25A1ABB531100877008 /* ThirdParty */, + BD6CC84129CDDA3400546A5D /* ZoomLevelStore.swift */, + ); + path = Storage; + sourceTree = ""; + }; + 2FCAE22A1ABB51F800877008 /* StorageTests */ = { + isa = PBXGroup; + children = ( + F8AAC1B5296637B7000BCDEC /* RustAutofillTests.swift */, 45D5EDCF292D854000311934 /* TestSQLitePinnedSites.swift */, D37DE2C81CA356F900A5EC69 /* testcert1.pem */, D37DE2C91CA356F900A5EC69 /* testcert2.pem */, @@ -11979,7 +12704,6 @@ children = ( 7B3632E71C29879300D12AF9 /* Snapshot */, D4AFA84B2AFA4FC6000BFEAA /* firefox-ios-tests */, - 2CE2945C2B7FC51E006C22B2 /* EcosiaTests */, 962C6C39297054A700354BE8 /* Package.swift */, 962C6C3F2971F5D400354BE8 /* Dangerfile.swift */, 962C6C3B297054EC00354BE8 /* Sources */, @@ -11991,6 +12715,8 @@ F8708D1E1A0970990051AB07 /* Extensions */, 047F9B2A24E1FE1C00CD7DF7 /* WidgetKit */, F8324A082649A188007E4BFA /* CredentialProvider */, + 1229356B2CE78D0A00EC1297 /* Ecosia */, + 122935792CE78D0A00EC1297 /* EcosiaTests */, 7B604FC11C496005006EEEC3 /* Frameworks */, F84B21BF1A090F8100AAB793 /* Products */, D34DC84C1A16C40C00D49B7B /* Providers */, @@ -12024,6 +12750,8 @@ 047F9B2724E1FE1C00CD7DF7 /* WidgetKitExtension.appex */, 43BE578A278BA4D900491291 /* RustMozillaAppServices.framework */, 2C6C90822C614A16007D9B43 /* EcosiaSnapshotTests.xctest */, + 1229356A2CE78D0A00EC1297 /* Ecosia.framework */, + 122935732CE78D0A00EC1297 /* EcosiaTests.xctest */, ); name = Products; sourceTree = ""; @@ -12031,9 +12759,9 @@ F84B21C01A090F8100AAB793 /* Client */ = { isa = PBXGroup; children = ( + 2CD265162CFDCB8A00A040A7 /* Ecosia */, 2CABD71A2C11E07300A0750F /* PersistedGenerated */, 2CBCAAFA2B88EEE40080AD68 /* Configuration */, - 2C61887E2B7A8A21006B70D7 /* Ecosia */, 8AD984F12AF1554F00B9FDA4 /* ContentBlocker */, 216A0D6F2A40D0E4008077BA /* Redux */, 8A93F85C29D36D9F004159D9 /* Coordinators */, @@ -12330,6 +13058,17 @@ }; /* End PBXGroup section */ +/* Begin PBXHeadersBuildPhase section */ + 122935652CE78D0A00EC1297 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1229357C2CE78D0A00EC1297 /* Ecosia.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + /* Begin PBXNativeTarget section */ 047F9B2624E1FE1C00CD7DF7 /* WidgetKitExtension */ = { isa = PBXNativeTarget; @@ -12353,43 +13092,88 @@ 5A70EF0F295DFD4900790249 /* Common */, 5A5AB97F296CA03500485E06 /* SiteImageView */, 5A68F0AA2AF2E5E00089AC62 /* TabDataStore */, - 2CE294482B7CDD78006C22B2 /* Core */, ); productName = WidgetKitExtension; productReference = 047F9B2724E1FE1C00CD7DF7 /* WidgetKitExtension.appex */; productType = "com.apple.product-type.app-extension"; }; - 2827315D1ABC9BE600AA1954 /* Sync */ = { + 122935692CE78D0A00EC1297 /* Ecosia */ = { isa = PBXNativeTarget; - buildConfigurationList = 282731971ABC9BE800AA1954 /* Build configuration list for PBXNativeTarget "Sync" */; + buildConfigurationList = 122935812CE78D0A00EC1297 /* Build configuration list for PBXNativeTarget "Ecosia" */; buildPhases = ( - 282731591ABC9BE600AA1954 /* Sources */, - 2827315A1ABC9BE600AA1954 /* Frameworks */, - 2827315C1ABC9BE600AA1954 /* Resources */, - 8AFD6277282C51D300451BC8 /* Embed Frameworks */, + 122935652CE78D0A00EC1297 /* Headers */, + 122935662CE78D0A00EC1297 /* Sources */, + 122935672CE78D0A00EC1297 /* Frameworks */, + 122935682CE78D0A00EC1297 /* Resources */, ); buildRules = ( ); dependencies = ( - 2F77F6B91ABCAF0700484F3A /* PBXTargetDependency */, - 2F11EE501ABCAE910083902D /* PBXTargetDependency */, ); - name = Sync; + name = Ecosia; packageProductDependencies = ( - 5A87148B292EA1640039A5BD /* Fuzi */, - 5A70EF11295DFD6400790249 /* Common */, - 2C1298AB2BF5EE3E005AE4E4 /* Core */, - ); - productName = Sync; - productReference = 2827315E1ABC9BE600AA1954 /* Sync.framework */; + 122935902CE78ED500EC1297 /* BrazeKit */, + 122935922CE78ED500EC1297 /* BrazeUI */, + 122935B42CE79EF900EC1297 /* SnowplowTracker */, + 2CAF5C1C2D01B40B00D3DCDD /* SwiftSoup */, + 2CAF5C1E2D01B44400D3DCDD /* Sentry */, + 2CAF5C202D01B5F300D3DCDD /* Common */, + ); + productName = Ecosia; + productReference = 1229356A2CE78D0A00EC1297 /* Ecosia.framework */; productType = "com.apple.product-type.framework"; }; - 282731671ABC9BE700AA1954 /* SyncTests */ = { + 122935722CE78D0A00EC1297 /* EcosiaTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 282731981ABC9BE800AA1954 /* Build configuration list for PBXNativeTarget "SyncTests" */; + buildConfigurationList = 122935872CE78D0A00EC1297 /* Build configuration list for PBXNativeTarget "EcosiaTests" */; buildPhases = ( - 282731641ABC9BE700AA1954 /* Sources */, - 282731651ABC9BE700AA1954 /* Frameworks */, + 1229356F2CE78D0A00EC1297 /* Sources */, + 122935702CE78D0A00EC1297 /* Frameworks */, + 122935712CE78D0A00EC1297 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2C21012D2D0B0FAF00CBE7EC /* PBXTargetDependency */, + ); + name = EcosiaTests; + packageProductDependencies = ( + 2C21012A2D0B0F3700CBE7EC /* SnowplowTracker */, + ); + productName = EcosiaTests; + productReference = 122935732CE78D0A00EC1297 /* EcosiaTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 2827315D1ABC9BE600AA1954 /* Sync */ = { + isa = PBXNativeTarget; + buildConfigurationList = 282731971ABC9BE800AA1954 /* Build configuration list for PBXNativeTarget "Sync" */; + buildPhases = ( + 282731591ABC9BE600AA1954 /* Sources */, + 2827315A1ABC9BE600AA1954 /* Frameworks */, + 2827315C1ABC9BE600AA1954 /* Resources */, + 8AFD6277282C51D300451BC8 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 2F77F6B91ABCAF0700484F3A /* PBXTargetDependency */, + 2F11EE501ABCAE910083902D /* PBXTargetDependency */, + ); + name = Sync; + packageProductDependencies = ( + 5A87148B292EA1640039A5BD /* Fuzi */, + 5A70EF11295DFD6400790249 /* Common */, + ); + productName = Sync; + productReference = 2827315E1ABC9BE600AA1954 /* Sync.framework */; + productType = "com.apple.product-type.framework"; + }; + 282731671ABC9BE700AA1954 /* SyncTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 282731981ABC9BE800AA1954 /* Build configuration list for PBXNativeTarget "SyncTests" */; + buildPhases = ( + 282731641ABC9BE700AA1954 /* Sources */, + 282731651ABC9BE700AA1954 /* Frameworks */, ); buildRules = ( ); @@ -12412,18 +13196,18 @@ 288A2D811AB8B3260023ABC3 /* Sources */, 288A2D821AB8B3260023ABC3 /* Frameworks */, 288A2D841AB8B3260023ABC3 /* Resources */, - 4368F838279665E00013419B /* Embed Frameworks */, + 2CAF5C1A2D01B2E200D3DCDD /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( + 2CAF5C192D01B2E200D3DCDD /* PBXTargetDependency */, ); name = Shared; packageProductDependencies = ( 5A9FF8482942454600DF9FBB /* Common */, 8A8881592B20FFE0009635AE /* GCDWebServers */, 8A88815B2B2103AD009635AE /* WebEngine */, - 2CE2E24C2B9B1FCB00973C16 /* Core */, ); productName = Shared; productReference = 288A2D861AB8B3260023ABC3 /* Shared.framework */; @@ -12446,7 +13230,6 @@ packageProductDependencies = ( 2C6C908E2C614A6C007D9B43 /* SnapshotTesting */, 2C69DA7A2C6225C400D7F69F /* Common */, - 2C69DA802C62459200D7F69F /* Core */, ); productName = EcosiaSnapshotTests; productReference = 2C6C90822C614A16007D9B43 /* EcosiaSnapshotTests.xctest */; @@ -12652,6 +13435,7 @@ 288A2D9C1AB8B3260023ABC3 /* PBXTargetDependency */, F84B22521A0920C600AAB793 /* PBXTargetDependency */, 047F9B3124E1FE1F00CD7DF7 /* PBXTargetDependency */, + 1229357E2CE78D0A00EC1297 /* PBXTargetDependency */, ); name = Client; packageProductDependencies = ( @@ -12668,7 +13452,6 @@ 216A0D752A40E7AB008077BA /* Redux */, 8AF2D0FB2A5F272A00C7DD69 /* ComponentLibrary */, 2C6189F02B7B7D5D006B70D7 /* SnowplowTracker */, - 2CE294462B7CDD56006C22B2 /* Core */, 126509842CD925B40011BA36 /* BrazeKit */, 126509862CD925B40011BA36 /* BrazeUI */, ); @@ -12693,7 +13476,6 @@ name = ClientTests; packageProductDependencies = ( 8A8BAE132B21110000D774EB /* GCDWebServers */, - 2C1F23BC2B9F405E00186F55 /* Core */, ); productName = ClientTests; productReference = F84B21D31A090F8100AAB793 /* ClientTests.xctest */; @@ -12720,7 +13502,6 @@ 5A87148D292EA3270039A5BD /* Fuzi */, 5A8FD0ED293A7D6D00333AA7 /* SnapKit */, 5A70EF13295DFD7C00790249 /* Common */, - 2CF4DA622BB31970001C340A /* Core */, ); productName = ShareToFirefox; productReference = F84B22491A0920C600AAB793 /* ShareTo.appex */; @@ -12741,6 +13522,13 @@ 047F9B2624E1FE1C00CD7DF7 = { CreatedOnToolsVersion = 12.0; }; + 122935692CE78D0A00EC1297 = { + CreatedOnToolsVersion = 15.4; + }; + 122935722CE78D0A00EC1297 = { + CreatedOnToolsVersion = 15.4; + TestTargetID = F84B21BD1A090F8100AAB793; + }; 2827315D1ABC9BE600AA1954 = { CreatedOnToolsVersion = 6.2; LastSwiftMigration = 1000; @@ -12972,11 +13760,11 @@ 43AFC0E027967BFA0039DDF4 /* XCRemoteSwiftPackageReference "Fuzi" */, 43C6A47D27A0679300C79856 /* XCRemoteSwiftPackageReference "MappaMundi" */, 5A37861729A2C337006B3A34 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, - 2C61887D2B7A89E4006B70D7 /* XCRemoteSwiftPackageReference "ios-core" */, 2C6189EF2B7B7D48006B70D7 /* XCRemoteSwiftPackageReference "snowplow-ios-tracker" */, 2CCFB3D82C0FC4DC00BEDCA0 /* XCRemoteSwiftPackageReference "rust-components-swift" */, 2CB172802C612D68008551E2 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, 126509832CD925B30011BA36 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */, + 2CAF5C1B2D01B3E200D3DCDD /* XCRemoteSwiftPackageReference "SwiftSoup" */, ); productRefGroup = F84B21BF1A090F8100AAB793 /* Products */; projectDirPath = ""; @@ -12999,6 +13787,8 @@ 288A2D851AB8B3260023ABC3 /* Shared */, 2827315D1ABC9BE600AA1954 /* Sync */, 2C6C90812C614A16007D9B43 /* EcosiaSnapshotTests */, + 122935692CE78D0A00EC1297 /* Ecosia */, + 122935722CE78D0A00EC1297 /* EcosiaTests */, ); }; /* End PBXProject section */ @@ -13015,6 +13805,42 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 122935682CE78D0A00EC1297 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2CD264972CFDC76A00A040A7 /* Plurals.stringsdict in Resources */, + 2CD264982CFDC76A00A040A7 /* Plurals.stringsdict in Resources */, + 2CD264992CFDC76A00A040A7 /* Plurals.stringsdict in Resources */, + 2CD264922CFDC76A00A040A7 /* Ecosia.strings in Resources */, + 2CD2648F2CFDC76A00A040A7 /* Ecosia.strings in Resources */, + 2CD264952CFDC76A00A040A7 /* Plurals.stringsdict in Resources */, + 2CD264A92CFDC76A00A040A7 /* markets.json in Resources */, + 2CD264902CFDC76A00A040A7 /* Ecosia.strings in Resources */, + 2CD264912CFDC76A00A040A7 /* Ecosia.strings in Resources */, + 2CD264932CFDC76A00A040A7 /* Ecosia.strings in Resources */, + 2CD264942CFDC76A00A040A7 /* Plurals.stringsdict in Resources */, + 2CD2648E2CFDC76A00A040A7 /* Ecosia.strings in Resources */, + 2CD264962CFDC76A00A040A7 /* Plurals.stringsdict in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 122935712CE78D0A00EC1297 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2CAF5BE02D00D1AB00D3DCDD /* import_input_bookmark_firefox.html in Resources */, + 2CAF5BE62D00D1AB00D3DCDD /* referrals.json in Resources */, + 2CAF5BDF2D00D1AB00D3DCDD /* import_input_bookmark_chrome.html in Resources */, + 2CAF5BE22D00D1AB00D3DCDD /* import_output_bookmark_chrome.txt in Resources */, + 2CAF5BE12D00D1AB00D3DCDD /* import_input_bookmark_safari.html in Resources */, + 2CAF5BE32D00D1AB00D3DCDD /* import_output_bookmark_firefox.txt in Resources */, + 2CAF5BE42D00D1AB00D3DCDD /* import_output_bookmark_safari.txt in Resources */, + 2CAF5BE52D00D1AB00D3DCDD /* notifications.json in Resources */, + 2CAF5BDE2D00D1AB00D3DCDD /* export_bookmark_ecosia.html in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 2827315C1ABC9BE600AA1954 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -13090,8 +13916,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2C19DACF2C74C7BF00D2641C /* snapshot_configuration.json in Resources */, - 2C4D16602C76360800E89C95 /* environment.json in Resources */, + 2CD7E60B2D09E9D60003B02B /* snapshot_configuration.json in Resources */, + 2CD7E60A2D09E8B00003B02B /* environment.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -13109,10 +13935,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2C6189632B7A8A22006B70D7 /* Colours.xcassets in Resources */, 8AA1A8842AF2A0C90026B0E0 /* disconnect-block-social.json in Resources */, - 2C61897A2B7A8A22006B70D7 /* Plurals.stringsdict in Resources */, - 2C6189792B7A8A22006B70D7 /* Ecosia.strings in Resources */, 8AA1A8852AF2A0C90026B0E0 /* disconnect-block-cookies-social.json in Resources */, 8AA1A8862AF2A0C90026B0E0 /* disconnect-block-fingerprinting.json in Resources */, 8AA1A8872AF2A0C90026B0E0 /* disconnect-block-cryptomining.json in Resources */, @@ -13129,22 +13952,22 @@ 8AEE62CB2756BA34003207D1 /* DownloadHelper.js in Resources */, 8AEE62CA2756BA34003207D1 /* TrackingProtectionStats.js in Resources */, 0BA1E0301B051A07007675AF /* NetError.css in Resources */, - 2C6189992B7A8A22006B70D7 /* markets.json in Resources */, 3BC659491E5BA4AE006D560F /* TopSites in Resources */, F84B220B1A0910F600AAB793 /* Images.xcassets in Resources */, F35B8D2B1D6380EA008E3D61 /* SessionRestore.html in Resources */, 7B42406E1CA04CAC009B5C28 /* Menu.xcassets in Resources */, 4336FAD2264B169000A6B076 /* WebcompatAllFramesAtDocumentStart.js in Resources */, + 2CAF5AE72D00B1D300D3DCDD /* EcosiaLaunchScreen.xib in Resources */, 23BEA767251A99ED00A014BF /* NewYorkMedium-Bold.otf in Resources */, 3BC659591E5BA505006D560F /* bundle_sites.json in Resources */, E4CD9F541A71506400318571 /* Reader.html in Resources */, E1AF3563286DE5F800960045 /* FullFunctionalTestPlan.xctestplan in Resources */, - 2C6189322B7A8A22006B70D7 /* EcosiaLaunchScreen.xib in Resources */, E1AF3567286DE5F800960045 /* PerformanceTestPlan.xctestplan in Resources */, 23BEA76A251A99ED00A014BF /* NewYorkMedium-RegularItalic.otf in Resources */, 7B2142FE1E5E055000CDD3FC /* InfoPlist.strings in Resources */, E1AF27442A17BCF700CE5991 /* engagementNotificationWithoutConditions.json in Resources */, E69922171B94E3EF007C480D /* Licenses.html in Resources */, + 2CD266042CFE382C00A040A7 /* Colours.xcassets in Resources */, E1AF3560286DE5F800960045 /* Smoketest3.xctestplan in Resources */, E4CD9F5B1A71506C00318571 /* Reader.css in Resources */, D0FCF8061FE4772D004A7995 /* AllFramesAtDocumentEnd.js in Resources */, @@ -13162,12 +13985,12 @@ FA9294011D6584A200AC8D33 /* QRCode.xcassets in Resources */, D308EE561CBF0BF5006843F2 /* CertError.css in Resources */, E1AF3566286DE5F800960045 /* SmokeXCUITests.xctestplan in Resources */, + 2CD266052CFE382C00A040A7 /* Ecosia.xcassets in Resources */, E1AF3561286DE5F800960045 /* Smoketest4.xctestplan in Resources */, 0BA1E00E1B03FB0B007675AF /* NetError.html in Resources */, 23BEA768251A99ED00A014BF /* NewYorkMedium-BoldItalic.otf in Resources */, E4A961381AC06FA50069AD6F /* ReaderViewLoading.html in Resources */, E1AF3565286DE5F800960045 /* UnitTest.xctestplan in Resources */, - 2C61893A2B7A8A22006B70D7 /* Ecosia.xcassets in Resources */, E1AF3564286DE5F800960045 /* SyncIntegrationTestPlan.xctestplan in Resources */, 8AEE62C92756BA34003207D1 /* LoginsHelper.js in Resources */, D0E17FB6201F847600F1FCB5 /* FxASignIn.js in Resources */, @@ -13329,18 +14152,20 @@ buildActionMask = 2147483647; files = ( 2C6189E42B7B7946006B70D7 /* LegacyDarkTheme.swift in Sources */, - 2C6189DD2B7B7776006B70D7 /* AppInfo+Ecosia.swift in Sources */, + 2CD266442CFF4FC400A040A7 /* SemanticColor.swift in Sources */, 047F9B3E24E1FF4000CD7DF7 /* SearchQuickLinks.swift in Sources */, 43118CF3251A9CCA00F24376 /* LegacyTabDataRetriever.swift in Sources */, DA9FD88624E213CD00168D1E /* Helpers.swift in Sources */, 1DF426CF251BDF6A0086386A /* photon-colors.swift in Sources */, 4392FB5C252EC51E00AD3D23 /* PrivilegedRequest.swift in Sources */, 1D06AE6A24FEE8D6000B092B /* TabProvider.swift in Sources */, - 2C6189E02B7B7916006B70D7 /* EcosiaThemeColourPalette.swift in Sources */, 8A03309828C2691800286539 /* LegacyTabFileManager.swift in Sources */, DA9FD88424E213B500168D1E /* SmallQuickLink.swift in Sources */, + 2CD266402CFF4FA300A040A7 /* EcosiaTheme.swift in Sources */, 435222C225882E3800FCA5B6 /* WidgetKitTopSiteModel.swift in Sources */, 1D06AE6624FEE4D5000B092B /* TopSitesProvider.swift in Sources */, + 2CD266462CFF4FD200A040A7 /* EcosiaThemeManager.swift in Sources */, + 2CD266422CFF4FB200A040A7 /* LegacyThemeManager+Ecosia.swift in Sources */, 215B458027D7FD7D00E5E800 /* LegacyTabGroupData.swift in Sources */, 2C6189E22B7B7929006B70D7 /* LegacyThemeManager.swift in Sources */, 2C6189EB2B7B7AB4006B70D7 /* DispatchQueueHelper.swift in Sources */, @@ -13348,8 +14173,6 @@ E18BAB0128E4AEF300098AE2 /* ImageIdentifiers.swift in Sources */, 4392FB48252EC50400AD3D23 /* InternalSchemeHandler.swift in Sources */, 435E34B3254A6A6000406D92 /* TimeConstants.swift in Sources */, - 2C6189E92B7B7979006B70D7 /* EcosiaTheme.swift in Sources */, - 2C6189E72B7B7952006B70D7 /* SemanticColor.swift in Sources */, 43E69ED7254D081F00B591C2 /* SimpleTab.swift in Sources */, 1DDAD13E24F0651C007623C8 /* TopSitesWidget.swift in Sources */, 4392FB20252EC49D00AD3D23 /* LegacySessionData.swift in Sources */, @@ -13357,15 +14180,205 @@ 047F9B4224E1FF4000CD7DF7 /* ImageButtonWithLabel.swift in Sources */, 2C6189DE2B7B78ED006B70D7 /* LegacyTheme.swift in Sources */, 047F9B2C24E1FE1C00CD7DF7 /* WidgetKit.swift in Sources */, - 2C3BD5EB2BF6193B00E25B0D /* EcosiaThemeManager.swift in Sources */, 1DA3CE6724EEE86C00422BB2 /* AppInfo.swift in Sources */, - 2C26FAAA2C8752D20055760A /* LegacyThemeManager+Ecosia.swift in Sources */, 4392FB34252EC4CE00AD3D23 /* SessionRestoreHandler.swift in Sources */, + 2CD266482CFF4FE800A040A7 /* EcosiaThemeColourPalette.swift in Sources */, DA9FD88824E213DD00168D1E /* QuickLink.swift in Sources */, 1DA3CE5D24EEE73100422BB2 /* OpenTabsWidget.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 122935662CE78D0A00EC1297 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2CAF5B8C2D00D16C00D3DCDD /* TreesProjection.swift in Sources */, + 2CD264FF2CFDC76A00A040A7 /* FakeNimbus.swift in Sources */, + 2CAF5B882D00D16C00D3DCDD /* URL+Extensions.swift in Sources */, + 2CAF5B902D00D16C00D3DCDD /* Favourites.swift in Sources */, + 2CAF5B822D00D16C00D3DCDD /* Date+TimestampProvider.swift in Sources */, + 2CAF5B8A2D00D16C00D3DCDD /* InvestmentsProjection.swift in Sources */, + 2CAF5B762D00D16C00D3DCDD /* Unleash.Model.swift in Sources */, + 2CAF5B852D00D16C00D3DCDD /* URLRequest+Extensions.swift in Sources */, + 2CAF5B752D00D16C00D3DCDD /* TimeBasedRefreshingRule.swift in Sources */, + 2CAF5B842D00D16C00D3DCDD /* PageStore.swift in Sources */, + 2CAF5B812D00D16C00D3DCDD /* ObjectPersister.swift in Sources */, + 2CAF5B832D00D16C00D3DCDD /* Page.swift in Sources */, + 2CAF5B792D00D16C00D3DCDD /* UnleashFeatureManagementSessionInitializer.swift in Sources */, + 2CAF5B982D00D16C00D3DCDD /* Bundle.swift in Sources */, + 2CD264AD2CFDC76A00A040A7 /* MMP.swift in Sources */, + 2CAF5B7E2D00D16C00D3DCDD /* Tabs.swift in Sources */, + 2CD264AC2CFDC76A00A040A7 /* FeatureManagement.swift in Sources */, + 2CAF5B9B2D00D16C00D3DCDD /* Locale+Extensions.swift in Sources */, + 2CAF5B802D00D16C00D3DCDD /* NewsModel.swift in Sources */, + 2CAF5B922D00D16C00D3DCDD /* AdultFilter.swift in Sources */, + 2CAF5B532D00D16C00D3DCDD /* HTTPClient.swift in Sources */, + 2CAF5B892D00D16C00D3DCDD /* FinancialReports.swift in Sources */, + 2CD264A02CFDC76A00A040A7 /* Version+Extensions.swift in Sources */, + 2CAF5B5E2D00D16C00D3DCDD /* Publisher.swift in Sources */, + 2CAF5B702D00D16C00D3DCDD /* TimeInterval+Extensions.swift in Sources */, + 2CAF5B592D00D16C00D3DCDD /* ReferralCreateCodeRequest.swift in Sources */, + 2CAF5B862D00D16C00D3DCDD /* Encodable+Dictionary.swift in Sources */, + 2CD2663E2CFF4ED000A040A7 /* DeviceInfo+Ecosia.swift in Sources */, + 2CAF5B5A2D00D16C00D3DCDD /* ReferralRefreshCodeRequest.swift in Sources */, + 2C21011D2D0B042300CBE7EC /* AnalyticsNotificationSettings.swift in Sources */, + 2CAF5B562D00D16C00D3DCDD /* URLSessionProtocol.swift in Sources */, + 2CAF5B912D00D16C00D3DCDD /* List.swift in Sources */, + 2CAF5B962D00D16C00D3DCDD /* FileManager.swift in Sources */, + 2C2101212D0B04E800CBE7EC /* APNConsent.swift in Sources */, + 2CAF5B4E2D00D16C00D3DCDD /* BookmarkSerializer.swift in Sources */, + 2CAF5B8D2D00D16C00D3DCDD /* EnvironmentFetcher.swift in Sources */, + 2CAF5B7F2D00D16C00D3DCDD /* News.swift in Sources */, + 2CAF5B6A2D00D16C00D3DCDD /* MMPProvider.swift in Sources */, + 2C2101222D0B04E800CBE7EC /* BrazeService.swift in Sources */, + 2CAF5B6B2D00D16C00D3DCDD /* SKAdNetworkProtocol.swift in Sources */, + 2CAF5B652D00D16C00D3DCDD /* SingularSessionInfoSendRequest.swift in Sources */, + 2CAF5B6D2D00D16C00D3DCDD /* Environment.Auth.swift in Sources */, + 2CAF5B722D00D16C00D3DCDD /* AppUpdateRule.swift in Sources */, + 2CAF5B732D00D16C00D3DCDD /* DeviceRegionChangeRule.swift in Sources */, + 2CAF5B742D00D16C00D3DCDD /* RefreshingRule.swift in Sources */, + 2CD264A12CFDC76A00A040A7 /* Analytics.swift in Sources */, + 2CAF5B872D00D16C00D3DCDD /* User.swift in Sources */, + 2CAF5B582D00D16C00D3DCDD /* ReferralClaimRequest.swift in Sources */, + 2CD2649C2CFDC76A00A040A7 /* DefaultAppVersionInfoProvider.swift in Sources */, + 2CAF5B9A2D00D16C00D3DCDD /* CloudFlareKeyProvider.swift in Sources */, + 2CAF5B6C2D00D16C00D3DCDD /* AppDeviceInfo.swift in Sources */, + 2CAF5B6F2D00D16C00D3DCDD /* URLProvider.swift in Sources */, + 2CAF5B682D00D16C00D3DCDD /* SingularEvent.swift in Sources */, + 2CAF5B572D00D16C00D3DCDD /* Cookie.swift in Sources */, + 2CAF5B952D00D16C00D3DCDD /* SearchesCounter.swift in Sources */, + 2CAF5B4F2D00D16C00D3DCDD /* Document+Safari.swift in Sources */, + 2CAF5B542D00D16C00D3DCDD /* HTTPMethod.swift in Sources */, + 2CAF5B942D00D16C00D3DCDD /* UserDefaults+ObjectPersister.swift in Sources */, + 2CD2649A2CFDC76A00A040A7 /* String.swift in Sources */, + 2CAF5B7D2D00D16C00D3DCDD /* Tab.swift in Sources */, + 2CD2663C2CFE423D00A040A7 /* AppInfo+Ecosia.swift in Sources */, + 2CAF5B4D2D00D16C00D3DCDD /* BookmarkParser.swift in Sources */, + 2CAF5B712D00D16C00D3DCDD /* History.swift in Sources */, + 2CAF5B7B2D00D16C00D3DCDD /* UnleashStartRequest.swift in Sources */, + 2CAF5B662D00D16C00D3DCDD /* Singular.swift in Sources */, + 2CAF5B552D00D16C00D3DCDD /* URLSessionHTTPClient.swift in Sources */, + 2CAF5B6E2D00D16C00D3DCDD /* Environment.swift in Sources */, + 2CAF5B522D00D16C00D3DCDD /* Requestable.swift in Sources */, + 2CAF5B992D00D16C00D3DCDD /* RegionLocatable.swift in Sources */, + 2CAF5B632D00D16C00D3DCDD /* SingularReponse.swift in Sources */, + 2CAF5B5D2D00D16C00D3DCDD /* Scheme.swift in Sources */, + 2CAF5B512D00D16C00D3DCDD /* BaseRequest.swift in Sources */, + 2CAF5B772D00D16C00D3DCDD /* Unleash.swift in Sources */, + 2CAF5B622D00D16C00D3DCDD /* SingularEventRequest.swift in Sources */, + 2CAF5B7A2D00D16C00D3DCDD /* UnleashRefreshConfigurator.swift in Sources */, + 2CAF5B5F2D00D16C00D3DCDD /* TimestampProvider.swift in Sources */, + 2CD265012CFDC76A00A040A7 /* FakeTelemetry.swift in Sources */, + 2CAF5B5B2D00D16C00D3DCDD /* Referrals.Model.swift in Sources */, + 2CD2649B2CFDC76A00A040A7 /* AppVersionInfoProvider.swift in Sources */, + 2CD266332CFE403800A040A7 /* BrazeIntegrationExperiment.swift in Sources */, + 2CD2649F2CFDC76A00A040A7 /* Version.swift in Sources */, + 2CD264A32CFDC76A00A040A7 /* Analytics+Configuration.swift in Sources */, + 2CAF5B8F2D00D16C00D3DCDD /* Language.swift in Sources */, + 2CD2649D2CFDC76A00A040A7 /* EcosiaInstallType.swift in Sources */, + 2CAF5B5C2D00D16C00D3DCDD /* Referrals.swift in Sources */, + 2CAF5B642D00D16C00D3DCDD /* SingularService.swift in Sources */, + 2CAF5B602D00D16C00D3DCDD /* SingularConversionValueRequest.swift in Sources */, + 2CAF5B7C2D00D16C00D3DCDD /* FeatureManagementSessionInitializer.swift in Sources */, + 2CAF5B782D00D16C00D3DCDD /* Unleash+RefreshComponent.swift in Sources */, + 2CAF5B502D00D16C00D3DCDD /* String+CssQuery.swift in Sources */, + 2CAF5B612D00D16C00D3DCDD /* SingularConversionValueResponse.swift in Sources */, + 2CAF5B8E2D00D16C00D3DCDD /* Local.swift in Sources */, + 2CAF5B972D00D16C00D3DCDD /* Market.swift in Sources */, + 2CD266342CFE403800A040A7 /* NewsletterCardExperiment.swift in Sources */, + 2CAF5B932D00D16C00D3DCDD /* Images.swift in Sources */, + 2CD265002CFDC76A00A040A7 /* FakeSentry.swift in Sources */, + 2CD2649E2CFDC76A00A040A7 /* EcosiaInstallType+Extensions.swift in Sources */, + 2CAF5B8B2D00D16C00D3DCDD /* Statistics.swift in Sources */, + 2CD264A22CFDC76A00A040A7 /* Analytics.Values.swift in Sources */, + 2CAF5AE32D00AAEE00D3DCDD /* Bundle+Ecosia.swift in Sources */, + 2CAF5B672D00D16C00D3DCDD /* SingularAdNetworkHelper.swift in Sources */, + 2CAF5B4C2D00D16C00D3DCDD /* Bookmark.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1229356F2CE78D0A00EC1297 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2CD7E5F72D09DF5A0003B02B /* MockURLBarView.swift in Sources */, + 2C423E2A2D0203190043D407 /* MockProfile.swift in Sources */, + 2CD263BF2CFDC5EB00A040A7 /* AnalyticsSpyTests.swift in Sources */, + 2CAF5BF12D00D1AB00D3DCDD /* HistoryTests.swift in Sources */, + 2CD7E6012D09E3B30003B02B /* XCTestCaseExtensions.swift in Sources */, + 2CAF5BDA2D00D1AB00D3DCDD /* MockURLSessionProtocol.swift in Sources */, + 2CAF5BE82D00D1AB00D3DCDD /* SingularServiceTests.swift in Sources */, + 2CD263C02CFDC5EB00A040A7 /* AnalyticsTests.swift in Sources */, + 2CD7E5FE2D09E1A80003B02B /* MockTabManager.swift in Sources */, + 2CD7E6002D09E3A50003B02B /* DependencyHelperMock.swift in Sources */, + 2CAF5BF02D00D1AB00D3DCDD /* ReferralsModelTests.swift in Sources */, + 2C2101282D0B0D9B00CBE7EC /* MockWelcomeDelegate.swift in Sources */, + 2CAF5C022D00D1AB00D3DCDD /* PublishersTests.swift in Sources */, + 2CAF5C062D00D1AB00D3DCDD /* FavouritesTests.swift in Sources */, + 2CD7E6022D09E3C20003B02B /* MockThemeManager.swift in Sources */, + 2CAF5BD92D00D1AB00D3DCDD /* MockURLSession.swift in Sources */, + 2CAF5BEB2D00D1AB00D3DCDD /* BookmarkParserTests.swift in Sources */, + 2CAF5BD52D00D1AB00D3DCDD /* User5_3.swift in Sources */, + 2CAF5BE92D00D1AB00D3DCDD /* UpgradeTests.swift in Sources */, + 2CAF5BDC2D00D1AB00D3DCDD /* URLTests.swift in Sources */, + 2CAF5C032D00D1AB00D3DCDD /* UnleashRefreshConfiguratorTests.swift in Sources */, + 2CD7E5FB2D09DFE30003B02B /* String+Extension.swift in Sources */, + 2CD263B32CFDC5EB00A040A7 /* AppDelegateFeatureManagementIntegrationTests.swift in Sources */, + 1229358D2CE78D5D00EC1297 /* BrazeServiceTests.swift in Sources */, + 2CAF5BFE2D00D1AB00D3DCDD /* ReferralsTests.swift in Sources */, + 2CAF5BED2D00D1AB00D3DCDD /* BookmarkSerializerTests.swift in Sources */, + 2CD263B52CFDC5EB00A040A7 /* EcosiaPerformanceTestHistory.swift in Sources */, + 2CAF5C012D00D1AB00D3DCDD /* InvestmentsProjectionTests.swift in Sources */, + 2CAF5BDB2D00D1AB00D3DCDD /* ListTests.swift in Sources */, + 2CD263B72CFDC5EB00A040A7 /* PrivateModeButtonTests.swift in Sources */, + 2CD7E5F82D09DF600003B02B /* MockOverlayModeManager.swift in Sources */, + 2CAF5C042D00D1AB00D3DCDD /* UserTests.swift in Sources */, + 2CAF5C002D00D1AB00D3DCDD /* CookieTests.swift in Sources */, + 2CAF5BEA2D00D1AB00D3DCDD /* UnleashFeatureManagementSessionInitializerTests.swift in Sources */, + 2CAF5BF72D00D1AB00D3DCDD /* TreesProjectionTests.swift in Sources */, + 2CD263BE2CFDC5EB00A040A7 /* EcosiaPageActionMenuCellTests.swift in Sources */, + 2CD263B62CFDC5EB00A040A7 /* MockAppVersionInfoProvider.swift in Sources */, + 2CD263B42CFDC5EB00A040A7 /* EcosiaInstallTypeTests.swift in Sources */, + 2CAF5BF22D00D1AB00D3DCDD /* BookmarkTests.swift in Sources */, + 2CAF5BEE2D00D1AB00D3DCDD /* ImagesTests.swift in Sources */, + 2CD263AF2CFDC5EB00A040A7 /* VersionTests.swift in Sources */, + 2CBF7AF42D13056300454AB4 /* BundleImageFetcherTests.swift in Sources */, + 2CAF5BE72D00D1AB00D3DCDD /* SingularTests.swift in Sources */, + 2CAF5BFB2D00D1AB00D3DCDD /* URLProviderTests.swift in Sources */, + 2C2101272D0B0D9B00CBE7EC /* MockNewsModel.swift in Sources */, + 2CAF5BEC2D00D1AB00D3DCDD /* NewsTests.swift in Sources */, + 2CD263BD2CFDC5EB00A040A7 /* EcosiaOverlayModeManagerTests.swift in Sources */, + 2CAF5BD62D00D1AB00D3DCDD /* BookmarkFixtures.swift in Sources */, + 2CD263BB2CFDC5EB00A040A7 /* UserDefaultsSeedProgressManagerTests.swift in Sources */, + 2CD263B22CFDC5EB00A040A7 /* WhatsNewLocalDataProviderTests.swift in Sources */, + 2C2101292D0B0D9B00CBE7EC /* MockUNNotificationSettings.swift in Sources */, + 2CAF5BFF2D00D1AB00D3DCDD /* FinancialReportsTests.swift in Sources */, + 2CAF5BFA2D00D1AB00D3DCDD /* StagingURLProviderTests.swift in Sources */, + 2CAF5BEF2D00D1AB00D3DCDD /* StatisticsTests.swift in Sources */, + 2CD7E5F92D09DF750003B02B /* ProfileTest.swift in Sources */, + 2CAF5BF62D00D1AB00D3DCDD /* TabsTests.swift in Sources */, + 2CAF5BF82D00D1AB00D3DCDD /* UnleashTests.swift in Sources */, + 2CD7E5FD2D09E19E0003B02B /* MockablePinnedSites.swift in Sources */, + 2CD7E6072D09E47E0003B02B /* Bundle+EcosiaTests.swift in Sources */, + 2CAF5BDD2D00D1AB00D3DCDD /* FeatureFlaggingSessionInitializerTests.swift in Sources */, + 2CAF5BF52D00D1AB00D3DCDD /* SingularAdNetworkHelperTests.swift in Sources */, + 2CD263B82CFDC5EB00A040A7 /* EcosiaNTPTooltipHighlightTests.swift in Sources */, + 2CAF5BF42D00D1AB00D3DCDD /* LocalTests.swift in Sources */, + 2CAF5BD72D00D1AB00D3DCDD /* HTTPClientMock.swift in Sources */, + 2CAF5BFD2D00D1AB00D3DCDD /* SearchesCounterTests.swift in Sources */, + 2CD263B92CFDC5EB00A040A7 /* EcosiaHomeViewModelTests.swift in Sources */, + 2CD263BC2CFDC5EB00A040A7 /* EcosiaTopSitesHelperTests.swift in Sources */, + 2CAF5C052D00D1AB00D3DCDD /* LanguageTests.swift in Sources */, + 2CD263BA2CFDC5EB00A040A7 /* UnleashUserDefaultsSeedProgressManagerTests.swift in Sources */, + 2CAF5BF92D00D1AB00D3DCDD /* ProductionURLProviderTests.swift in Sources */, + 2CD7E5FA2D09DF7E0003B02B /* TopSitesHelperTests.swift in Sources */, + 2CAF5BD82D00D1AB00D3DCDD /* MockTimestampProvider.swift in Sources */, + 2CAF5BFC2D00D1AB00D3DCDD /* UserStateTests.swift in Sources */, + 2CAF5C072D00D1AB00D3DCDD /* URLRequestTests.swift in Sources */, + 2CD7E6052D09E43B0003B02B /* MockTabDataStore.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 282731591ABC9BE600AA1954 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -13450,22 +14463,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2C69DA752C62185A00D7F69F /* Welcome.swift in Sources */, + 2C423E292D02029C0043D407 /* LocaleRetriever.swift in Sources */, + 2C423E272D0202530043D407 /* SnapshotBaseTests.swift in Sources */, + 2C423E282D0202530043D407 /* SnapshotTestHelper.swift in Sources */, + 2CD7E6042D09E3ED0003B02B /* LocalizationOverrideTestingBundle.swift in Sources */, + 2CAF5C272D01C33300D3DCDD /* EcosiaMockThemeManager.swift in Sources */, + 2CAF5C262D01C32C00D3DCDD /* Welcome.swift in Sources */, + 2CAF5C252D01C31900D3DCDD /* NTPTests.swift in Sources */, + 2CAF5C242D01C30B00D3DCDD /* OnboardingTests.swift in Sources */, + 2CAF5C232D01C2F300D3DCDD /* NTPComponentTests.swift in Sources */, + 2CD7E5FC2D09DFEA0003B02B /* String+Extension.swift in Sources */, + 2CD7E6032D09E3E30003B02B /* DeviceType.swift in Sources */, 2C69DA7D2C6244BE00D7F69F /* MockTabDataStore.swift in Sources */, - 2C69DA922C63A92D00D7F69F /* SnapshotBaseTests.swift in Sources */, - 2C6C90902C614A76007D9B43 /* OnboardingTests.swift in Sources */, - 2C69DA942C63B0C000D7F69F /* String+Extension.swift in Sources */, - 2C82625E2C66661700E2A255 /* EcosiaMockThemeManager.swift in Sources */, - 2C69DA772C62243300D7F69F /* NTPTests.swift in Sources */, 2C69DA782C62259D00D7F69F /* MockProfile.swift in Sources */, - 2C5B81C82C75388300B81D95 /* LocaleRetriever.swift in Sources */, - 2C82625A2C64BB9300E2A255 /* DeviceType.swift in Sources */, 2C69DA792C6225AE00D7F69F /* DependencyHelperMock.swift in Sources */, - 2C69DA842C62B44B00D7F69F /* NTPComponentTests.swift in Sources */, - 2C82625C2C6648D900E2A255 /* LocalizationOverrideTestingBundle.swift in Sources */, 2C69DA7C2C6225DB00D7F69F /* MockOverlayModeManager.swift in Sources */, 2C69DA7E2C6244C400D7F69F /* MockThemeManager.swift in Sources */, - 2C69DA722C62175400D7F69F /* SnapshotTestHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -13628,6 +14641,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2CD265EE2CFE382C00A040A7 /* NTPTooltipDelegate.swift in Sources */, 45D5EDD2292D89A200311934 /* PinnedSite.swift in Sources */, 8A4AC0EC28C929D700439F83 /* URLSessionProtocol.swift in Sources */, C2A72A672A76938C002ACCE2 /* DownloadsCoordinator.swift in Sources */, @@ -13635,8 +14649,8 @@ E1CD81C5290C6D5800124B27 /* HelpView.swift in Sources */, 5A47CFF52860FB8900B2B7BF /* AppLaunchUtil.swift in Sources */, D04CD74A216CF86B004FF5B0 /* SiriShortcuts.swift in Sources */, + 2CD265E82CFE382C00A040A7 /* NTPConfigurableNudgeCardCellViewModel.swift in Sources */, EB9A179F20E6C1A200B12184 /* ThemedTableViewController.swift in Sources */, - 2C6189482B7A8A22006B70D7 /* NTPLayout.swift in Sources */, E68E39BE1C46F42000B85F42 /* AppSettingsTableViewController.swift in Sources */, E1A6AB4628CA6A4C00EBEBDD /* String+Extension.swift in Sources */, 1D2F68AF2ACB272500524B92 /* RemoteTabsTableViewController.swift in Sources */, @@ -13646,14 +14660,13 @@ F85C7F0F271DD154004BDBA4 /* AppAuthenticator.swift in Sources */, D029A04920A62DB0001DB72F /* TemporaryDocument.swift in Sources */, C8BA0E7627F20B8E00DD8214 /* HistoryDeletionUtility.swift in Sources */, - 2C61894D2B7A8A22006B70D7 /* NTPLibraryShortcutView.swift in Sources */, 8A93F86D29D3A131004159D9 /* DefaultRouter.swift in Sources */, 5AB4237E28A2BA9C003BC40C /* HistoryHighlightsDataAdaptor.swift in Sources */, 8ADAE4262A33A13B007BF926 /* OpenSupportPageSetting.swift in Sources */, 8A590C6128C123100032F1AA /* OpenPassBookHelper.swift in Sources */, EBB895332193FFF400EB91A0 /* ContentBlockerSettingViewController.swift in Sources */, - 2CCBB5372CAF0E8C006E2E10 /* SeedCounterHiddenSettings.swift in Sources */, 8A5D1CB42A30D7D9005AD35C /* NoImageModeSetting.swift in Sources */, + 2CD266012CFE382C00A040A7 /* WhatsNewItem.swift in Sources */, 8A3EF8152A2FD08800796E3A /* OpenFiftyTabsDebugOption.swift in Sources */, D38F02D11C05127100175932 /* Authenticator.swift in Sources */, CAA3B7E62497DCB60094E3C1 /* LoginDataSource.swift in Sources */, @@ -13665,43 +14678,36 @@ 396E38F11EE0C8EC00CC180F /* FxAPushMessageHandler.swift in Sources */, 8A76B01629F6EB3900A82607 /* ScreenshotService.swift in Sources */, E4CD9F6D1A77DD2800318571 /* ReaderModeStyleViewController.swift in Sources */, - 2C6189622B7A8A22006B70D7 /* NTPAboutEcosiaCell.swift in Sources */, E13E9AB52AAB0FB5001A0E9D /* FakespotViewModel.swift in Sources */, - 2C61894B2B7A8A22006B70D7 /* NTPCustomizationCell.swift in Sources */, 8A5D1CBD2A30DC4E005AD35C /* AccountStatusSetting.swift in Sources */, E1CD81BC290C5C3F00124B27 /* DevicePickerTableViewCell.swift in Sources */, 1DDE3DB72AC3820A0039363B /* TabModel.swift in Sources */, 8A19ACB02A329078001C2147 /* AutofillCreditCardSettings.swift in Sources */, - 2C61895D2B7A8A22006B70D7 /* NTPImpactRowView.swift in Sources */, E1E5BE252A28F7BE00248F77 /* PasswordDetailViewControllerModel.swift in Sources */, - 12147F332CDBA7230009D300 /* NewsletterCardExperiment.swift in Sources */, E1FE133129C22726002A65FF /* BackgroundFetchAndProcessingUtility.swift in Sources */, + 2CD265D52CFE382C00A040A7 /* CustomizableNTPSettingConfig.swift in Sources */, 8AD40FD527BB1C1000672675 /* LockButton.swift in Sources */, - 2C61898F2B7A8A22006B70D7 /* AppInfo+Ecosia.swift in Sources */, 5F130D2E2483508E00B0F7D0 /* FxAWebViewModel.swift in Sources */, 2109478928AFD24C00B73D44 /* OnboardingViewControllerProtocol.swift in Sources */, D0FCF7F51FE45842004A7995 /* UserScriptManager.swift in Sources */, 8AB8573027D94CAD0075C173 /* HomepageViewModelProtocol.swift in Sources */, - 2C6189692B7A8A22006B70D7 /* MarketsController.swift in Sources */, 5A9F83422B2B796800272819 /* TabPeekState.swift in Sources */, 21E77E522AA8BE5C00FABA10 /* TabTrayFlagManager.swift in Sources */, CA8226F324C11DB7008A6F38 /* PasswordManagerTableViewCell.swift in Sources */, 8A0A1BA02B2200FD00E8706F /* PrivateHomepageViewController.swift in Sources */, E4A960061ABB9C450069AD6F /* ReaderModeUtils.swift in Sources */, - 2C61896A2B7A8A22006B70D7 /* EmptyBookmarksView.swift in Sources */, 8AD40FCB27BADC4B00672675 /* StatefulButton.swift in Sources */, C84655E62887398700861B4A /* WallpaperCollection.swift in Sources */, EBF47E701F7979DF00899189 /* TelemetryWrapper.swift in Sources */, - 2C6189502B7A8A22006B70D7 /* NTPTooltip.swift in Sources */, 8AD40FCF27BADC6B00672675 /* URLTextField.swift in Sources */, - 2C6189762B7A8A22006B70D7 /* EcosiaDebugSettings.swift in Sources */, + 2CD265EB2CFE382C00A040A7 /* NTPLayout.swift in Sources */, 31ADB5DA1E58CEC300E87909 /* ClipboardBarDisplayHandler.swift in Sources */, 8AD1980F27BEB3F100D64B0E /* PhotonActionSheetViewModel.swift in Sources */, EB9A179D20E69A7F00B12184 /* LegacyTheme.swift in Sources */, 2128E2802934FBB400FB91BE /* CopyLinkActivity.swift in Sources */, E17BE4C42A94BA6900C5124E /* FakespotHighlightGroupView.swift in Sources */, C825E9832832A425006CB811 /* NimbusSearchBarLayer.swift in Sources */, - 2C6B5B3F2CAAF28000F15323 /* NTPSeedCounterViewModel.swift in Sources */, + 2CD265C72CFE382C00A040A7 /* AboutEcosiaSection.swift in Sources */, E1FC23F12A8629380089E14D /* FakespotReliabilityCardView.swift in Sources */, C8DC90C92A0675E70008832B /* MarkupParsingUtility.swift in Sources */, CA03B26A247F1D9E00382B62 /* BreachAlertsClient.swift in Sources */, @@ -13713,7 +14719,9 @@ DFFC9AD12A681FA0002A6AAD /* NimbusFakespotFeatureLayer.swift in Sources */, 1DEBC55E2AC4ED70006E4801 /* RemoteTabsPanel.swift in Sources */, 435D660523D794B90046EFA2 /* UpdateViewModel.swift in Sources */, + 2CD265FE2CFE382C00A040A7 /* WhatsNewDataProvider.swift in Sources */, 9658143C29FAB610007339BD /* CreditCardInputFieldHelper.swift in Sources */, + 2CD265F42CFE382C00A040A7 /* WelcomeTourGreen.swift in Sources */, 5A9F83442B2B8CE900272819 /* TabPeekModel.swift in Sources */, 810FF3542B178343009F062C /* FeltPrivacyMiddleware.swift in Sources */, 215B457F27D7FD4B00E5E800 /* LegacyTabGroupData.swift in Sources */, @@ -13723,9 +14731,7 @@ D0152245229855A8009DE753 /* OneLineTableViewCell.swift in Sources */, F018F84C2719AE8300B9A52D /* ThemedDefaultNavigationController.swift in Sources */, 602B3D6729B0E1DB0066DEF8 /* ConversionValueUtil.swift in Sources */, - 2C61894C2B7A8A22006B70D7 /* NTPCustomizationCellViewModel.swift in Sources */, 8ADED7EE276A7750009C19E6 /* CumulativeDaysOfUseCounter.swift in Sources */, - 2C6189352B7A8A22006B70D7 /* PageActionMenuCell.swift in Sources */, 4346FF08295BA6A300F4D220 /* CreditCardSettingsViewController.swift in Sources */, E19B38B528A42EBC00D8C541 /* WallpaperCellViewModel.swift in Sources */, C80E1A102A0943640025B9E1 /* UIFont+Extension.swift in Sources */, @@ -13733,48 +14739,47 @@ C23889DF2A4EFCE500429673 /* ShareExtensionCoordinator.swift in Sources */, 8C6F94652A972EB300415FF6 /* FakespotAdjustRatingView.swift in Sources */, 8A3EF7FD2A2FCFAC00796E3A /* AppReviewPromptSetting.swift in Sources */, - 2C6189602B7A8A22006B70D7 /* AboutEcosiaSection.swift in Sources */, D3B6923F1B9F9A58004B87A4 /* FindInPageHelper.swift in Sources */, E1FE133329C22782002A65FF /* BackgroundNotificationSurfaceUtility.swift in Sources */, + 2CD2662B2CFE402000A040A7 /* SeedCounterNTPExperiment.swift in Sources */, 8A395552299AF83400B2AFBB /* UIControl+Extension.swift in Sources */, C2A72A692A769460002ACCE2 /* ReadingListCoordinator.swift in Sources */, F85C7F122721048E004BDBA4 /* Layout.swift in Sources */, DF036E43274FD434002E834E /* HistoryHighlightsCell.swift in Sources */, - 2C6189392B7A8A22006B70D7 /* EcosiaFindInPageBar.swift in Sources */, EB9A179C20E69A7F00B12184 /* LegacyDarkTheme.swift in Sources */, 21618A8A2A4389F700A5189E /* ActiveScreenState.swift in Sources */, + 2CD265D42CFE382C00A040A7 /* Sparkle.swift in Sources */, DA52E1DA25F5961F0092204C /* LegacyTabTrayViewController.swift in Sources */, + 2CD2655A2CFDCF0900A040A7 /* NumberFormatter+Ecosia.swift in Sources */, 8AD40FD327BB068F00672675 /* MainMenuActionHelper.swift in Sources */, EB1C84BF212EFFBF001489DF /* BrowserViewController+ReaderMode.swift in Sources */, 96D95016270238500079D39D /* Throttler.swift in Sources */, 8A93080B27C01AD60052167D /* SingleActionViewModel.swift in Sources */, 219914052AF963F900153598 /* TabTrayAction.swift in Sources */, 5AA0CC662A4B8F6100014E2A /* PasswordManagerCoordinator.swift in Sources */, + 2CD265C92CFE382C00A040A7 /* NTPAboutEcosiaCellViewModel.swift in Sources */, 8A7368AD27924AAF005D7704 /* CanRemoveQuickActionBookmark.swift in Sources */, C2296FCC2A601C190046ECA6 /* IntensityVisualEffectView.swift in Sources */, 8ADC2A102A33758E00543DAA /* FxALaunchParams.swift in Sources */, - 2C6189572B7A8A22006B70D7 /* NTPNewsCell.swift in Sources */, D3C3696E1CC6B78800348A61 /* LocalRequestHelper.swift in Sources */, E17496382991A2720096900A /* AdaptiveStack.swift in Sources */, + 2CD265522CFDCF0900A040A7 /* AppSettingsTableViewController+Ecosia.swift in Sources */, E4B423DD1ABA0318007E66C8 /* ReaderModeHandlers.swift in Sources */, F8A0B08229AD61FA0091C75B /* RustSyncManager.swift in Sources */, D308E4E41A5306F500842685 /* SearchEngines.swift in Sources */, - 2C6189922B7A8A22006B70D7 /* BrowserCoordinator+Ecosia.swift in Sources */, 438FE8642988ABA600155B10 /* CreditCardTableViewController.swift in Sources */, - 2C6189702B7A8A22006B70D7 /* WelcomeTourTransparent.swift in Sources */, 3BCE6D3C1CEB9E4D0080928C /* ThirdPartySearchAlerts.swift in Sources */, - 2C61895A2B7A8A22006B70D7 /* ProgressView.swift in Sources */, 8A93F86229D36F0F004159D9 /* NavigationController.swift in Sources */, + 2CD265E32CFE382C00A040A7 /* NTPNewsCell.swift in Sources */, E13F8C342928194800BDC8B4 /* PhotonActionSheetSiteHeaderView.swift in Sources */, C2D71B9B2A3850B4003DEC7A /* ThemedTableViewCellViewModel.swift in Sources */, C869912F28917688007ACC5C /* WallpaperMetadataLoader.swift in Sources */, - 2C6189372B7A8A22006B70D7 /* PageActionMenu.swift in Sources */, 2137785D297F1F2800D01309 /* DownloadedFile.swift in Sources */, 745DAB301CDAAFAA00D44181 /* RecentlyClosedTabsPanel.swift in Sources */, D0B9483D22A18B78002F4AA1 /* TextFieldTableViewCell.swift in Sources */, 8AF99B4F29EF1BA700108DEC /* BrowserDelegate.swift in Sources */, C87D8B802818333F00A6307D /* NimbusManager.swift in Sources */, - 2C6189582B7A8A22006B70D7 /* NTPLogoCell.swift in Sources */, + 2CD2655E2CFDCF0900A040A7 /* UIFont+Ecosia.swift in Sources */, 8A4AC0EB28C929D700439F83 /* URLSessionDataTaskProtocol.swift in Sources */, C45F44691D087DB600CB7EF0 /* TopTabsViewController.swift in Sources */, 8ADC2A212A3399DC00543DAA /* YourRightsSetting.swift in Sources */, @@ -13786,11 +14791,12 @@ D01017F5219CB6BD009CBB5A /* DownloadContentScript.swift in Sources */, 8A093D832A4B68940099ABA5 /* PrivacySettingsDelegate.swift in Sources */, 39F819C61FD70F5D009E31E4 /* TabEventHandlers.swift in Sources */, + 2CD265CF2CFE382C00A040A7 /* NTPSeedCounterViewModel.swift in Sources */, C8DC90C32A066B4A0008832B /* MarkupToken.swift in Sources */, FA6B2AC21D41F02D00429414 /* String+Punycode.swift in Sources */, E174963C2992B6A60096900A /* HostingTableViewSectionHeader.swift in Sources */, + 2CD265602CFDCF0900A040A7 /* URL+Ecosia.swift in Sources */, 8A471185287F6E4800F5A6EA /* SeparatorTableViewCell.swift in Sources */, - 2C6B5B412CAAF3AA00F15323 /* SeedCounterNTPExperiment.swift in Sources */, D301AAEE1A3A55B70078DD1D /* LegacyGridTabViewController.swift in Sources */, EB9A179B20E69A7F00B12184 /* LegacyThemeManager.swift in Sources */, EBFDB790211C83A5005CCA2F /* BrowserViewController+FindInPage.swift in Sources */, @@ -13803,19 +14809,17 @@ D3BE7B461B054F8600641031 /* UITestAppDelegate.swift in Sources */, C8DC90C72A06759E0008832B /* MarkupAttributionUtility.swift in Sources */, 219A0FD52ACC8506009A6D1A /* InactiveTabsCell.swift in Sources */, - 2C6189422B7A8A22006B70D7 /* WhatsNewCell.swift in Sources */, 23D57E6E25ED6F2700883FAD /* SearchViewController.swift in Sources */, - 2C6189342B7A8A22006B70D7 /* SemanticColor.swift in Sources */, + 2CD265FB2CFE382C00A040A7 /* EcosiaTheme.swift in Sources */, C82A94F2269F68ED00624AA7 /* LegacyFeatureFlagsManager.swift in Sources */, C8610DAA2A0EBF7100B79FF1 /* OnboardingCardDelegate.swift in Sources */, E127313D28B6AD99006F39D2 /* WallpaperSettingsViewModel.swift in Sources */, 8A07910F278F62F2005529CB /* AdjustHelper.swift in Sources */, - 2C6189802B7A8A22006B70D7 /* EcosiaTopSiteItemCell.swift in Sources */, - 2C6189952B7A8A22006B70D7 /* ConnectionStatusImage+WebsiteConnectionTypeStatus.swift in Sources */, + 2CD266232CFE3ADA00A040A7 /* BookmarksExchange.swift in Sources */, + 2CD265E52CFE382C00A040A7 /* NTPNewsletterCardCell.swift in Sources */, E1442FC2294782C3003680B0 /* NSAttributedString+Extension.swift in Sources */, 967A028E28FA026F003C35E3 /* SceneDelegate.swift in Sources */, 810FF3562B1783B0009F062C /* FeltPrivacyManager.swift in Sources */, - 2C6189A32B7A8A22006B70D7 /* AppVersionInfoProvider.swift in Sources */, C8B07A4128199500000AFCE7 /* NimbusFlaggableFeature.swift in Sources */, 0B62EFD21AD63CD100ACB9CD /* Clearables.swift in Sources */, 431C0CA925C890E500395CE4 /* DefaultBrowserOnboardingViewModel.swift in Sources */, @@ -13825,10 +14829,8 @@ C88E7A602A05551B0072E638 /* NimbusOnboardingFeatureLayerProtocol.swift in Sources */, 8CFD56882AAF057D003157A6 /* SwitchFakespotProduction.swift in Sources */, C40046FA1CF8E0B200B08303 /* BackForwardListAnimator.swift in Sources */, + 2CD265DF2CFE382C00A040A7 /* NTPLibraryCell.swift in Sources */, DD31E0FB1B382B520077078A /* TabPrintPageRenderer.swift in Sources */, - 2C6189732B7A8A22006B70D7 /* WelcomeTourProfit.swift in Sources */, - 2C6189412B7A8A22006B70D7 /* EmptyHeader.swift in Sources */, - 2C61896F2B7A8A22006B70D7 /* WelcomeTour.swift in Sources */, D81E45131F82C56D004EFFBA /* NewTabContentSettingsViewController.swift in Sources */, E4CD9E911A6897FB00318571 /* ReaderMode.swift in Sources */, 96A562A327D7B32A0045144A /* Contile.swift in Sources */, @@ -13839,30 +14841,28 @@ E1442FD8294782D9003680B0 /* UIPasteboard+Extension.swift in Sources */, C8A4137428BE58C900D8EFEA /* WallpaperMetadataCodableProtocol.swift in Sources */, 8A93080927BFE88F0052167D /* PhotonActionSheetContainerCell.swift in Sources */, - 2C6189A52B7A8A22006B70D7 /* EcosiaInstallType+Extensions.swift in Sources */, C8B41E0A29F0284B00FE218A /* NimbusOnboardingFeatureLayer.swift in Sources */, BD4B2DE229BB4CD9005FAA50 /* SnackButton.swift in Sources */, F85C7F0E2711C556004BDBA4 /* SettingsViewController.swift in Sources */, - 2C6189362B7A8A22006B70D7 /* PageActionsShortcutsHeader.swift in Sources */, - 2C6B5B3E2CAAF28000F15323 /* NTPSeedCounterCell.swift in Sources */, C8B509E3293FA39900AC013C /* AppVersionUpdateCheckerProtocol.swift in Sources */, E1AFBAF9292EA0330065E35E /* SendToDeviceHelper.swift in Sources */, + 2CD2655D2CFDCF0900A040A7 /* UIButton+Ecosia.swift in Sources */, 21D151262AFC28960062D891 /* TabManagerMiddleware.swift in Sources */, 74C027451B2A348C001B1E88 /* LegacySessionData.swift in Sources */, + 2CD265F82CFE382C00A040A7 /* PageActionMenu.swift in Sources */, C84266752728462900382274 /* AccessibilityIdentifiers.swift in Sources */, C8BD87622A0C257C00CD803A /* OnboardingCardInfoModelProtocol.swift in Sources */, DA27EEDB28BADF4700DD6F5D /* MenuBuilderHelper.swift in Sources */, E16AD22C2A8A7AE800F0AA58 /* FakespotHighlightsCardView.swift in Sources */, B2999FF12B194A5800F0FEC1 /* CreditCardPayload.swift in Sources */, + 2CD265622CFDCF6D00A040A7 /* UIImage+Ecosia.swift in Sources */, E1877A81286E0EFD00F5BDF2 /* WebViewNavigationHandler.swift in Sources */, 274A36CC239EB99400A21587 /* LibraryPanelContextMenu.swift in Sources */, D314E7F71A37B98700426A76 /* TabToolbar.swift in Sources */, 43D00493296FC48F00CB0F31 /* CreditCardSettingsEmptyView.swift in Sources */, - 2C61896D2B7A8A22006B70D7 /* WelcomeTour.Step.swift in Sources */, - 2C6189A82B7A8A22006B70D7 /* Analytics.swift in Sources */, - 2CCBB5252CAEA9DF006E2E10 /* SeedProgressView.swift in Sources */, CEFA977E1FAA6B490016F365 /* SyncContentSettingsViewController.swift in Sources */, C8CD80DC2A1E8C970097C3AE /* OnboardingTelemetryUtility.swift in Sources */, + 2CD2661D2CFE38AD00A040A7 /* EcosiaSettings.swift in Sources */, 96C11E9B2864C2DD00840E7C /* DependencyHelper.swift in Sources */, F18859502A3E454E0004AA7B /* EnhancedTrackingProtectionCoordinator.swift in Sources */, 21618A8C2A438A0900A5189E /* ActiveScreenAction.swift in Sources */, @@ -13871,14 +14871,13 @@ 21E77E4E2AA8BA5200FABA10 /* TabTrayViewController.swift in Sources */, 43BDBBFE2752FA8600254DE4 /* LegacyTabCell.swift in Sources */, E60D03181D511398002FE3F6 /* SyncDisplayState.swift in Sources */, - 2C4ABD492CB58E4F00FF86F9 /* Sparkle.swift in Sources */, C4E3983D1D21F1E7004E89BA /* TopTabCell.swift in Sources */, - 2C61898C2B7A8A22006B70D7 /* HomepageViewController+Ecosia.swift in Sources */, 210E0EBA298D9D6400BB4F33 /* OpenSearchEngine.swift in Sources */, FA9293D41D6580E100AC8D33 /* QRCodeViewController.swift in Sources */, 5A679E4B2B239FAE004F2B0D /* TabPeekViewController.swift in Sources */, 2178A6A0291454B5002EC290 /* ReaderModeThemeButton.swift in Sources */, 39F4C10A2045DB2E00746155 /* FocusHelper.swift in Sources */, + 2CD265E92CFE382C00A040A7 /* CircleButton.swift in Sources */, E4CD9F2D1A6DC91200318571 /* TabLocationView.swift in Sources */, 81CAE4DB2B1A2C220040C78A /* BrowserViewControllerState.swift in Sources */, C8BD87602A0C248000CD803A /* OnboardingButtonsModel.swift in Sources */, @@ -13887,9 +14886,7 @@ 8AB8572C27D945FA0075C173 /* TopSitesDataAdaptor.swift in Sources */, EBC4869F2195F58300CDA48D /* SessionRestoreHandler.swift in Sources */, C889569A27E8D1AC00E3779E /* LegacyInactiveTabHeader.swift in Sources */, - 2C6189862B7A8A22006B70D7 /* BrowserViewController+Ecosia.swift in Sources */, 7BEFC6801BFF68C30059C952 /* QuickActions.swift in Sources */, - 2C6189932B7A8A22006B70D7 /* AppSettingsTableViewController+Ecosia.swift in Sources */, 8A3EF8132A2FD07A00796E3A /* ResetContextualHints.swift in Sources */, D0C95E36200FDC5500E4E51C /* MetadataParserHelper.swift in Sources */, 0BF1B7E31AC60DEA00A7B407 /* InsetButton.swift in Sources */, @@ -13904,7 +14901,6 @@ 8A2783F1275FFDC50080D29D /* KeyboardPressesHandler.swift in Sources */, E650755C1E37F747006961AC /* Swizzling.m in Sources */, 74821FC51DB56A2500EEEA72 /* OpenWithSettingsViewController.swift in Sources */, - 1285E2B72CC68BF00053506B /* APNConsent.swift in Sources */, C84656012887A0F700861B4A /* WallpaperMetadataUtility.swift in Sources */, E1B04A9D28E20A8300670E54 /* InstructionsView.swift in Sources */, D3B6923D1B9F9444004B87A4 /* FindInPageBar.swift in Sources */, @@ -13918,8 +14914,8 @@ C87DF9DB267247190097E707 /* UIConstants+BottomInset.swift in Sources */, 8AD08D1527E9198E00B8E907 /* TabsQuantityTelemetry.swift in Sources */, 74B420C92A1D0D7A00370E53 /* OnboardingInstructionsPopupInfoModel.swift in Sources */, - 2C61898B2B7A8A22006B70D7 /* SnapKit+Ecosia.swift in Sources */, E13E9AB32AAB0FB5001A0E9D /* FakespotViewController.swift in Sources */, + 2CD265DA2CFE382C00A040A7 /* NTPImpactCellViewModel.swift in Sources */, E15DE7C4293A7B0F00B32667 /* PhotonActionSheetTitleHeaderView.swift in Sources */, 392ED7E61D0AEFEF009D9B62 /* HomePageAccessors.swift in Sources */, 8A0017C128A3FF6100FEFC8B /* MessageCardDataAdaptor.swift in Sources */, @@ -13928,41 +14924,40 @@ 8A5D1CA42A30D69A005AD35C /* SearchSetting.swift in Sources */, 74BBDF472A17979000D3BEFE /* OnboardingDefaultBrowserModelProtocol.swift in Sources */, 8AD40FC727BADC3400672675 /* ToolbarTextField.swift in Sources */, + 2CD266092CFE382C00A040A7 /* EmptyBookmarksViewDelegate.swift in Sources */, 8A720C5E2A4C85DA0003018A /* AccountSettingsDelegate.swift in Sources */, CA90753824929B22005B794D /* NoLoginsView.swift in Sources */, E4B423BE1AB9FE6A007E66C8 /* ReaderModeCache.swift in Sources */, 396CDB55203C5B870034A3A3 /* TabTrayController+KeyCommands.swift in Sources */, - 2C6189AA2B7A8A22006B70D7 /* Analytics.Values.swift in Sources */, C855728629AEA3FB00AF32B0 /* SurveySurfaceViewController.swift in Sources */, 74E36D781B71323500D69DA1 /* SettingsContentViewController.swift in Sources */, EBB8950C21939E4100EB91A0 /* FirefoxTabContentBlocker.swift in Sources */, 21618A632A422A3900A5189E /* ThemeMiddleware.swift in Sources */, 219A0FDB2ACCCFFC009A6D1A /* InactiveTabsSectionManager.swift in Sources */, - 2CCBB5272CAEAD53006E2E10 /* SeedCounterView.swift in Sources */, 211F00AC27F4D918001D9189 /* HistoryPanel+Search.swift in Sources */, 96EB6C3827D821B800A9D159 /* HistoryPanelViewModel.swift in Sources */, + 2CD2655C2CFDCF0900A040A7 /* SnapKit+Ecosia.swift in Sources */, AB52ED3B2A0E8873001067F5 /* UserConversionMetrics.swift in Sources */, 219935EC2B07110900E5966F /* TabTrayModel.swift in Sources */, 8A3EF7F72A2FCF6D00796E3A /* ExportLogDataSetting.swift in Sources */, - 2C5A5E652CB53DB7005BFE8B /* SeedCounterConfig.swift in Sources */, 43E69EC3254D081D00B591C2 /* SimpleTab.swift in Sources */, 8ADAE4202A33A0FD007BF926 /* SendFeedbackSetting.swift in Sources */, C29B64812AD6959E00F3244B /* QRCodeCoordinator.swift in Sources */, - 2C6189442B7A8A22006B70D7 /* WhatsNewViewController.swift in Sources */, 21A7C45028353D0E0071D996 /* OnboardingCardViewController.swift in Sources */, 742A56391D80B54A00BDB803 /* PhotonActionSheet.swift in Sources */, C4EFEECF1CEBB6F2009762A4 /* BackForwardTableViewCell.swift in Sources */, + 2CD265DB2CFE382C00A040A7 /* NTPImpactDividerFooter.swift in Sources */, E1442FD3294782D9003680B0 /* UIView+Constraints.swift in Sources */, 2C0360DA2C1747E6006706F2 /* FxNimbus.swift in Sources */, 5A70EF21295E3E0B00790249 /* UnitTestSceneDelegate.swift in Sources */, 2C49854E206173C800893DAE /* photon-colors.swift in Sources */, B2999FF72B194ADE00F0FEC1 /* FormAutofillPayloadType.swift in Sources */, 8A285B08294A5D4C00149B0F /* HomepageHeroImageViewModel.swift in Sources */, - 2C61897D2B7A8A22006B70D7 /* String.swift in Sources */, 8C92DE8B2A711ED60090BD28 /* FakespotClient.swift in Sources */, 8AC1065F28D0CD700013263A /* OpenQLPreviewHelper.swift in Sources */, EBA3B2C32268F16300728BDB /* PhotonActionSheetView.swift in Sources */, E18EA56F28AD3279003F97FC /* UIDevice+Extension.swift in Sources */, + 2CD266202CFE39E300A040A7 /* EcosiaPrimaryButton.swift in Sources */, CDB3BE8724746787009320EE /* FirefoxAccountSignInViewController.swift in Sources */, 8A471183287F6D9C00F5A6EA /* BookmarksPanelViewModel.swift in Sources */, E1FF93E428A2E74600E6360E /* WallpaperSelectorViewModel.swift in Sources */, @@ -13973,9 +14968,7 @@ 9614BF4428AD1C6700D3F7EA /* AccountSyncHandler.swift in Sources */, 282DA4731A68C1E700A406E2 /* OpenSearchParser.swift in Sources */, 8A13FA8D2AD834FA007527AB /* BackgroundTabLoader.swift in Sources */, - 2C6189822B7A8A22006B70D7 /* BrazeIntegrationExperiment.swift in Sources */, D04CD74B216CF86B004FF5B0 /* DevicePickerViewController.swift in Sources */, - 2C61896B2B7A8A22006B70D7 /* WelcomeTourRow.swift in Sources */, C8DC90BD2A06699E0008832B /* MarkupNode.swift in Sources */, E63ED8E11BFD25580097D08E /* PasswordManagerListViewController.swift in Sources */, 8ADC2A162A33765E00543DAA /* UrlToOpenModel.swift in Sources */, @@ -13993,45 +14986,40 @@ 4331A9BD271D267E005E8080 /* ContextualHintViewProvider.swift in Sources */, 8CAF29A02AA5E76B00DC3486 /* FakespotMessageCardView.swift in Sources */, 8A093D7F2A4B3E7D0099ABA5 /* GeneralSettingsDelegate.swift in Sources */, + 2CD2661C2CFE38AD00A040A7 /* EcosiaDebugSettings.swift in Sources */, D88FDAAF1F4E2BA000FD9709 /* PhotonActionSheetAnimator.swift in Sources */, C83432FE26BAD30D00ABAAA6 /* EnhancedTrackingProtectionDetailsVC.swift in Sources */, E698FFDA1B4AADF40001F623 /* TabScrollController.swift in Sources */, - 2C6189662B7A8A22006B70D7 /* EcosiaThemeColourPalette.swift in Sources */, 8A359EF32A1FD449004A5BB7 /* AdjustWrapper.swift in Sources */, D34510881ACF415700EC27F0 /* SearchLoader.swift in Sources */, - 2C61897E2B7A8A22006B70D7 /* FeatureManagement.swift in Sources */, E17496402994302D0096900A /* PreferredFont.swift in Sources */, E1442FD4294782D9003680B0 /* URL+Mail.swift in Sources */, 9636D92827F5D72D00771F5E /* GleanPlumbMessageManager.swift in Sources */, C80685D126A0C93900DCD895 /* UserResearch.swift in Sources */, B2999FF52B194AB200F0FEC1 /* FormAutofillHelperError.swift in Sources */, + 2CD2660F2CFE382C00A040A7 /* SemanticColor.swift in Sources */, + 2CD265572CFDCF0900A040A7 /* ErrorPageHandler+Ecosia.swift in Sources */, 8AED23C527AC1F9500DE7E97 /* BaseContentStackView.swift in Sources */, C8699131289176A5007ACC5C /* WallpaperNetworking.swift in Sources */, 5A271ABD2860B0D700471CE4 /* WebServerUtil.swift in Sources */, 964FA97528A1A8F20024BB3B /* ContextualHintEligibilityUtility.swift in Sources */, 439C489C29760575007C3DCD /* CreditCardValidator.swift in Sources */, 8ADC2A122A3375B900543DAA /* FxAEntryPoint.swift in Sources */, - 12147F312CDA3CD80009D300 /* NTPNewsletterCardViewModel.swift in Sources */, 216A0D7B2A40F08B008077BA /* ThemeSettingsAction.swift in Sources */, - 2C6189A22B7A8A22006B70D7 /* DefaultAppVersionInfoProvider.swift in Sources */, C2506C932A6A863600F2B76E /* HistoryCoordinator.swift in Sources */, 8A3EF8012A2FCFC900796E3A /* FasterInactiveTabs.swift in Sources */, CA7FC7D324A6A9B70012F347 /* PasswordManagerDataSourceHelper.swift in Sources */, 43AB6FA425DC53D30016B015 /* LabelButtonHeaderView.swift in Sources */, + 2CD265532CFDCF0900A040A7 /* BrowserCoordinator+Ecosia.swift in Sources */, 43D16B7C29831CD0009F8279 /* CreditCardItemRow.swift in Sources */, + 2CD265F92CFE382C00A040A7 /* PageActionMenuCell.swift in Sources */, 965C3C942933A860006499ED /* LaunchSessionProvider.swift in Sources */, - 2C6189382B7A8A22006B70D7 /* EmptyBookmarksViewDelegate.swift in Sources */, C8163851268A0899004C7160 /* AddCredentialViewController.swift in Sources */, - 2C61898E2B7A8A22006B70D7 /* UIView+maskedCorners.swift in Sources */, - 2C61895F2B7A8A22006B70D7 /* ClimateImpactInfo.swift in Sources */, - 2C6189612B7A8A22006B70D7 /* NTPAboutEcosiaCellViewModel.swift in Sources */, 8A19ACB22A3290AE001C2147 /* ClearPrivateDataSetting.swift in Sources */, CA520E7A24913C1B00CCAB48 /* PasswordManagerViewModel.swift in Sources */, - 2CA9952A2CA2C0BB001064CC /* NTPConfigurableNudgeCardCellViewModel.swift in Sources */, 8AE1E1CD27B191110024C45E /* SearchBarSettingsViewModel.swift in Sources */, 43D16B8529831EA5009F8279 /* Style.swift in Sources */, E16258EF2A83BE0800522742 /* FakespotLoadingView.swift in Sources */, - 2C61894A2B7A8A22006B70D7 /* CustomizableNTPSettingConfig.swift in Sources */, 8A19ACAB2A32895E001C2147 /* BrowserNavigationHandler.swift in Sources */, 8A01891C275E9C2A00923EFE /* ClearHistorySheetProvider.swift in Sources */, 8C44A9D22A6A99FE009A1AA7 /* ShoppingProduct.swift in Sources */, @@ -14043,19 +15031,26 @@ 8CBDE8E32AB09804001985BF /* ProductAnalyzeResponse.swift in Sources */, 96EB6C4327DC205D00A9D159 /* SearchGroupedItemsViewModel.swift in Sources */, 8A5BD95F2878B7B6000FE773 /* TopSitesWidgetManager.swift in Sources */, + 2CD265F52CFE382C00A040A7 /* WelcomeTourProfit.swift in Sources */, F84B22041A0910F600AAB793 /* AppDelegate.swift in Sources */, E1442FD2294782D9003680B0 /* UIViewController+Extension.swift in Sources */, E653422D1C5944F90039DD9E /* BrowserPrompts.swift in Sources */, + 2CD2660D2CFE382C00A040A7 /* LoadingScreen.swift in Sources */, E127313C28B6AD99006F39D2 /* WallpaperSettingsViewController.swift in Sources */, 43D4BCBA2972082400775FB5 /* CreditCardSettingsViewModel.swift in Sources */, 21583E422B1A3703009D084D /* LegacyInactiveTabModel.swift in Sources */, DF529EA12AB1B421003C5373 /* FakespotReliabilityScoreView.swift in Sources */, + 2CD265F32CFE382C00A040A7 /* WelcomeTourAction.swift in Sources */, 9636D92A27F767EC00771F5E /* NimbusMessagingEvaluationUtility.swift in Sources */, E127313F28B6C194006F39D2 /* WallpaperSettingsHeaderView.swift in Sources */, 2FDE87FE1ABB3817005317B1 /* LegacyRemoteTabsPanel.swift in Sources */, 8A3EF80F2A2FD05D00796E3A /* ToggleInactiveTabs.swift in Sources */, 435222C125882E3800FCA5B6 /* WidgetKitTopSiteModel.swift in Sources */, + 2CD265E62CFE382C00A040A7 /* NTPNewsletterCardViewModel.swift in Sources */, + 2CD265DE2CFE382C00A040A7 /* NTPLibaryCellViewModel.swift in Sources */, 96EB6C3E27D9266500A9D159 /* HistoryActionables.swift in Sources */, + 2CD265E02CFE382C00A040A7 /* NTPLibraryShortcutView.swift in Sources */, + 2CD2660C2CFE382C00A040A7 /* FilterController.swift in Sources */, 8C6F94662A972EB300415FF6 /* FakespotStarRatingView.swift in Sources */, C4F3B29A1CFCF93A00966259 /* ButtonToast.swift in Sources */, 1D7B78972ADF32590011E9F2 /* EventQueue.swift in Sources */, @@ -14066,83 +15061,85 @@ 8A57519927AD80B800A84DBF /* ReaderModeStyleViewModel.swift in Sources */, 435D7CC5246209AA0043ACB9 /* IntroViewController.swift in Sources */, C855728429AEA3C300AF32B0 /* SurveySurfaceViewModel.swift in Sources */, - 2C6189562B7A8A22006B70D7 /* NTPNewsCellViewModel.swift in Sources */, CA4ACE4924C8C91600F55894 /* BreachAlertsDetailView.swift in Sources */, C834ACD128D3ACA900203AD1 /* Blurrable.swift in Sources */, + 2CD265562CFDCF0900A040A7 /* DispatchQueueHelper+BuildChannel.swift in Sources */, C84655E22887388F00861B4A /* Wallpaper.swift in Sources */, C849E46326B9C3AF00260F0B /* EnhancedTrackingProtectionVM.swift in Sources */, 5A9F83402B2B4AE800272819 /* TabPeekAction.swift in Sources */, - 2C6189842B7A8A22006B70D7 /* WebsiteConnectionStatus.swift in Sources */, C8EDDBF429DF119F003A4C07 /* DeeplinkInput.swift in Sources */, D0E55C4F1FB4FD23006DC274 /* FormPostHelper.swift in Sources */, + 2CD265F12CFE382C00A040A7 /* WelcomeTour.Step.swift in Sources */, E118B9292862674E00C84831 /* LegacyInactiveTabItemCellModel.swift in Sources */, C88E7A552A0553180072E638 /* OnboardingViewModel.swift in Sources */, 8C29627C2B1F473800571655 /* AdEventsResponse.swift in Sources */, - 2C61898A2B7A8A22006B70D7 /* ErrorPageHandler+Ecosia.swift in Sources */, 962021E128B8078400BDF3D9 /* ContextualHintCopyProvider.swift in Sources */, - 2C6189672B7A8A22006B70D7 /* EcosiaTheme.swift in Sources */, 8AE1E1D227B1ADC40024C45E /* TopBottomInterchangeable.swift in Sources */, 8A093D7D2A4B3E4F0099ABA5 /* DebugSettingsDelegate.swift in Sources */, 96EA9454293655BF00123345 /* AppSession+Enums.swift in Sources */, C8B0F5F4283B7CCE007AE65D /* PocketProvider.swift in Sources */, - 2C6189642B7A8A22006B70D7 /* EcosiaNavigation.swift in Sources */, - 2CCBB5352CAF06DE006E2E10 /* SeedProgressManagerProtocol.swift in Sources */, 1DC372022B23C80F000F96C8 /* WindowManager.swift in Sources */, + 2CD265F22CFE382C00A040A7 /* WelcomeTour.swift in Sources */, C8E531C829E5EB6100E03FEF /* RouteBuilder.swift in Sources */, D3972BF41C22412B00035B87 /* TitleActivityItemProvider.swift in Sources */, D38A1BEE1A9FA2CA00F6A386 /* SiteTableViewController.swift in Sources */, 7BA0601B1C0F4DE200DFADB6 /* LegacyTabPeekViewController.swift in Sources */, - 2C6189652B7A8A22006B70D7 /* EcosiaThemeManager.swift in Sources */, 212985E42A6F078800546684 /* ScreenState.swift in Sources */, + 2CD2661E2CFE38AD00A040A7 /* NTPCustomizationSettingsViewController.swift in Sources */, D51EA5BA26406A0000334331 /* ExperimentsBranchesViewController.swift in Sources */, + 2CD265C62CFE382C00A040A7 /* MultiplyImpactStep.swift in Sources */, C84655F728879EF100861B4A /* WallpaperManager.swift in Sources */, 6669B5E2211418A200CA117B /* WebsiteDataSearchResultsViewController.swift in Sources */, - 2C6189722B7A8A22006B70D7 /* WelcomeNavigation.swift in Sources */, D51EA5CF26406D8300334331 /* ExperimentsViewController.swift in Sources */, + 2CD265C52CFE382C00A040A7 /* MultiplyImpact.swift in Sources */, 1DFE57FB27B2CB870025DE58 /* HighlightItem.swift in Sources */, CA77ABFD24773C92005079F9 /* BreachAlertsManager.swift in Sources */, 8AB8572727D93AEC0075C173 /* TopSiteHistoryManager.swift in Sources */, C81A8F2526D3ED1900EBA539 /* UIWindow+Extension.swift in Sources */, EBC4869E2195F58300CDA48D /* AboutHomeHandler.swift in Sources */, DDA24A431FD84D630098F159 /* DefaultSearchPrefs.swift in Sources */, - 2C9258DB2CEFB26500C6BB8D /* AnalyticsNotificationSettings.swift in Sources */, + 2CD265CE2CFE382C00A040A7 /* NTPSeedCounterCell.swift in Sources */, E65075611E37F77D006961AC /* MenuHelper.swift in Sources */, 8A7A26E529D4C0A800EA76F1 /* IntroScreenManager.swift in Sources */, 8AB8574827D97CD40075C173 /* HomePanelType.swift in Sources */, E174963A2992B42C0096900A /* CreditCardSectionHeader.swift in Sources */, - 2C61895C2B7A8A22006B70D7 /* NTPImpactCellViewModel.swift in Sources */, 8A3EF7F42A2FCF5700796E3A /* ExportBrowserDataSetting.swift in Sources */, 2128E27B292E624400FB91BE /* SendToDeviceActivity.swift in Sources */, E63ED7D81BFCD9990097D08E /* LoginDetailTableViewCell.swift in Sources */, C855728229AE7F1700AF32B0 /* SurveySurfaceManager.swift in Sources */, 66CE54A820FCF6CF00CC310B /* WebsiteDataManagementViewController.swift in Sources */, - 2C6189772B7A8A22006B70D7 /* EcosiaSettings.swift in Sources */, C8E2E80E23D20FD2005AACE6 /* FxAWebViewController.swift in Sources */, E14F7DF2288F3F9F00E3722C /* ThemedTableSectionHeaderFooterView.swift in Sources */, 8A3EF7F22A2FCF4000796E3A /* DeleteExportedDataSetting.swift in Sources */, 8ADC2A142A33762900543DAA /* ReferringPage.swift in Sources */, E1442FBF294782B6003680B0 /* CGRect+Extension.swift in Sources */, + 2CD265EA2CFE382C00A040A7 /* DefaultBrowser.swift in Sources */, 96F8DA49280452CA00E53239 /* GleanPlumbContextProvider.swift in Sources */, + 2CD265DD2CFE382C00A040A7 /* ProgressView.swift in Sources */, 43DB9784292D6846002E0B9F /* ShareButton.swift in Sources */, 8AB8574627D97CB00075C173 /* HomepageContextMenuProtocol.swift in Sources */, + 2CD265DC2CFE382C00A040A7 /* NTPImpactRowView.swift in Sources */, 3BB50E201D627539004B33DF /* HomepageViewController.swift in Sources */, C82A94F3269F68F300624AA7 /* CoreFlaggableFeature.swift in Sources */, - 2C6189742B7A8A22006B70D7 /* MultiplyImpact.swift in Sources */, F84B22241A09122500AAB793 /* LibraryViewController.swift in Sources */, 39455F771FC83F430088A22C /* TabEventHandler.swift in Sources */, + 2CD2660E2CFE382C00A040A7 /* MarketsController.swift in Sources */, 8A36BE2929EDBC6900AC1C5C /* ContentContainer.swift in Sources */, 215B458227DA420400E5E800 /* LegacyTabMetadataManager.swift in Sources */, E47616C71AB74CA600E7DD25 /* ReaderModeBarView.swift in Sources */, D821E90E2141B71C00452C55 /* SiriSettingsViewController.swift in Sources */, EBB89507219398E500EB91A0 /* ContentBlocker.swift in Sources */, - 2C6189A42B7A8A22006B70D7 /* EcosiaInstallType.swift in Sources */, 216A0D792A40E85A008077BA /* ThemeSettingsState.swift in Sources */, 5A3A2A0D287F742C00B79EAC /* BackgroundSyncUtility.swift in Sources */, 21AFCFEE2AE80B700027E9CE /* TabsCoordinator.swift in Sources */, 23ED80FF25C89C9800D0E9D5 /* DefaultBrowserOnboardingViewController.swift in Sources */, + 2CD266032CFE382C00A040A7 /* WhatsNewViewModel.swift in Sources */, + 2CD2660B2CFE382C00A040A7 /* EmptyReadingListView.swift in Sources */, + 2CD265EC2CFE382C00A040A7 /* NTPTooltip.Highlight.swift in Sources */, 8A3EF80D2A2FD04D00796E3A /* ResetWallpaperOnboardingPage.swift in Sources */, E1FE132F29C0B3CB002A65FF /* NotificationSurfaceManager.swift in Sources */, D88FDA9F1F4E2B9200FD9709 /* PhotonActionSheetProtocol.swift in Sources */, + 2CD265D72CFE382C00A040A7 /* NTPCustomizationCellViewModel.swift in Sources */, 21618A932A4499FC00A5189E /* AppState.swift in Sources */, BD1C89CA2A1E3CE7000A4201 /* PocketFooterView.swift in Sources */, E12BD0AE28AC38480029AAF0 /* UIImage+Extension.swift in Sources */, @@ -14153,16 +15150,15 @@ C8656D79270F866700E199EA /* CustomizeHomepageSectionCell.swift in Sources */, ABE4393E2AC432040074FFE1 /* PartnerWebsites.swift in Sources */, C84655FB28879FC600861B4A /* WallpaperStorageUtility.swift in Sources */, + 2CD265E12CFE382C00A040A7 /* NTPLogoCell.swift in Sources */, 8A13FA8B2AD82E6D007527AB /* ApplicationStateProvider.swift in Sources */, 4347B39A298DA5BB0045F677 /* CreditCardInputViewModel.swift in Sources */, 6025B10D267B6C5400F59F6B /* LoginRecordExtension.swift in Sources */, - 2C61893B2B7A8A22006B70D7 /* EmptyReadingListView.swift in Sources */, 2F44FCC51A9E85E900FD20CC /* SettingsTableViewController.swift in Sources */, - 2C6189752B7A8A22006B70D7 /* MultiplyImpactStep.swift in Sources */, 211046C92A7ADE9000A7309F /* BlockPopupSetting.swift in Sources */, 8A3233FC286270CF003E1C33 /* FxBookmarkNode.swift in Sources */, E1CEC2022A28C3F100B177D5 /* LoginDetailCenteredTableViewCell.swift in Sources */, - 2C61898D2B7A8A22006B70D7 /* SimpleToast+Ecosia.swift in Sources */, + 2CD265D12CFE382C00A040A7 /* SeedCounterHiddenSettings.swift in Sources */, C2D71B972A384F40003DEC7A /* ThemedSubtitleTableViewCell.swift in Sources */, 8A83B7462A264FA0002FF9AC /* SettingsCoordinator.swift in Sources */, A9072B801D07B34100459960 /* NoImageModeHelper.swift in Sources */, @@ -14170,24 +15166,21 @@ D8AA923421A602DC002605C0 /* HomePageSettingViewController.swift in Sources */, 8A161411282C035D00DDBB02 /* CustomizeHomepageSectionViewModel.swift in Sources */, C8DC90C52A066B6A0008832B /* MarkupTokenizingUtility.swift in Sources */, - 2C61895B2B7A8A22006B70D7 /* NTPImpactCell.swift in Sources */, - 2C6189522B7A8A22006B70D7 /* NTPTooltip.Highlight.swift in Sources */, EBC4869D2195F58300CDA48D /* ErrorPageHelper.swift in Sources */, C84655E8288739CB00861B4A /* WallpaperCollectionAvailability.swift in Sources */, C834330026BAD32800ABAAA6 /* EnhancedTrackingProtectionDetailsVM.swift in Sources */, + 2CD265E22CFE382C00A040A7 /* NewsController.swift in Sources */, + 2CD2661A2CFE38AD00A040A7 /* EcosiaTopSiteItemCell.swift in Sources */, 8A03309528C2653600286539 /* LegacyTabFileManager.swift in Sources */, 8AB5958A284145B30090F4AE /* HomepageSectionHandler.swift in Sources */, - 2C6189512B7A8A22006B70D7 /* CircleButton.swift in Sources */, E1442FD0294782D9003680B0 /* UIAlertController+Extension.swift in Sources */, 8A76B01429F6E45600A82607 /* CoordinatorFlagManager.swift in Sources */, E660BDD91BB06521009AC090 /* TabsButton.swift in Sources */, 8C92DE932A7128DE0090BD28 /* ProductAdsResponse.swift in Sources */, E1A6AB4828CA833000EBEBDD /* WallpaperBaseViewController.swift in Sources */, 4331A9BB27193DF0005E8080 /* ContextualHintViewController.swift in Sources */, - 2C6189882B7A8A22006B70D7 /* UIButton+Ecosia.swift in Sources */, 39EF434E260A73950011E22E /* Experiments.swift in Sources */, E15DE7C0293A670700B32667 /* PhotonActionSheetSeparator.swift in Sources */, - 2C6189452B7A8A22006B70D7 /* WhatsNewItem.swift in Sources */, DFEA639E279F468A00D489C3 /* DynamicHeightCollectionView.swift in Sources */, F8B7109E2ABE380B0029726E /* RustErrors.swift in Sources */, C8124BB129D6F55400540B79 /* Route.swift in Sources */, @@ -14198,6 +15191,8 @@ 8A7A26EA29D4C3C800EA76F1 /* LaunchType.swift in Sources */, D04D1B92209790B60074B35F /* Toast.swift in Sources */, 8A93F87429D3A5C1004159D9 /* LaunchCoordinator.swift in Sources */, + 2CD266022CFE382C00A040A7 /* WhatsNewViewController.swift in Sources */, + 2CD265CC2CFE382C00A040A7 /* UserDefaultsSeedProgressManager.swift in Sources */, 8AD5702F27AB4DEA005BFDC8 /* UIStackView+Extension.swift in Sources */, C2D71B952A384F11003DEC7A /* ThemedTableViewCell.swift in Sources */, 219A0FD72ACC8C03009A6D1A /* InactiveTabsHeaderView.swift in Sources */, @@ -14212,25 +15207,25 @@ 8A1E93EA2A3CDC6100DD540A /* BaseCoordinator.swift in Sources */, E1442FD6294782D9003680B0 /* UIView+Extension.swift in Sources */, 74F80D342A0A52D700013C3D /* PrivacyPolicyViewController.swift in Sources */, - 2C61894F2B7A8A22006B70D7 /* NTPLibaryCellViewModel.swift in Sources */, + 2CD265FC2CFE382C00A040A7 /* EcosiaThemeColourPalette.swift in Sources */, 274A36CE239EB9EC00A21587 /* LibraryViewController+LibraryPanelDelegate.swift in Sources */, - 126509822CD924C00011BA36 /* BrazeService.swift in Sources */, C869912D28917688007ACC5C /* WallpaperImageLoader.swift in Sources */, 96A5F73829928B3700234E5F /* GeneralizedImageFetcher.swift in Sources */, 1D7B78992ADF328E0011E9F2 /* AppEvent.swift in Sources */, - 2C6189972B7A8A22006B70D7 /* ConnectionStatusImage.swift in Sources */, 43F7952525795F69005AEE40 /* SearchTelemetry.swift in Sources */, E65075541E37F6FC006961AC /* LegacyDynamicFontHelper.swift in Sources */, - 2CCBB5232CAE9826006E2E10 /* ArcProgressView.swift in Sources */, 8ADAE4242A33A126007BF926 /* StudiesToggleSetting.swift in Sources */, C82CDD47233E8996002E2743 /* Tab+ChangeUserAgent.swift in Sources */, 81122E212B221AC0003DD9F8 /* SearchScreenState.swift in Sources */, + 2CD265ED2CFE382C00A040A7 /* NTPTooltip.swift in Sources */, C4E3984C1D21F2FD004E89BA /* TabTrayButtonExtensions.swift in Sources */, 437A857827E43FE100E42764 /* FxAWebViewTelemetry.swift in Sources */, E13E9AB42AAB0FB5001A0E9D /* FakespotCoordinator.swift in Sources */, E1442FD1294782D9003680B0 /* UIModalPresentationStyle+Photon.swift in Sources */, E1ADE23E2B06559500FD17AA /* FakespotAction.swift in Sources */, 5A32C2B62AD8517200A9B5A4 /* MetricKitWrapper.swift in Sources */, + 2CD265D32CFE382C00A040A7 /* SeedProgressView.swift in Sources */, + 2CD266512CFF56CB00A040A7 /* ConnectionStatusImage.swift in Sources */, 8A95FF642B1E969E00AC303D /* TelemetryContextualIdentifier.swift in Sources */, D3FEC38D1AC4B42F00494F45 /* AutocompleteTextField.swift in Sources */, 8A19ACAE2A329058001C2147 /* PasswordManagerSetting.swift in Sources */, @@ -14240,7 +15235,7 @@ 1D8487B42AD0C6C100F7527C /* RemoteTabsPanelMiddleware.swift in Sources */, C849E46526B9C3DD00260F0B /* SlideoverPresentationController.swift in Sources */, E18F44072A951C330056160F /* FakespotHighlightGroup.swift in Sources */, - 2C6189312B7A8A22006B70D7 /* EcosiaLaunchScreenView.swift in Sources */, + 2CD265CB2CFE382C00A040A7 /* SeedProgressManagerProtocol.swift in Sources */, D5D0532E2645B3A700759F85 /* ExperimentsTableView.swift in Sources */, 8A832A9029DC96C50025D5DD /* LaunchScreenView.swift in Sources */, 214EF4152AC5D5D0005BCCDA /* TabDisplayView.swift in Sources */, @@ -14256,7 +15251,9 @@ AB03032B2AB47AF300DCD8EF /* FakespotOptInCardView.swift in Sources */, 0BA02DB22942605600C92603 /* FormAutofillHelper.swift in Sources */, 8A19ACB82A329128001C2147 /* PrivacyPolicySetting.swift in Sources */, + 2CD265592CFDCF0900A040A7 /* LegacyThemeManager+Ecosia.swift in Sources */, E68AEDB01B18F81A00133D99 /* SwipeAnimator.swift in Sources */, + 2CD266002CFE382C00A040A7 /* WhatsNewCell.swift in Sources */, 1DDE3DB32AC34E1E0039363B /* TabCell.swift in Sources */, 3BF56D271CDBBE1F00AC4D75 /* SimpleToast.swift in Sources */, C8B0F5F6283B7CCE007AE65D /* PocketStory.swift in Sources */, @@ -14266,21 +15263,22 @@ EBB89504219398E500EB91A0 /* TrackingProtectionPageStats.swift in Sources */, D31F95E91AC226CB005C9F3B /* ScreenshotHelper.swift in Sources */, 8A5D1CA62A30D6BD005AD35C /* NewTabPageSetting.swift in Sources */, + 2CD266082CFE382C00A040A7 /* EmptyBookmarksView.swift in Sources */, D3968F251A38FE8500CEFD3B /* TabManager.swift in Sources */, 2178A6A229145506002EC290 /* ReaderModeFontSizeLabel.swift in Sources */, 8ADC2A1D2A33999800543DAA /* VersionSetting.swift in Sources */, 96EB6C4027DBEE9800A9D159 /* SearchGroupedItemsViewController.swift in Sources */, 8ADAE41E2A33A0E2007BF926 /* ShowIntroductionSetting.swift in Sources */, 8AF10D8F29D774090086351D /* SceneSetupHelper.swift in Sources */, - 2C6189492B7A8A22006B70D7 /* DefaultBrowser.swift in Sources */, - 2C61896C2B7A8A22006B70D7 /* Welcome.swift in Sources */, C4E398601D22C409004E89BA /* TopTabsLayout.swift in Sources */, + 2CD265F72CFE382C00A040A7 /* WelcomeTourTransparent.swift in Sources */, + 2CD265FF2CFE382C00A040A7 /* WhatsNewLocalDataProvider.swift in Sources */, E1A102D62AC19B30007B617A /* FakespotUtils.swift in Sources */, 8A5D1CC12A30DCA4005AD35C /* SettingDisclosureUtility.swift in Sources */, E1442FD5294782D9003680B0 /* UIView+SnapKit.swift in Sources */, E1ADE23C2B0649F200FD17AA /* FakespotState.swift in Sources */, + 2CD265D62CFE382C00A040A7 /* NTPCustomizationCell.swift in Sources */, 2816F0001B33E05400522243 /* UIConstants.swift in Sources */, - 2C6189432B7A8A22006B70D7 /* WhatsNewViewModel.swift in Sources */, 21E78A7228F9A93100F8D687 /* UIDeviceInterface.swift in Sources */, EBB89508219398E500EB91A0 /* ContentBlocker+Safelist.swift in Sources */, 437A9B6A2681257F00FB41C1 /* LegacyInactiveTabViewModel.swift in Sources */, @@ -14289,35 +15287,32 @@ D0B9483422A03468002F4AA1 /* BookmarkDetailPanel.swift in Sources */, 968BD7EB27DFF0F8003148B3 /* ASGroup.swift in Sources */, 392ED7E41D0AEF56009D9B62 /* NewTabAccessors.swift in Sources */, - 2C61895E2B7A8A22006B70D7 /* NTPImpactDividerFooter.swift in Sources */, - 2C6189782B7A8A22006B70D7 /* NTPCustomizationSettingsViewController.swift in Sources */, 1D2F68AD2ACB266300524B92 /* RemoteTabsPanelState.swift in Sources */, + 2CD266062CFE382C00A040A7 /* EcosiaFindInPageBar.swift in Sources */, 4347B398298D6D7B0045F677 /* CreditCardTableViewModel.swift in Sources */, 1DFE57FD27BADD7D0025DE58 /* HomepageViewModel.swift in Sources */, - 2C03A4152CB7C7CC00AB228B /* DispatchQueueHelper+BuildChannel.swift in Sources */, D3A9949D1A3686BD008AD1AC /* Tab.swift in Sources */, 8AD40FD127BADCBA00672675 /* ToolbarButton.swift in Sources */, + 2CD2660A2CFE382C00A040A7 /* EmptyHeader.swift in Sources */, A93067E81D0FE18E00C49C6E /* NightModeHelper.swift in Sources */, 742BD99E2A13AC9000BA6B15 /* OnboardingInstructionPopupViewController.swift in Sources */, 9636D92C27F9E50100771F5E /* GleanPlumbMessageStore.swift in Sources */, 213B67A627CE682B000542F5 /* StartAtHomeHelper.swift in Sources */, E10A1F752863BC51001EEA80 /* LegacyInactiveTabItemCell.swift in Sources */, - 2C6189A72B7A8A22006B70D7 /* Version+Extensions.swift in Sources */, 8A8629E2288096C40096DDB1 /* BookmarksFolderCell.swift in Sources */, DAE6DF1B29AD78DA0094BD1B /* BrowserViewController+ZoomPage.swift in Sources */, 8AB8574A27D97CE90075C173 /* HomePanelDelegate.swift in Sources */, - 2C6189872B7A8A22006B70D7 /* LegacyThemeManager+Ecosia.swift in Sources */, 2137786529832C8900D01309 /* OverlayModeManager.swift in Sources */, 8AF99B4D29EF076800108DEC /* WebviewViewController.swift in Sources */, 3B39EDCB1E16E1AA00EF029F /* CustomSearchViewController.swift in Sources */, - 2C5A5E672CB53DF9005BFE8B /* UserDefaultsSeedProgressManager.swift in Sources */, C8B0F5F7283B7CCE007AE65D /* PocketFeedStory.swift in Sources */, 96A562A027D6D0E80045144A /* ContileProvider.swift in Sources */, 8A5D1CAC2A30D70B005AD35C /* OpenWithSetting.swift in Sources */, 8AFA263227B6E9AB00D0C33B /* ToolbarBadge.swift in Sources */, 43D16B7A29831C7F009F8279 /* CreditCardAutofillToggle.swift in Sources */, + 2CD265D82CFE382C00A040A7 /* ClimateImpactInfo.swift in Sources */, C23889E12A4F3E7200429673 /* ParentCoordinatorDelegate.swift in Sources */, - 2C6189332B7A8A22006B70D7 /* MMP.swift in Sources */, + 2CD265D92CFE382C00A040A7 /* NTPImpactCell.swift in Sources */, 8AD40FCD27BADC5C00672675 /* TabLocationContainerView.swift in Sources */, 8A720C602A4C8B700003018A /* SharedSettingsDelegate.swift in Sources */, 211046CD2A7D842A00A7309F /* TPAccessoryInfo.swift in Sources */, @@ -14326,10 +15321,9 @@ E18EA57128AD46D3003F97FC /* WallpaperCollectionType.swift in Sources */, C84655E42887394B00861B4A /* WallpaperMetadata.swift in Sources */, 8AB8572E27D94A1A0075C173 /* UXSizeClass.swift in Sources */, + 2CD265F62CFE382C00A040A7 /* WelcomeTourRow.swift in Sources */, 965C3C8F29313A1B006499ED /* AppSessionManager.swift in Sources */, - 2C6189902B7A8A22006B70D7 /* NumberFormatter+Ecosia.swift in Sources */, 45D5EDA729269F7500311934 /* DataObserver.swift in Sources */, - 2C61896E2B7A8A22006B70D7 /* WelcomeTourAction.swift in Sources */, 961577922A38FDB300391E8D /* SponsoredTileDataUtility.swift in Sources */, D863C8F21F68BFC20058D95F /* GradientProgressBar.swift in Sources */, C8445A14264428DC00B83F53 /* LibraryPanelViewState.swift in Sources */, @@ -14337,22 +15331,22 @@ E134D5802B31FF3100C6B17B /* FakespotAdLinkButton.swift in Sources */, 8AE0BF4F2819B10E00F33EC4 /* TopSitesSettingsViewController.swift in Sources */, 8A8DDEBF276259A900E7B97A /* RatingPromptManager.swift in Sources */, + 2CD265CD2CFE382C00A040A7 /* ArcProgressView.swift in Sources */, 1D969C702B21322B004255B1 /* BrowserWindow.swift in Sources */, 8AB8571D27D929350075C173 /* TopSitesViewModel.swift in Sources */, - 2C6189942B7A8A22006B70D7 /* DeviceInfo+Ecosia.swift in Sources */, - 2C78374B2C1765DF00BBFFEB /* LoadingScreen.swift in Sources */, 8A93F85E29D36DA9004159D9 /* Coordinator.swift in Sources */, + 2CD265F02CFE382C00A040A7 /* WelcomeNavigation.swift in Sources */, 1D2F68B12ACCA22000524B92 /* RemoteTabsEmptyView.swift in Sources */, 43D16B8729831EEF009F8279 /* RemoveCardButton.swift in Sources */, D59431ED25E9912900F0BA82 /* WidgetIntents.intentdefinition in Sources */, 213778632980448C00D01309 /* DownloadFileFetcher.swift in Sources */, F85C7EDF2710B4DD004BDBA4 /* LoginOnboarding.swift in Sources */, C8B0F5F5283B7CCE007AE65D /* PocketSponsoredStory.swift in Sources */, - 2C6189982B7A8A22006B70D7 /* BookmarksExchange.swift in Sources */, 8AF6D4E12A856B4500B0474B /* ContileNetworking.swift in Sources */, 8AB8571F27D931B40075C173 /* EmptyTopSiteCell.swift in Sources */, CAC458F1249429C20042561A /* PasswordManagerSelectionHelper.swift in Sources */, C8656D75270F834600E199EA /* FlaggableFeatureOptions.swift in Sources */, + 2CD2655F2CFDCF0900A040A7 /* UIView+maskedCorners.swift in Sources */, C8CD80D82A1E31C20097C3AE /* NimbusOnboardingTestingConfigUtility.swift in Sources */, 8AB8573727D951640075C173 /* HomeLogoHeaderViewModel.swift in Sources */, D3C744CD1A687D6C004CE85D /* URIFixup.swift in Sources */, @@ -14363,9 +15357,11 @@ AB42CC752A1F5240003C9594 /* CreditCardBottomSheetHeaderView.swift in Sources */, 21371FA428AA7A8D00BC3F37 /* OnboardingViewModelProtocol.swift in Sources */, 21DB34342B20FE35008CCB8E /* LegacyRemoteTabsTableViewController.swift in Sources */, - 2C6189912B7A8A22006B70D7 /* URL+Ecosia.swift in Sources */, 8ABCFEA32B45C36100C2988A /* PrivateBrowsingTelemetry.swift in Sources */, + 2CD265EF2CFE382C00A040A7 /* Welcome.swift in Sources */, + 2CD266552CFF56EA00A040A7 /* WebsiteConnectionStatus.swift in Sources */, 8ADC2A1F2A3399BD00543DAA /* LicenseAndAcknowledgementsSetting.swift in Sources */, + 2CD266522CFF56CB00A040A7 /* ConnectionStatusImage+WebsiteConnectionTypeStatus.swift in Sources */, 8CCCB08B2AE26B5C0073ADB9 /* ReportResponse.swift in Sources */, 21A43CDD291461C700B1206D /* ReaderModeFontTypeButton.swift in Sources */, D3BE7B261B054D4400641031 /* main.swift in Sources */, @@ -14373,17 +15369,16 @@ C2D71B992A384F6A003DEC7A /* ThemedLeftAlignedTableViewCell.swift in Sources */, 21A7C44E283539170071D996 /* IntroViewModel.swift in Sources */, E1FF93E228A2E55700E6360E /* WallpaperSelectorViewController.swift in Sources */, - 2C6189A92B7A8A22006B70D7 /* Analytics+Configuration.swift in Sources */, D3A9949C1A3686BD008AD1AC /* BrowserViewController.swift in Sources */, 8A5D1CBB2A30DC0B005AD35C /* ConnectSetting.swift in Sources */, + 2CD265FD2CFE382C00A040A7 /* EcosiaThemeManager.swift in Sources */, E6CF28E71CB43B7900151AB3 /* SensitiveViewController.swift in Sources */, 43AB6FA225DC53D30016B015 /* GoogleTopSiteManager.swift in Sources */, - 2C6189592B7A8A22006B70D7 /* NTPTooltipDelegate.swift in Sources */, 8A5D1CB62A30DBB0005AD35C /* DefaultBrowserSetting.swift in Sources */, 4393932029AC6CE900DC5A85 /* EnvironmentValues+Extension.swift in Sources */, ABEF80D12A24D2BE003F52C4 /* CreditCardBottomSheetViewModel.swift in Sources */, 21AFCFF02AE80D370027E9CE /* RemoteTabsCoordinator.swift in Sources */, - 12147F2F2CDA3CD00009D300 /* NTPNewsletterCardCell.swift in Sources */, + 2CD265FA2CFE382C00A040A7 /* PageActionsShortcutsHeader.swift in Sources */, 43162A2F2492DB7800F91658 /* EmptyPrivateTabsView.swift in Sources */, E1380B8D2AEA897C00630AFA /* SidebarEnabledView.swift in Sources */, D0625C98208E87F10081F3B2 /* DownloadQueue.swift in Sources */, @@ -14392,21 +15387,23 @@ 966E4B2629F2D4AC00299B8D /* AccessoryViewProvider.swift in Sources */, ABEF80D52A254185003F52C4 /* CreditCardBottomSheetFooterView.swift in Sources */, C869912E28917688007ACC5C /* WallpaperDataService.swift in Sources */, + 2CD2661B2CFE38AD00A040A7 /* EcosiaHomepageSectionType.swift in Sources */, 5A8017E029CE15D90047120D /* TabManagerImplementation.swift in Sources */, 8AABBD012A001ADF0089941E /* ApplicationHelper.swift in Sources */, E633E2DA1C21EAF8001FFF6C /* PasswordDetailViewController.swift in Sources */, - 2C6189472B7A8A22006B70D7 /* WhatsNewLocalDataProvider.swift in Sources */, + 2CD265E42CFE382C00A040A7 /* NTPNewsCellViewModel.swift in Sources */, 8A5D1CAE2A30D71A005AD35C /* ThemeSetting.swift in Sources */, C82F4C2B29AE2DF1005BD116 /* NotificationsSettingsViewController.swift in Sources */, - 2C6189712B7A8A22006B70D7 /* WelcomeTourGreen.swift in Sources */, 59A68B280D62462B85CF57A4 /* HistoryPanel.swift in Sources */, - 2CA995282CA2C06A001064CC /* NTPConfigurableNudgeCardCell.swift in Sources */, C400467C1CF4E43E00B08303 /* BackForwardListViewController.swift in Sources */, D5D237782640BBA600326204 /* ExperimentsSettingsViewController.swift in Sources */, + 2CD266072CFE382C00A040A7 /* EcosiaNavigation.swift in Sources */, D3972BF31C22412B00035B87 /* ShareExtensionHelper.swift in Sources */, + 2CAF5AE82D00B1D300D3DCDD /* EcosiaLaunchScreenView.swift in Sources */, F35B8D2D1D6383E9008E3D61 /* SessionRestoreHelper.swift in Sources */, 8A19ACB62A3290F9001C2147 /* NotificationsSetting.swift in Sources */, 43D16B8229831E6A009F8279 /* CreditCardInputField.swift in Sources */, + 2CD265D02CFE382C00A040A7 /* SeedCounterConfig.swift in Sources */, EB98550124226EF70040F24B /* AppDelegate+SyncSentTabs.swift in Sources */, 744ED5611DBFEB8D00A2B5BE /* MailtoLinkHandler.swift in Sources */, 8A720C622A4CBB370003018A /* SupportSettingsDelegate.swift in Sources */, @@ -14424,27 +15421,28 @@ 8A3EF8112A2FD06B00796E3A /* ToggleHistoryGroups.swift in Sources */, 219A0FD92ACC8C0F009A6D1A /* InactiveTabsFooterView.swift in Sources */, 884CA7492344A301002E4711 /* TextContentDetector.swift in Sources */, - 2C6189552B7A8A22006B70D7 /* NewsController.swift in Sources */, C8656D77270F858900E199EA /* TabsSettingsViewControler.swift in Sources */, 8A9F0B5627C595F300FE09AE /* ImageIdentifiers.swift in Sources */, 7B844E3D1BBDDB9D00E733A2 /* ChevronView.swift in Sources */, 43A5643823CD1E1C00B6857D /* UpdateViewController.swift in Sources */, 437A9B682681256800FB41C1 /* LegacyInactiveTabCell.swift in Sources */, - 2C6189A62B7A8A22006B70D7 /* Version.swift in Sources */, 1D69FF8D27B17286001F660E /* HomeLogoHeaderCell.swift in Sources */, E4C358551AF144BA00299F7E /* FSReadingList.m in Sources */, 8AE1E1CB27B18F560024C45E /* SearchBarSettingsViewController.swift in Sources */, 8AD40FC527BADC1F00672675 /* TabToolbarHelper.swift in Sources */, + 2CD265E72CFE382C00A040A7 /* NTPConfigurableNudgeCardCell.swift in Sources */, 9609F4CA26B57CE800F81493 /* Calendar+Extension.swift in Sources */, + 2CD265D22CFE382C00A040A7 /* SeedCounterView.swift in Sources */, E663D5781BB341C4001EF30E /* ToggleButton.swift in Sources */, DFA51481275FFEE500266AA0 /* HistoryHighlightsManager.swift in Sources */, + 2CD265542CFDCF0900A040A7 /* BrowserViewController+Ecosia.swift in Sources */, 8ADC2A182A33775F00543DAA /* FxASignInViewParameters.swift in Sources */, EBA3B2D22268F57E00728BDB /* BadgeWithBackdrop.swift in Sources */, 8A83B7482A264FB7002FF9AC /* LibraryCoordinator.swift in Sources */, 96EB6C3C27D82AEA00A9D159 /* HistoryPanel+ContextMenuExtensions.swift in Sources */, - 2C61894E2B7A8A22006B70D7 /* NTPLibraryCell.swift in Sources */, E1CD81C2290C62A600124B27 /* HostingTableViewCell.swift in Sources */, 43175DB826B87D2C00C41C31 /* AdsTelemetryHelper.swift in Sources */, + 2CD2655B2CFDCF0900A040A7 /* SimpleToast+Ecosia.swift in Sources */, 8A3EF7FF2A2FCFBB00796E3A /* ChangeToChinaSetting.swift in Sources */, E6327A641BF6438E008D12E0 /* DebugSettingsBundleOptions.swift in Sources */, F85C7EDD27109241004BDBA4 /* PasswordManagerOnboardingViewController.swift in Sources */, @@ -14453,7 +15451,6 @@ 21F2A2D22B0BC85200626AEC /* InactiveTabsModel.swift in Sources */, D3E8EF101B97BE69001900FB /* ClearPrivateDataTableViewController.swift in Sources */, C8F457AA1F1FDD9B000CB895 /* BrowserViewController+KeyCommands.swift in Sources */, - 2C6189462B7A8A22006B70D7 /* WhatsNewDataProvider.swift in Sources */, 21357F2F294237D8004BF9FD /* RemoteTabsClientAndTabsDataSource.swift in Sources */, 59A68FD5260B8D520F890F4A /* ReaderPanel.swift in Sources */, 21D0F62129B91BF500022292 /* ToastView.swift in Sources */, @@ -14462,7 +15459,6 @@ 59A68D66379CFA85C4EAF00B /* TwoLineImageOverlayCell.swift in Sources */, 8A04136928258DF600D20B10 /* SponsoredTileTelemetry.swift in Sources */, D04CD718215EBD85004FF5B0 /* SettingsLoadingView.swift in Sources */, - 2C6189682B7A8A22006B70D7 /* FilterController.swift in Sources */, 21420EF72ABA338D00B28550 /* TabTrayCoordinator.swift in Sources */, 5A70EF1F295E3DFC00790249 /* UnitTestAppDelegate.swift in Sources */, 2128E27E2934F78600FB91BE /* CustomAppActivity.swift in Sources */, @@ -14471,7 +15467,7 @@ C8CD80D72A1E2C6E0097C3AE /* NimbusMessagingHelperUtilityProtocol.swift in Sources */, 8ACE9BFB2A54A010001E7A73 /* ExpandButtonState.swift in Sources */, 8AC5D55F28BFE6C8001F6F7F /* Presenter.swift in Sources */, - 2C6189892B7A8A22006B70D7 /* UIFont+Ecosia.swift in Sources */, + 2CD265C82CFE382C00A040A7 /* NTPAboutEcosiaCell.swift in Sources */, 8AEDB11529F9F00400F2A53B /* SceneContainer.swift in Sources */, AB42CC742A1F5240003C9594 /* CreditCardBottomSheetViewController.swift in Sources */, C8741FE928C4D30F00030029 /* FileManagerInterface.swift in Sources */, @@ -14479,9 +15475,9 @@ 8A3233FE28627446003E1C33 /* LocalDesktopFolder.swift in Sources */, 8AD40FCA27BADC4B00672675 /* ReaderModeButton.swift in Sources */, E1C437A32A96343A00D188CB /* FakespotFadeLabel.swift in Sources */, - 2C61897F2B7A8A22006B70D7 /* EcosiaHomepageSectionType.swift in Sources */, 8A3EF7FB2A2FCF9D00796E3A /* ForceCrashSetting.swift in Sources */, 2178A6A4291455F7002EC290 /* ReaderModeFontSizeButton.swift in Sources */, + 2CD265582CFDCF0900A040A7 /* HomepageViewController+Ecosia.swift in Sources */, BCFF93F02AABA55A005B5B71 /* BackgroundFirefoxSuggestIngestUtility.swift in Sources */, BCFF93F22AAF9688005B5B71 /* FirefoxSuggestSettings.swift in Sources */, BCFF93F42AAF9879005B5B71 /* FirefoxSuggestSettingsViewController.swift in Sources */, @@ -14492,14 +15488,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2C872A5A2B8CD7E000B318A0 /* MockAppVersionInfoProvider.swift in Sources */, - 2C872A5B2B8CD7E000B318A0 /* AnalyticsTests.swift in Sources */, - 2C872A5E2B8CD7E000B318A0 /* EcosiaInstallTypeTests.swift in Sources */, - 2C872A5F2B8CD7E000B318A0 /* EcosiaNTPTooltipHighlightTests.swift in Sources */, - 2C872A602B8CD7E000B318A0 /* EcosiaPageActionMenuCellTests.swift in Sources */, - 2C872A622B8CD7E000B318A0 /* VersionTests.swift in Sources */, - 2C872A632B8CD7E000B318A0 /* WhatsNewLocalDataProviderTests.swift in Sources */, - 2C872A642B8CD7E000B318A0 /* EcosiaHomeViewModelTests.swift in Sources */, E4CD9F1D1A6D9C2800318571 /* WebServerTests.swift in Sources */, C869916528918C8E007ACC5C /* WallpaperURLSessionMock.swift in Sources */, 961577942A39008100391E8D /* SponsoredTileDataUtilityTests.swift in Sources */, @@ -14508,7 +15496,6 @@ 213B67A827CE721E000542F5 /* StartAtHomeHelperTests.swift in Sources */, 8A13FA892AD82BC8007527AB /* AppSendTabDelegateTests.swift in Sources */, C8CD80D42A1E268C0097C3AE /* MockGleanPlumbEvaluationUtility.swift in Sources */, - 2C2349A32C57E5BC007A5894 /* EcosiaPerformanceTestHistory.swift in Sources */, 8A7A26E829D4C0FE00EA76F1 /* IntroScreenManagerTests.swift in Sources */, E1AEC17A286E0CF500062E29 /* WebViewNavigationHandlerTests.swift in Sources */, D3D488591ABB54CD00A93597 /* FileAccessorTests.swift in Sources */, @@ -14522,15 +15509,12 @@ C29B64872AD69D0200F3244B /* QRCodeCoordinatorTests.swift in Sources */, 0BA8964B1A250E6500C1010C /* ProfileTest.swift in Sources */, 8AE80BAF2891960300BC12EA /* MockTraitCollection.swift in Sources */, - 2C9A62C22CDE4A3B00CDA7D1 /* MockNewsModel.swift in Sources */, E19443F82AF953B000964EA5 /* MockSidebarEnabledView.swift in Sources */, 8A832A9729DCBD3C0025D5DD /* LaunchTypeTests.swift in Sources */, 8A28C628291028870078A81A /* CanRemoveQuickActionBookmarkTests.swift in Sources */, 03CCC9181AF05E7300DBF30D /* RelativeDatesTests.swift in Sources */, F84B21DA1A090F8100AAB793 /* ClientTests.swift in Sources */, - 2CABD7282C12EF1E00A0750F /* PrivateModeButtonTests.swift in Sources */, 219935F12B07DFA200E5966F /* TabDisplayPanelTests.swift in Sources */, - 2C16B7672CAF2441006118F8 /* UserDefaultsSeedProgressManagerTests.swift in Sources */, DF940A0C2A96352B00C1497D /* FakespotSettingsCardViewModelTests.swift in Sources */, 281B2BEA1ADF4D90002917DC /* MockProfile.swift in Sources */, 96AF8C1C29FC14F700EC2219 /* CreditCardInputFieldHelperTests.swift in Sources */, @@ -14587,7 +15571,6 @@ C81C66C429F00D1000F6422F /* UserActivityRouteTests.swift in Sources */, 8AFCE50529DDF38300B1B253 /* LaunchScreenViewControllerTests.swift in Sources */, D3FA777B1A43B2990010CD32 /* SearchTests.swift in Sources */, - 2C26EA142C04CAD100795552 /* EcosiaTopSitesHelperTests.swift in Sources */, D3BA41681BD82F2200DA5457 /* XCTestCaseExtensions.swift in Sources */, 8A86DAD8277298DE00D7BFFF /* ClosedTabsStoreTests.swift in Sources */, 21E78A7028F9A8C500F8D687 /* MockUIDevice.swift in Sources */, @@ -14606,7 +15589,6 @@ C2B808B12A77FA3F00A65487 /* DownloadsCoordinatorTests.swift in Sources */, 5A475E9129DB8AA7009C13FD /* MockDiskImageStore.swift in Sources */, DFA51484276103A000266AA0 /* HistoryHighlightsManagerTests.swift in Sources */, - 2C9A62C02CDE1F7600CDA7D1 /* MockWelcomeDelegate.swift in Sources */, C2506C952A6A8D2600F2B76E /* HistoryCoordinatorTests.swift in Sources */, 8ADAFAC628AEBF6300FFEBE3 /* HomeLogoHeaderViewModelTests.swift in Sources */, E14BF33E2950B1230039758D /* MailProvidersTests.swift in Sources */, @@ -14643,12 +15625,10 @@ 5AF6254728A58AC100A90253 /* MockHistoryHighlightsDataAdaptor.swift in Sources */, 8A7A26E329D4ACF300EA76F1 /* SceneCoordinatorTests.swift in Sources */, 39C137972655798A003DC662 /* NimbusIntegrationTests.swift in Sources */, - 2C7DBABD2C4EA37200BCD03F /* AppDelegateFeatureManagementIntegrationTests.swift in Sources */, 215B458427DA87FC00E5E800 /* TabMetadataManagerTests.swift in Sources */, 2173326A29CCF901007F20C7 /* UIPanGestureRecognizerMock.swift in Sources */, 5A9A09D628B01FD500B6F51E /* MockURLBarView.swift in Sources */, 8A33222227DFE658008F809E /* NimbusMock.swift in Sources */, - 2C728D7E2CBBDCDC00C7684B /* UnleashUserDefaultsSeedProgressManagerTests.swift in Sources */, 8A6E139E2A71C78A00A88FA8 /* GridTabViewControllerTests.swift in Sources */, 8A8629E72880B7330096DDB1 /* BookmarksPanelTests.swift in Sources */, C8B394362A0ED55D00700E49 /* MockOnboardingCardDelegate.swift in Sources */, @@ -14701,7 +15681,6 @@ 8A5604F629DF09FA00035CA3 /* MockLaunchCoordinatorDelegate.swift in Sources */, C807CCCC28367446008E6A5A /* FeatureFlagManagerTests.swift in Sources */, 8A5C3BC5282ABF8E003A8CCF /* LegacyRemoteTabsPanelTests.swift in Sources */, - 1285E2B52CC293CA0053506B /* AnalyticsSpyTests.swift in Sources */, 8AABBD032A001CBC0089941E /* MockApplicationHelper.swift in Sources */, 8A1E3BE328CBACDD003388C4 /* SponsoredContentFilterUtilityTests.swift in Sources */, 2173326829CCDA8E007F20C7 /* TabScrollControllerTests.swift in Sources */, @@ -14716,7 +15695,6 @@ A83E5B1D1C1DA8D80026D912 /* UIPasteboardExtensionsTests.swift in Sources */, 5AB4237C28A1947A003BC40C /* MockNotificationCenter.swift in Sources */, 8A37C79F28DA4BA600B1FAD4 /* ContextualHintViewProviderTests.swift in Sources */, - 2C9258D92CEF97B100C6BB8D /* MockUNNotificationSettings.swift in Sources */, 5A81C5DD2A4C981A00BE88C2 /* PasswordManagerCoordinatorTests.swift in Sources */, 8A355E5E27D267A400B9AF34 /* RecentItemsHelperTests.swift in Sources */, C8699153289177FB007ACC5C /* WallpaperDataServiceTests.swift in Sources */, @@ -14735,12 +15713,10 @@ D82ED2641FEB3C420059570B /* DefaultSearchPrefsTests.swift in Sources */, 1D74FF502B2797EA00FF01D0 /* WindowManagerTests.swift in Sources */, CA24B53B24ABFE5D0093848C /* PasswordManagerDataSourceHelperTests.swift in Sources */, - 2CD48B7F2C7F7E4100A70908 /* EcosiaOverlayModeManagerTests.swift in Sources */, E1390FB828B42EF200C9EF3E /* WallpaperManagerMock.swift in Sources */, ABB507CF2A136FB2009CAA67 /* UserConversionMetricsTests.swift in Sources */, 21FA8FB22AE856EB0013B815 /* MockTabTrayCoordinatorDelegate.swift in Sources */, C8699152289177F5007ACC5C /* WallpaperNetworkingTests.swift in Sources */, - 1265098A2CDA32790011BA36 /* BrazeServiceTests.swift in Sources */, E1AEC178286E0CF500062E29 /* HomepageViewControllerTests.swift in Sources */, 8ADEC6832A40F208002D2ED8 /* AppSettingsTableViewControllerTests.swift in Sources */, 5A475E8E29DB89C7009C13FD /* TabManagerTests.swift in Sources */, @@ -14777,7 +15753,10 @@ D04CD74D216CF86F004FF5B0 /* DevicePickerViewController.swift in Sources */, 2C6189E12B7B7922006B70D7 /* LegacyTheme.swift in Sources */, E13A72D9291154E600E9A99D /* ReusableCell.swift in Sources */, + 2CD266412CFF4FB200A040A7 /* LegacyThemeManager+Ecosia.swift in Sources */, + 2CD266432CFF4FC300A040A7 /* SemanticColor.swift in Sources */, 6025B10F267B6C7F00F59F6B /* LoginRecordExtension.swift in Sources */, + 2CD2663F2CFF4FA300A040A7 /* EcosiaTheme.swift in Sources */, E418D0D91A251B3200CAE47A /* Profile.swift in Sources */, E1CD81C3290C670A00124B27 /* HostingTableViewCell.swift in Sources */, DDA24A451FD84D630098F159 /* DefaultSearchPrefs.swift in Sources */, @@ -14785,13 +15764,11 @@ F8708D321A0970B70051AB07 /* ShareViewController.swift in Sources */, EB9407492081353100702E05 /* UXConstants.swift in Sources */, 2C6189E32B7B792A006B70D7 /* LegacyThemeManager.swift in Sources */, - 2C6189EA2B7B7979006B70D7 /* EcosiaTheme.swift in Sources */, F8B710A02ABE38980029726E /* RustErrors.swift in Sources */, E60D03271D511554002FE3F6 /* SyncDisplayState.swift in Sources */, - 2C6189D22B7A8D69006B70D7 /* EcosiaThemeColourPalette.swift in Sources */, - 2C6189D12B7A8D3E006B70D7 /* EcosiaThemeManager.swift in Sources */, + 2CD266472CFF4FE800A040A7 /* EcosiaThemeColourPalette.swift in Sources */, 210E0EBB298D9D6600BB4F33 /* OpenSearchEngine.swift in Sources */, - 2C6189E62B7B7952006B70D7 /* SemanticColor.swift in Sources */, + 2CD266452CFF4FD100A040A7 /* EcosiaThemeManager.swift in Sources */, 2128E27C2930216F00FB91BE /* SendToDeviceHelper.swift in Sources */, E136D41A2B19D35D003D0302 /* EmbeddedNavController.swift in Sources */, E1CD81C0290C5C9800124B27 /* DevicePickerTableViewCell.swift in Sources */, @@ -14799,7 +15776,6 @@ 2C6189E52B7B7946006B70D7 /* LegacyDarkTheme.swift in Sources */, E1CD81BF290C5C9500124B27 /* DevicePickerTableViewHeaderCell.swift in Sources */, 1D1933782AF2C8CE005089C9 /* AppEvent.swift in Sources */, - 2C26FAA92C8752D20055760A /* LegacyThemeManager+Ecosia.swift in Sources */, 1D969C722B2132B7004255B1 /* BrowserWindow.swift in Sources */, 1D1933752AF2C8C9005089C9 /* EventQueue.swift in Sources */, E41A7D4B1A1BE04500245963 /* InitialViewController.swift in Sources */, @@ -14814,6 +15790,11 @@ target = 047F9B2624E1FE1C00CD7DF7 /* WidgetKitExtension */; targetProxy = 047F9B3024E1FE1F00CD7DF7 /* PBXContainerItemProxy */; }; + 1229357E2CE78D0A00EC1297 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 122935692CE78D0A00EC1297 /* Ecosia */; + targetProxy = 1229357D2CE78D0A00EC1297 /* PBXContainerItemProxy */; + }; 2827316B1ABC9BE700AA1954 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 2827315D1ABC9BE600AA1954 /* Sync */; @@ -14834,11 +15815,21 @@ target = 2FCAE2191ABB51F800877008 /* Storage */; targetProxy = 2C1298A92BF5EE23005AE4E4 /* PBXContainerItemProxy */; }; + 2C21012D2D0B0FAF00CBE7EC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F84B21BD1A090F8100AAB793 /* Client */; + targetProxy = 2C21012C2D0B0FAF00CBE7EC /* PBXContainerItemProxy */; + }; 2C6C90872C614A17007D9B43 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F84B21BD1A090F8100AAB793 /* Client */; targetProxy = 2C6C90862C614A17007D9B43 /* PBXContainerItemProxy */; }; + 2CAF5C192D01B2E200D3DCDD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 122935692CE78D0A00EC1297 /* Ecosia */; + targetProxy = 2CAF5C182D01B2E200D3DCDD /* PBXContainerItemProxy */; + }; 2F11EE501ABCAE910083902D /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 288A2D851AB8B3260023ABC3 /* Shared */; @@ -15250,28 +16241,98 @@ name = Today.strings; sourceTree = ""; }; - 2C6188DB2B7A8A22006B70D7 /* Ecosia.strings */ = { + 2CD263C92CFDC76800A040A7 /* Ecosia.strings */ = { + isa = PBXVariantGroup; + children = ( + 2CD263C82CFDC76800A040A7 /* de */, + ); + name = Ecosia.strings; + sourceTree = ""; + }; + 2CD263CB2CFDC76800A040A7 /* Ecosia.strings */ = { + isa = PBXVariantGroup; + children = ( + 2CD263CA2CFDC76800A040A7 /* en */, + ); + name = Ecosia.strings; + sourceTree = ""; + }; + 2CD263CD2CFDC76800A040A7 /* Ecosia.strings */ = { + isa = PBXVariantGroup; + children = ( + 2CD263CC2CFDC76800A040A7 /* es */, + ); + name = Ecosia.strings; + sourceTree = ""; + }; + 2CD263CF2CFDC76800A040A7 /* Ecosia.strings */ = { + isa = PBXVariantGroup; + children = ( + 2CD263CE2CFDC76800A040A7 /* it */, + ); + name = Ecosia.strings; + sourceTree = ""; + }; + 2CD263D12CFDC76800A040A7 /* Ecosia.strings */ = { + isa = PBXVariantGroup; + children = ( + 2CD263D02CFDC76800A040A7 /* fr */, + ); + name = Ecosia.strings; + sourceTree = ""; + }; + 2CD263D32CFDC76800A040A7 /* Ecosia.strings */ = { isa = PBXVariantGroup; children = ( - 2C6188DC2B7A8A22006B70D7 /* de */, - 2C6188DF2B7A8A22006B70D7 /* en */, - 2C6188E12B7A8A22006B70D7 /* es */, - 2C6188E32B7A8A22006B70D7 /* it */, - 2C6188E92B7A8A22006B70D7 /* fr */, - 2C6188EB2B7A8A22006B70D7 /* nl */, + 2CD263D22CFDC76800A040A7 /* nl */, ); name = Ecosia.strings; sourceTree = ""; }; - 2C6188DD2B7A8A22006B70D7 /* Plurals.stringsdict */ = { + 2CD263D52CFDC76800A040A7 /* Plurals.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + 2CD263D42CFDC76800A040A7 /* de */, + ); + name = Plurals.stringsdict; + sourceTree = ""; + }; + 2CD263D72CFDC76800A040A7 /* Plurals.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + 2CD263D62CFDC76800A040A7 /* en */, + ); + name = Plurals.stringsdict; + sourceTree = ""; + }; + 2CD263D92CFDC76800A040A7 /* Plurals.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + 2CD263D82CFDC76800A040A7 /* es */, + ); + name = Plurals.stringsdict; + sourceTree = ""; + }; + 2CD263DB2CFDC76800A040A7 /* Plurals.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + 2CD263DA2CFDC76800A040A7 /* it */, + ); + name = Plurals.stringsdict; + sourceTree = ""; + }; + 2CD263DD2CFDC76800A040A7 /* Plurals.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + 2CD263DC2CFDC76800A040A7 /* fr */, + ); + name = Plurals.stringsdict; + sourceTree = ""; + }; + 2CD263DF2CFDC76800A040A7 /* Plurals.stringsdict */ = { isa = PBXVariantGroup; children = ( - 2C6188DE2B7A8A22006B70D7 /* de */, - 2C6188E02B7A8A22006B70D7 /* en */, - 2C6188E22B7A8A22006B70D7 /* es */, - 2C6188E42B7A8A22006B70D7 /* it */, - 2C6188EA2B7A8A22006B70D7 /* fr */, - 2C6188EC2B7A8A22006B70D7 /* nl */, + 2CD263DE2CFDC76800A040A7 /* nl */, ); name = Plurals.stringsdict; sourceTree = ""; @@ -19576,46 +20637,918 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 282731771ABC9BE800AA1954 /* Debug */ = { + 122935822CE78D0A00EC1297 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_ENTITLEMENTS = ""; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 33YMRSYD2L; - GCC_WARN_INHIBIT_ALL_WARNINGS = YES; - INFOPLIST_FILE = Sync/Info.plist; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Sync/Sync-Bridging-Header.h"; - }; - name = Debug; - }; - 2827317A1ABC9BE800AA1954 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = ""; - CODE_SIGN_IDENTITY = ""; - INFOPLIST_FILE = "firefox-ios/firefox-ios-tests/Tests/SyncTests/Info.plist"; - SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/firefox-ios/firefox-ios-tests/Tests/SyncTests/SyncTests-Bridging-Header.h"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + DYLIB_COMPATIBILITY_VERSION = ""; + DYLIB_CURRENT_VERSION = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Ecosia/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "$(MOZ_BUNDLE_DISPLAY_NAME)"; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCameraUsageDescription = "Firefox uses your camera to scan QR codes and take photos and video."; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Firefox requires Face ID to access your saved passwords and payment methods."; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Mozilla. All rights reserved."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Websites you visit may request your location."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Firefox uses your microphone to record and upload audio."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "This lets you save photos."; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = EcosiaLaunchScreen; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = armv7; + INFOPLIST_KEY_UIStatusBarHidden = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.ecosia.framework.Ecosia; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = ""; + VERSION_INFO_PREFIX = ""; }; name = Debug; }; - 288A2DA01AB8B3260023ABC3 /* Debug */ = { + 122935832CE78D0A00EC1297 /* BetaDebug */ = { isa = XCBuildConfiguration; buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_ENTITLEMENTS = ""; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 33YMRSYD2L; - GCC_WARN_INHIBIT_ALL_WARNINGS = YES; - INFOPLIST_FILE = "Shared/Supporting Files/Info.plist"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 33YMRSYD2L; + "DEVELOPMENT_TEAM[sdk=macosx*]" = 33YMRSYD2L; + DYLIB_COMPATIBILITY_VERSION = ""; + DYLIB_CURRENT_VERSION = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Ecosia/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "$(MOZ_BUNDLE_DISPLAY_NAME)"; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCameraUsageDescription = "Firefox uses your camera to scan QR codes and take photos and video."; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Firefox requires Face ID to access your saved passwords and payment methods."; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Mozilla. All rights reserved."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Websites you visit may request your location."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Firefox uses your microphone to record and upload audio."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "This lets you save photos."; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = EcosiaLaunchScreen; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = armv7; + INFOPLIST_KEY_UIStatusBarHidden = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.ecosia.framework.Ecosia; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = ""; + VERSION_INFO_PREFIX = ""; + }; + name = BetaDebug; + }; + 122935842CE78D0A00EC1297 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = ""; + DYLIB_CURRENT_VERSION = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Ecosia/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "$(MOZ_BUNDLE_DISPLAY_NAME)"; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCameraUsageDescription = "Firefox uses your camera to scan QR codes and take photos and video."; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Firefox requires Face ID to access your saved passwords and payment methods."; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Mozilla. All rights reserved."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Websites you visit may request your location."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Firefox uses your microphone to record and upload audio."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "This lets you save photos."; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = EcosiaLaunchScreen; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = armv7; + INFOPLIST_KEY_UIStatusBarHidden = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.ecosia.framework.Ecosia; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = ""; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 122935852CE78D0A00EC1297 /* Development_TestFlight */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = ""; + DYLIB_CURRENT_VERSION = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Ecosia/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "$(MOZ_BUNDLE_DISPLAY_NAME)"; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCameraUsageDescription = "Firefox uses your camera to scan QR codes and take photos and video."; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Firefox requires Face ID to access your saved passwords and payment methods."; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Mozilla. All rights reserved."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Websites you visit may request your location."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Firefox uses your microphone to record and upload audio."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "This lets you save photos."; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = EcosiaLaunchScreen; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = armv7; + INFOPLIST_KEY_UIStatusBarHidden = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.ecosia.framework.Ecosia; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = ""; + VERSION_INFO_PREFIX = ""; + }; + name = Development_TestFlight; + }; + 122935862CE78D0A00EC1297 /* Development_AppCenter */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = ""; + DYLIB_CURRENT_VERSION = ""; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Ecosia/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "$(MOZ_BUNDLE_DISPLAY_NAME)"; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + INFOPLIST_KEY_NSCameraUsageDescription = "Firefox uses your camera to scan QR codes and take photos and video."; + INFOPLIST_KEY_NSFaceIDUsageDescription = "Firefox requires Face ID to access your saved passwords and payment methods."; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 Mozilla. All rights reserved."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Websites you visit may request your location."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Firefox uses your microphone to record and upload audio."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "This lets you save photos."; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = EcosiaLaunchScreen; + INFOPLIST_KEY_UIRequiredDeviceCapabilities = armv7; + INFOPLIST_KEY_UIStatusBarHidden = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.ecosia.framework.Ecosia; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = ""; + VERSION_INFO_PREFIX = ""; + }; + name = Development_AppCenter; + }; + 122935882CE78D0A00EC1297 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 33YMRSYD2L; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.ecosia.framework.EcosiaTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Client"; + }; + name = Debug; + }; + 122935892CE78D0A00EC1297 /* BetaDebug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 33YMRSYD2L; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.ecosia.framework.EcosiaTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Client"; + VALIDATE_PRODUCT = YES; + }; + name = BetaDebug; + }; + 1229358A2CE78D0A00EC1297 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 33YMRSYD2L; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.ecosia.framework.EcosiaTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Client"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1229358B2CE78D0A00EC1297 /* Development_TestFlight */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 33YMRSYD2L; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.ecosia.framework.EcosiaTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Client"; + }; + name = Development_TestFlight; + }; + 1229358C2CE78D0A00EC1297 /* Development_AppCenter */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 33YMRSYD2L; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.ecosia.framework.EcosiaTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Client"; + }; + name = Development_AppCenter; + }; + 282731771ABC9BE800AA1954 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = ""; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 33YMRSYD2L; + GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + INFOPLIST_FILE = Sync/Info.plist; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/Sync/Sync-Bridging-Header.h"; + }; + name = Debug; + }; + 2827317A1ABC9BE800AA1954 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = ""; + CODE_SIGN_IDENTITY = ""; + INFOPLIST_FILE = "firefox-ios/firefox-ios-tests/Tests/SyncTests/Info.plist"; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/firefox-ios/firefox-ios-tests/Tests/SyncTests/SyncTests-Bridging-Header.h"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Client.app/Client"; + }; + name = Debug; + }; + 288A2DA01AB8B3260023ABC3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CODE_SIGN_ENTITLEMENTS = ""; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 33YMRSYD2L; + GCC_WARN_INHIBIT_ALL_WARNINGS = YES; + INFOPLIST_FILE = "Shared/Supporting Files/Info.plist"; LOCALIZED_STRING_MACRO_NAMES = ( MZLocalizedString, NSLocalizedString, @@ -19630,6 +21563,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 2CBCAB0B2B88EEE40080AD68 /* EcosiaBeta.xcconfig */; buildSettings = { + INFOPLIST_FILE = Client/Info.plist; OTHER_LDFLAGS = ( "$(inherited)", "-Xlinker", @@ -20517,6 +22451,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 2CBCAB0C2B88EEE40080AD68 /* Ecosia.xcconfig */; buildSettings = { + INFOPLIST_FILE = Client/Info.plist; OTHER_LDFLAGS = ( "$(inherited)", "-Xlinker", @@ -20764,6 +22699,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 2CBCAAFE2B88EEE40080AD68 /* EcosiaBetaDebug.xcconfig */; buildSettings = { + INFOPLIST_FILE = Client/Info.plist; OTHER_LDFLAGS = ( "$(inherited)", "-Xlinker", @@ -20997,6 +22933,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 2CBCAB0B2B88EEE40080AD68 /* EcosiaBeta.xcconfig */; buildSettings = { + INFOPLIST_FILE = Client/Info.plist; OTHER_LDFLAGS = ( "$(inherited)", "-Xlinker", @@ -21187,6 +23124,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 2CBCAB0E2B88EEE40080AD68 /* EcosiaDebug.xcconfig */; buildSettings = { + INFOPLIST_FILE = Client/Info.plist; OTHER_LDFLAGS = ( "$(inherited)", "-Xlinker", @@ -21270,6 +23208,30 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; }; + 122935812CE78D0A00EC1297 /* Build configuration list for PBXNativeTarget "Ecosia" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 122935822CE78D0A00EC1297 /* Debug */, + 122935832CE78D0A00EC1297 /* BetaDebug */, + 122935842CE78D0A00EC1297 /* Release */, + 122935852CE78D0A00EC1297 /* Development_TestFlight */, + 122935862CE78D0A00EC1297 /* Development_AppCenter */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 122935872CE78D0A00EC1297 /* Build configuration list for PBXNativeTarget "EcosiaTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 122935882CE78D0A00EC1297 /* Debug */, + 122935892CE78D0A00EC1297 /* BetaDebug */, + 1229358A2CE78D0A00EC1297 /* Release */, + 1229358B2CE78D0A00EC1297 /* Development_TestFlight */, + 1229358C2CE78D0A00EC1297 /* Development_AppCenter */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; 282731971ABC9BE800AA1954 /* Build configuration list for PBXNativeTarget "Sync" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -21493,14 +23455,6 @@ version = 2.0.0; }; }; - 2C61887D2B7A89E4006B70D7 /* XCRemoteSwiftPackageReference "ios-core" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ecosia/ios-core.git"; - requirement = { - branch = main; - kind = branch; - }; - }; 2C6189EF2B7B7D48006B70D7 /* XCRemoteSwiftPackageReference "snowplow-ios-tracker" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/snowplow/snowplow-ios-tracker.git"; @@ -21509,6 +23463,14 @@ minimumVersion = 6.0.8; }; }; + 2CAF5C1B2D01B3E200D3DCDD /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/scinfu/SwiftSoup"; + requirement = { + kind = exactVersion; + version = 2.5.3; + }; + }; 2CB172802C612D68008551E2 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing"; @@ -21592,6 +23554,21 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 122935902CE78ED500EC1297 /* BrazeKit */ = { + isa = XCSwiftPackageProductDependency; + package = 126509832CD925B30011BA36 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; + productName = BrazeKit; + }; + 122935922CE78ED500EC1297 /* BrazeUI */ = { + isa = XCSwiftPackageProductDependency; + package = 126509832CD925B30011BA36 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; + productName = BrazeUI; + }; + 122935B42CE79EF900EC1297 /* SnowplowTracker */ = { + isa = XCSwiftPackageProductDependency; + package = 2C6189EF2B7B7D48006B70D7 /* XCRemoteSwiftPackageReference "snowplow-ios-tracker" */; + productName = SnowplowTracker; + }; 126509842CD925B40011BA36 /* BrazeKit */ = { isa = XCSwiftPackageProductDependency; package = 126509832CD925B30011BA36 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; @@ -21611,15 +23588,10 @@ isa = XCSwiftPackageProductDependency; productName = Redux; }; - 2C1298AB2BF5EE3E005AE4E4 /* Core */ = { - isa = XCSwiftPackageProductDependency; - package = 2C61887D2B7A89E4006B70D7 /* XCRemoteSwiftPackageReference "ios-core" */; - productName = Core; - }; - 2C1F23BC2B9F405E00186F55 /* Core */ = { + 2C21012A2D0B0F3700CBE7EC /* SnowplowTracker */ = { isa = XCSwiftPackageProductDependency; - package = 2C61887D2B7A89E4006B70D7 /* XCRemoteSwiftPackageReference "ios-core" */; - productName = Core; + package = 2C6189EF2B7B7D48006B70D7 /* XCRemoteSwiftPackageReference "snowplow-ios-tracker" */; + productName = SnowplowTracker; }; 2C6189F02B7B7D5D006B70D7 /* SnowplowTracker */ = { isa = XCSwiftPackageProductDependency; @@ -21630,11 +23602,6 @@ isa = XCSwiftPackageProductDependency; productName = Common; }; - 2C69DA802C62459200D7F69F /* Core */ = { - isa = XCSwiftPackageProductDependency; - package = 2C61887D2B7A89E4006B70D7 /* XCRemoteSwiftPackageReference "ios-core" */; - productName = Core; - }; 2C6C908E2C614A6C007D9B43 /* SnapshotTesting */ = { isa = XCSwiftPackageProductDependency; package = 2CB172802C612D68008551E2 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; @@ -21645,25 +23612,19 @@ package = 2CCFB3D82C0FC4DC00BEDCA0 /* XCRemoteSwiftPackageReference "rust-components-swift" */; productName = MozillaAppServices; }; - 2CE294462B7CDD56006C22B2 /* Core */ = { - isa = XCSwiftPackageProductDependency; - package = 2C61887D2B7A89E4006B70D7 /* XCRemoteSwiftPackageReference "ios-core" */; - productName = Core; - }; - 2CE294482B7CDD78006C22B2 /* Core */ = { + 2CAF5C1C2D01B40B00D3DCDD /* SwiftSoup */ = { isa = XCSwiftPackageProductDependency; - package = 2C61887D2B7A89E4006B70D7 /* XCRemoteSwiftPackageReference "ios-core" */; - productName = Core; + package = 2CAF5C1B2D01B3E200D3DCDD /* XCRemoteSwiftPackageReference "SwiftSoup" */; + productName = SwiftSoup; }; - 2CE2E24C2B9B1FCB00973C16 /* Core */ = { + 2CAF5C1E2D01B44400D3DCDD /* Sentry */ = { isa = XCSwiftPackageProductDependency; - package = 2C61887D2B7A89E4006B70D7 /* XCRemoteSwiftPackageReference "ios-core" */; - productName = Core; + package = 5A37861729A2C337006B3A34 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; }; - 2CF4DA622BB31970001C340A /* Core */ = { + 2CAF5C202D01B5F300D3DCDD /* Common */ = { isa = XCSwiftPackageProductDependency; - package = 2C61887D2B7A89E4006B70D7 /* XCRemoteSwiftPackageReference "ios-core" */; - productName = Core; + productName = Common; }; 432BD0232790EBD000A0F3C3 /* Adjust */ = { isa = XCSwiftPackageProductDependency; diff --git a/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 067f83af1a6b..1fd0d7c12023 100644 --- a/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/braze-inc/braze-swift-sdk", "state" : { - "revision" : "36481467bc4f333a0a2e31bcdbf981ad8e526dd0", - "version" : "11.2.0" + "revision" : "fc575bee323c5ce58e0608b0168fdc3ac8727443", + "version" : "11.4.0" } }, { @@ -54,15 +54,6 @@ "version" : "56.0.0" } }, - { - "identity" : "ios-core", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ecosia/ios-core.git", - "state" : { - "branch" : "main", - "revision" : "def9d182ab7c7fd65b092ab5d8044811ac1a3826" - } - }, { "identity" : "ios_sdk", "kind" : "remoteSourceControl", @@ -140,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/snowplow/snowplow-ios-tracker.git", "state" : { - "revision" : "bf1495987d63dc3595270df1a1bb516bfef8585f", - "version" : "6.0.8" + "revision" : "c105d9f8b875f2442512c3c8b1c8bbe9c209a302", + "version" : "6.0.9" } }, { @@ -167,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/scinfu/SwiftSoup", "state" : { - "branch" : "2.5.3", - "revision" : "f707b8680cddb96dc1855632340a572ef37bbb98" + "revision" : "f707b8680cddb96dc1855632340a572ef37bbb98", + "version" : "2.5.3" } }, { diff --git a/Client.xcodeproj/xcshareddata/xcschemes/Ecosia.xcscheme b/Client.xcodeproj/xcshareddata/xcschemes/Ecosia.xcscheme index e6327e4a6ea9..e254c806c253 100644 --- a/Client.xcodeproj/xcshareddata/xcschemes/Ecosia.xcscheme +++ b/Client.xcodeproj/xcshareddata/xcschemes/Ecosia.xcscheme @@ -136,20 +136,6 @@ ReferencedContainer = "container:FxA/FxA.xcodeproj"> - - - - - - - - - - - - + skipped = "YES"> + + + + + + + + diff --git a/Client.xcodeproj/xcshareddata/xcschemes/EcosiaFramework.xcscheme b/Client.xcodeproj/xcshareddata/xcschemes/EcosiaFramework.xcscheme new file mode 100644 index 000000000000..6d2d74506b81 --- /dev/null +++ b/Client.xcodeproj/xcshareddata/xcschemes/EcosiaFramework.xcscheme @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Client.xcodeproj/xcshareddata/xcschemes/EcosiaSnapshotTests.xcscheme b/Client.xcodeproj/xcshareddata/xcschemes/EcosiaSnapshotTests.xcscheme index a08761bc7f27..4efd68f4fb8d 100644 --- a/Client.xcodeproj/xcshareddata/xcschemes/EcosiaSnapshotTests.xcscheme +++ b/Client.xcodeproj/xcshareddata/xcschemes/EcosiaSnapshotTests.xcscheme @@ -14,7 +14,7 @@ shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/Client/Application/AppDelegate.swift b/Client/Application/AppDelegate.swift index 69e49f66f9a9..f45b1d540ef5 100644 --- a/Client/Application/AppDelegate.swift +++ b/Client/Application/AppDelegate.swift @@ -9,8 +9,7 @@ import UIKit import Common // Ecosia: remove Glean dependency // import Glean import TabDataStore -// Ecosia: Import Core -import Core +import Ecosia class AppDelegate: UIResponder, UIApplicationDelegate { let logger = DefaultLogger.shared diff --git a/Client/Configuration/Ecosia.ShareTo.xcconfig b/Client/Configuration/Ecosia.ShareTo.xcconfig index 9591255634c9..8d92c0357c42 100644 --- a/Client/Configuration/Ecosia.ShareTo.xcconfig +++ b/Client/Configuration/Ecosia.ShareTo.xcconfig @@ -5,6 +5,6 @@ #include "Ecosia.xcconfig" PROVISIONING_PROFILE_SPECIFIER = match AdHoc com.ecosia.ecosiaapp.ShareTo -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/AppExtensions/Ecosia.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/AppExtensions/Ecosia.entitlements MOZ_BUNDLE_ID = com.ecosia.ecosiaapp PRODUCT_BUNDLE_IDENTIFIER = $(MOZ_BUNDLE_ID).$(PRODUCT_NAME) diff --git a/Client/Configuration/Ecosia.WidgetKit.xcconfig b/Client/Configuration/Ecosia.WidgetKit.xcconfig index 51b6520c29cb..d35b0b3061ac 100644 --- a/Client/Configuration/Ecosia.WidgetKit.xcconfig +++ b/Client/Configuration/Ecosia.WidgetKit.xcconfig @@ -6,6 +6,6 @@ ECOSIA_PRODUCT_NAME = WidgetKit PROVISIONING_PROFILE_SPECIFIER = match AdHoc com.ecosia.ecosiaapp.WidgetKit -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/AppExtensions/Ecosia.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/AppExtensions/Ecosia.entitlements MOZ_BUNDLE_ID = com.ecosia.ecosiaapp PRODUCT_BUNDLE_IDENTIFIER = $(MOZ_BUNDLE_ID).$(ECOSIA_PRODUCT_NAME) diff --git a/Client/Configuration/Ecosia.xcconfig b/Client/Configuration/Ecosia.xcconfig index 5764bbf47b42..e37b8da43b03 100644 --- a/Client/Configuration/Ecosia.xcconfig +++ b/Client/Configuration/Ecosia.xcconfig @@ -8,7 +8,7 @@ MOZ_BUNDLE_DISPLAY_NAME = Ecosia MOZ_BUNDLE_ID = com.ecosia.ecosiaapp -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/Ecosia.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/Ecosia.entitlements OTHER_SWIFT_FLAGS = $(OTHER_SWIFT_FLAGS_common) -DMOZ_CHANNEL_RELEASE MOZ_INTERNAL_URL_SCHEME = ecosia DEVELOPMENT_TEAM = 33YMRSYD2L diff --git a/Client/Configuration/EcosiaBeta.ShareTo.xcconfig b/Client/Configuration/EcosiaBeta.ShareTo.xcconfig index 8fe469541dc8..c67747edf897 100644 --- a/Client/Configuration/EcosiaBeta.ShareTo.xcconfig +++ b/Client/Configuration/EcosiaBeta.ShareTo.xcconfig @@ -6,5 +6,5 @@ PROVISIONING_PROFILE_SPECIFIER = match AdHoc com.ecosia.ecosiaapp.firefox.ShareTo PRODUCT_BUNDLE_IDENTIFIER = $(MOZ_BUNDLE_ID).$(PRODUCT_NAME) -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements MOZ_BUNDLE_ID = com.ecosia.ecosiaapp.firefox diff --git a/Client/Configuration/EcosiaBeta.WidgetKit.xcconfig b/Client/Configuration/EcosiaBeta.WidgetKit.xcconfig index 0b7ed6352bd6..07a3b69401de 100644 --- a/Client/Configuration/EcosiaBeta.WidgetKit.xcconfig +++ b/Client/Configuration/EcosiaBeta.WidgetKit.xcconfig @@ -6,5 +6,5 @@ ECOSIA_PRODUCT_NAME = WidgetKit PROVISIONING_PROFILE_SPECIFIER = match AdHoc com.ecosia.ecosiaapp.firefox.WidgetKit -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements PRODUCT_BUNDLE_IDENTIFIER = $(MOZ_BUNDLE_ID).$(ECOSIA_PRODUCT_NAME) diff --git a/Client/Configuration/EcosiaBeta.xcconfig b/Client/Configuration/EcosiaBeta.xcconfig index f1ef06bfde39..79d580484b96 100644 --- a/Client/Configuration/EcosiaBeta.xcconfig +++ b/Client/Configuration/EcosiaBeta.xcconfig @@ -10,7 +10,7 @@ MOZ_BUNDLE_DISPLAY_NAME = Ecosia Beta MOZ_BUNDLE_ID = com.ecosia.ecosiaapp.firefox -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/EcosiaBeta.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/EcosiaBeta.entitlements OTHER_SWIFT_FLAGS = $(OTHER_SWIFT_FLAGS_common) -DMOZ_CHANNEL_BETA MOZ_INTERNAL_URL_SCHEME = ecosia DEVELOPMENT_TEAM = 33YMRSYD2L diff --git a/Client/Configuration/EcosiaBetaDebug.ShareTo.xcconfig b/Client/Configuration/EcosiaBetaDebug.ShareTo.xcconfig index 651cc6e72cb8..84e0889a17ed 100644 --- a/Client/Configuration/EcosiaBetaDebug.ShareTo.xcconfig +++ b/Client/Configuration/EcosiaBetaDebug.ShareTo.xcconfig @@ -5,6 +5,6 @@ #include "EcosiaBetaDebug.xcconfig" PROVISIONING_PROFILE_SPECIFIER = match Development com.ecosia.ecosiaapp.firefox.ShareTo -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements MOZ_BUNDLE_ID = com.ecosia.ecosiaapp.firefox PRODUCT_BUNDLE_IDENTIFIER = $(MOZ_BUNDLE_ID).$(PRODUCT_NAME) diff --git a/Client/Configuration/EcosiaBetaDebug.WidgetKit.xcconfig b/Client/Configuration/EcosiaBetaDebug.WidgetKit.xcconfig index 0f0f76cb8d93..933452864191 100644 --- a/Client/Configuration/EcosiaBetaDebug.WidgetKit.xcconfig +++ b/Client/Configuration/EcosiaBetaDebug.WidgetKit.xcconfig @@ -6,5 +6,5 @@ ECOSIA_PRODUCT_NAME = WidgetKit PROVISIONING_PROFILE_SPECIFIER = match Development com.ecosia.ecosiaapp.firefox.WidgetKit -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements PRODUCT_BUNDLE_IDENTIFIER = $(MOZ_BUNDLE_ID).$(ECOSIA_PRODUCT_NAME) diff --git a/Client/Configuration/EcosiaBetaDebug.xcconfig b/Client/Configuration/EcosiaBetaDebug.xcconfig index 5be6da645a84..336bd8b33139 100644 --- a/Client/Configuration/EcosiaBetaDebug.xcconfig +++ b/Client/Configuration/EcosiaBetaDebug.xcconfig @@ -14,7 +14,7 @@ MOZ_BUNDLE_ID = com.ecosia.ecosiaapp.firefox INCLUDE_SETTINGS_BUNDLE = YES LEANPLUM_ENVIRONMENT = development MOZ_TODAY_WIDGET_SEARCH_DISPLAY_NAME = Ecosia - Search -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/EcosiaBeta.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/EcosiaBeta.entitlements OTHER_SWIFT_FLAGS = $(OTHER_SWIFT_FLAGS_common) -DMOZ_CHANNEL_FENNEC MOZ_INTERNAL_URL_SCHEME = ecosia OTHER_LDFLAGS = -ObjC -lxml2 -fprofile-instr-generate diff --git a/Client/Configuration/EcosiaDebug.ShareTo.xcconfig b/Client/Configuration/EcosiaDebug.ShareTo.xcconfig index 91a515e4ad6c..9422367b8577 100644 --- a/Client/Configuration/EcosiaDebug.ShareTo.xcconfig +++ b/Client/Configuration/EcosiaDebug.ShareTo.xcconfig @@ -3,6 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. PROVISIONING_PROFILE_SPECIFIER = match Development com.ecosia.ecosiaapp.ShareTo -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/AppExtensions/Ecosia.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/AppExtensions/Ecosia.entitlements MOZ_BUNDLE_ID = com.ecosia.ecosiaapp PRODUCT_BUNDLE_IDENTIFIER = $(MOZ_BUNDLE_ID).$(PRODUCT_NAME) diff --git a/Client/Configuration/EcosiaDebug.WidgetKit.xcconfig b/Client/Configuration/EcosiaDebug.WidgetKit.xcconfig index b02d283b8b50..3c7c79d3ea9e 100644 --- a/Client/Configuration/EcosiaDebug.WidgetKit.xcconfig +++ b/Client/Configuration/EcosiaDebug.WidgetKit.xcconfig @@ -6,5 +6,5 @@ ECOSIA_PRODUCT_NAME = WidgetKit PROVISIONING_PROFILE_SPECIFIER = match Development com.ecosia.ecosiaapp.WidgetKit -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/AppExtensions/Ecosia.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/AppExtensions/Ecosia.entitlements PRODUCT_BUNDLE_IDENTIFIER = $(MOZ_BUNDLE_ID).$(ECOSIA_PRODUCT_NAME) diff --git a/Client/Configuration/EcosiaDebug.xcconfig b/Client/Configuration/EcosiaDebug.xcconfig index a26937361b9d..eacb6ca54948 100644 --- a/Client/Configuration/EcosiaDebug.xcconfig +++ b/Client/Configuration/EcosiaDebug.xcconfig @@ -15,7 +15,7 @@ MOZ_BUNDLE_ID = com.ecosia.ecosiaapp INCLUDE_SETTINGS_BUNDLE = YES LEANPLUM_ENVIRONMENT = development MOZ_TODAY_WIDGET_SEARCH_DISPLAY_NAME = Ecosia - Search -CODE_SIGN_ENTITLEMENTS = Client/Ecosia/Entitlements/Ecosia.entitlements +CODE_SIGN_ENTITLEMENTS = Ecosia/Entitlements/Ecosia.entitlements OTHER_SWIFT_FLAGS = $(OTHER_SWIFT_FLAGS_common) -DMOZ_CHANNEL_FENNEC MOZ_INTERNAL_URL_SCHEME = ecosia OTHER_LDFLAGS = -ObjC -lxml2 -fprofile-instr-generate diff --git a/Client/Coordinators/Browser/BrowserCoordinator.swift b/Client/Coordinators/Browser/BrowserCoordinator.swift index 0b3336e35dd2..a52c5b37149a 100644 --- a/Client/Coordinators/Browser/BrowserCoordinator.swift +++ b/Client/Coordinators/Browser/BrowserCoordinator.swift @@ -9,8 +9,7 @@ import Shared import Storage import Redux import TabDataStore -// Ecosia: Import Core -import Core +import Ecosia class BrowserCoordinator: BaseCoordinator, LaunchCoordinatorDelegate, diff --git a/Client/Coordinators/Launch/LaunchCoordinator.swift b/Client/Coordinators/Launch/LaunchCoordinator.swift index bdb1121b295d..b66b7b7bd06e 100644 --- a/Client/Coordinators/Launch/LaunchCoordinator.swift +++ b/Client/Coordinators/Launch/LaunchCoordinator.swift @@ -4,6 +4,7 @@ import Common import Foundation +import Ecosia protocol LaunchCoordinatorDelegate: AnyObject { func didFinishLaunch(from coordinator: LaunchCoordinator) diff --git a/Client/Coordinators/LaunchView/LaunchScreenViewController.swift b/Client/Coordinators/LaunchView/LaunchScreenViewController.swift index 409eb9cffbb9..5b3079568d91 100644 --- a/Client/Coordinators/LaunchView/LaunchScreenViewController.swift +++ b/Client/Coordinators/LaunchView/LaunchScreenViewController.swift @@ -4,6 +4,7 @@ import Common import Foundation +import Ecosia class LaunchScreenViewController: UIViewController, LaunchFinishedLoadingDelegate { // Ecosia: Custom launch screen diff --git a/Client/Coordinators/Router/RouteBuilder.swift b/Client/Coordinators/Router/RouteBuilder.swift index eedbe4749b5c..cd85ae3a66dd 100644 --- a/Client/Coordinators/Router/RouteBuilder.swift +++ b/Client/Coordinators/Router/RouteBuilder.swift @@ -5,6 +5,7 @@ import Foundation import CoreSpotlight import Shared +import Ecosia final class RouteBuilder { private var isPrivate = false diff --git a/Client/Coordinators/Scene/SceneCoordinator.swift b/Client/Coordinators/Scene/SceneCoordinator.swift index 3b6f2f979d94..97d7577a511d 100644 --- a/Client/Coordinators/Scene/SceneCoordinator.swift +++ b/Client/Coordinators/Scene/SceneCoordinator.swift @@ -6,8 +6,7 @@ import Common import UIKit import Shared import Storage -// Ecosia: Import Core -import Core +import Ecosia /// Each scene has it's own scene coordinator, which is the root coordinator for a scene. class SceneCoordinator: BaseCoordinator, LaunchCoordinatorDelegate, LaunchFinishedLoadingDelegate { diff --git a/Client/Ecosia/Bookmarks/BookmarksExchange.swift b/Client/Ecosia/Bookmarks/BookmarksExchange.swift index e8ef1de5c92b..b1bc4ba75f04 100644 --- a/Client/Ecosia/Bookmarks/BookmarksExchange.swift +++ b/Client/Ecosia/Bookmarks/BookmarksExchange.swift @@ -3,12 +3,12 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core import Shared import Storage +import Ecosia protocol BookmarksExchangable { - func export(bookmarks: [Core.BookmarkItem], in viewController: UIViewController, barButtonItem: UIBarButtonItem) async throws + func export(bookmarks: [Ecosia.BookmarkItem], in viewController: UIViewController, barButtonItem: UIBarButtonItem) async throws func `import`(from fileURL: URL, in viewController: UIViewController) async throws } @@ -31,7 +31,7 @@ final class BookmarksExchange: BookmarksExchangable { } @MainActor - func export(bookmarks: [Core.BookmarkItem], in viewController: UIViewController, barButtonItem: UIBarButtonItem) async throws { + func export(bookmarks: [Ecosia.BookmarkItem], in viewController: UIViewController, barButtonItem: UIBarButtonItem) async throws { guard let view = viewController.view else { return } let activityIndicator = UIActivityIndicatorView(style: .medium) @@ -99,7 +99,7 @@ final class BookmarksExchange: BookmarksExchangable { } private func importBookmarks( - _ bookmarks: [Core.BookmarkItem], + _ bookmarks: [Ecosia.BookmarkItem], viewController: UIViewController, toast: SimpleToast ) async throws { @@ -145,7 +145,7 @@ final class BookmarksExchange: BookmarksExchangable { } } - private func processBookmarks(_ bookmarks: [Core.BookmarkItem], parentGUID: GUID) async throws { + private func processBookmarks(_ bookmarks: [Ecosia.BookmarkItem], parentGUID: GUID) async throws { for bookmark in bookmarks { switch bookmark { case let .folder(title, children, _): diff --git a/Client/Ecosia/Experiments/Unleash/SeedCounterNTPExperiment.swift b/Client/Ecosia/Experiments/Unleash/SeedCounterNTPExperiment.swift index 15d73761dbcd..d729804d1a76 100644 --- a/Client/Ecosia/Experiments/Unleash/SeedCounterNTPExperiment.swift +++ b/Client/Ecosia/Experiments/Unleash/SeedCounterNTPExperiment.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core +import Ecosia struct SeedCounterNTPExperiment { private enum Variant: String { diff --git a/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift b/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift index 4331fb567ed0..0623b91cb5ef 100644 --- a/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift +++ b/Client/Ecosia/Extensions/AppSettingsTableViewController+Ecosia.swift @@ -5,6 +5,7 @@ import Common import UIKit import Shared +import Ecosia extension AppSettingsTableViewController { diff --git a/Client/Ecosia/Extensions/BrowserCoordinator+Ecosia.swift b/Client/Ecosia/Extensions/BrowserCoordinator+Ecosia.swift index 467108a2bde9..d86d4279128b 100644 --- a/Client/Ecosia/Extensions/BrowserCoordinator+Ecosia.swift +++ b/Client/Ecosia/Extensions/BrowserCoordinator+Ecosia.swift @@ -4,6 +4,5 @@ import Foundation import Shared -import Core extension BrowserCoordinator { } diff --git a/Client/Ecosia/Extensions/BrowserViewController+Ecosia.swift b/Client/Ecosia/Extensions/BrowserViewController+Ecosia.swift index c44379ff1c52..6197ad2402ff 100644 --- a/Client/Ecosia/Extensions/BrowserViewController+Ecosia.swift +++ b/Client/Ecosia/Extensions/BrowserViewController+Ecosia.swift @@ -3,8 +3,8 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core import Shared +import Ecosia extension BrowserViewController: HomepageViewControllerDelegate { func homeDidTapSearchButton(_ home: HomepageViewController) { diff --git a/Client/Ecosia/Extensions/HomepageViewController+Ecosia.swift b/Client/Ecosia/Extensions/HomepageViewController+Ecosia.swift index ea5ad4abee40..2369498987df 100644 --- a/Client/Ecosia/Extensions/HomepageViewController+Ecosia.swift +++ b/Client/Ecosia/Extensions/HomepageViewController+Ecosia.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core +import Ecosia protocol HomepageViewControllerDelegate: AnyObject { func homeDidTapSearchButton(_ home: HomepageViewController) diff --git a/Client/Ecosia/Extensions/Toolbar+URLBar/ConnectionStatusImage.swift b/Client/Ecosia/Extensions/Toolbar+URLBar/ConnectionStatusImage.swift index f0b58927d67b..f10587ebfd63 100644 --- a/Client/Ecosia/Extensions/Toolbar+URLBar/ConnectionStatusImage.swift +++ b/Client/Ecosia/Extensions/Toolbar+URLBar/ConnectionStatusImage.swift @@ -2,7 +2,7 @@ // 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 Foundation +import UIKit struct ConnectionStatusImage { diff --git a/Client/Ecosia/Extensions/UIImage+Ecosia.swift b/Client/Ecosia/Extensions/UIImage+Ecosia.swift new file mode 100644 index 000000000000..bafb63b3c673 --- /dev/null +++ b/Client/Ecosia/Extensions/UIImage+Ecosia.swift @@ -0,0 +1,12 @@ +// 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 Foundation + +extension UIImage { + convenience init?(themed name: String) { + let suffix = LegacyThemeManager.instance.current.isDark ? "Dark" : "" + self.init(named: name + suffix) + } +} diff --git a/Client/Ecosia/Settings/EcosiaDebugSettings.swift b/Client/Ecosia/Settings/EcosiaDebugSettings.swift index 8efd4a0190a9..34c2de9f0de1 100644 --- a/Client/Ecosia/Settings/EcosiaDebugSettings.swift +++ b/Client/Ecosia/Settings/EcosiaDebugSettings.swift @@ -4,9 +4,9 @@ import Foundation import UIKit -import Core import Shared import Common +import Ecosia final class PushBackInstallation: HiddenSetting { override var title: NSAttributedString? { diff --git a/Client/Ecosia/Settings/EcosiaSettings.swift b/Client/Ecosia/Settings/EcosiaSettings.swift index 384453d591b9..04f6aaf1bf64 100644 --- a/Client/Ecosia/Settings/EcosiaSettings.swift +++ b/Client/Ecosia/Settings/EcosiaSettings.swift @@ -3,9 +3,9 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core import Shared import Common +import Ecosia var ecosiaDisclosureIndicator: UIImageView { let config = UIImage.SymbolConfiguration(pointSize: 16) diff --git a/Client/Ecosia/Settings/NTPCustomizationSettingsViewController.swift b/Client/Ecosia/Settings/NTPCustomizationSettingsViewController.swift index 1cb207094a6c..187884fd166c 100644 --- a/Client/Ecosia/Settings/NTPCustomizationSettingsViewController.swift +++ b/Client/Ecosia/Settings/NTPCustomizationSettingsViewController.swift @@ -5,6 +5,7 @@ import Foundation import Shared import Common +import Ecosia final class NTPCustomizationSettingsViewController: SettingsTableViewController { init() { diff --git a/Client/Ecosia/UI/EcosiaPrimaryButton.swift b/Client/Ecosia/UI/EcosiaPrimaryButton.swift new file mode 100644 index 000000000000..ba561a397107 --- /dev/null +++ b/Client/Ecosia/UI/EcosiaPrimaryButton.swift @@ -0,0 +1,31 @@ +// 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 UIKit + +class EcosiaPrimaryButton: UIButton { + override var isSelected: Bool { + get { + return super.isSelected + } + set { + super.isSelected = newValue + update() + } + } + + override var isHighlighted: Bool { + get { + return super.isHighlighted + } + set { + super.isHighlighted = newValue + update() + } + } + + private func update() { + backgroundColor = (isSelected || isHighlighted) ? .legacyTheme.ecosia.primaryButtonActive : .legacyTheme.ecosia.primaryButton + } +} diff --git a/Client/Ecosia/UI/EmptyBookmarksView.swift b/Client/Ecosia/UI/EmptyBookmarksView.swift index ff9ba0cfcf32..cd00c309d5a8 100644 --- a/Client/Ecosia/UI/EmptyBookmarksView.swift +++ b/Client/Ecosia/UI/EmptyBookmarksView.swift @@ -4,6 +4,7 @@ import UIKit import Common +import Ecosia final class EmptyBookmarksView: UIView, Themeable { diff --git a/Client/Ecosia/UI/FilterController.swift b/Client/Ecosia/UI/FilterController.swift index 5b253d17965a..d109e00c1c38 100644 --- a/Client/Ecosia/UI/FilterController.swift +++ b/Client/Ecosia/UI/FilterController.swift @@ -2,7 +2,7 @@ // 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 Core +import Ecosia import UIKit import Common diff --git a/Client/Ecosia/LaunchScreen/EcosiaLaunchScreen.xib b/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreen.xib similarity index 100% rename from Client/Ecosia/LaunchScreen/EcosiaLaunchScreen.xib rename to Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreen.xib diff --git a/Client/Ecosia/LaunchScreen/EcosiaLaunchScreenView.swift b/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreenView.swift similarity index 72% rename from Client/Ecosia/LaunchScreen/EcosiaLaunchScreenView.swift rename to Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreenView.swift index 094e5e1a19b0..d468aa5a0b05 100644 --- a/Client/Ecosia/LaunchScreen/EcosiaLaunchScreenView.swift +++ b/Client/Ecosia/UI/LaunchScreen/EcosiaLaunchScreenView.swift @@ -4,11 +4,11 @@ import UIKit -/// LaunchScreen is the LaunchScreen.xib we show at launch, but loaded programmatically -class EcosiaLaunchScreenView: UIView { +/// LaunchScreen is the EcosiaLaunchScreen.xib we show at launch, but loaded programmatically +public class EcosiaLaunchScreenView: UIView { private static let viewName = "EcosiaLaunchScreen" - class func fromNib() -> UIView { + public class func fromNib() -> UIView { return Bundle.main.loadNibNamed(EcosiaLaunchScreenView.viewName, owner: nil, options: nil)![0] as! UIView diff --git a/Client/Ecosia/UI/LoadingScreen.swift b/Client/Ecosia/UI/LoadingScreen.swift index 63f5d4a8d8aa..10ea4cfa166a 100644 --- a/Client/Ecosia/UI/LoadingScreen.swift +++ b/Client/Ecosia/UI/LoadingScreen.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core +import Ecosia final class LoadingScreen: UIViewController { private weak var profile: Profile! diff --git a/Client/Ecosia/UI/MarketsController.swift b/Client/Ecosia/UI/MarketsController.swift index cf1f2bb1eb1b..8d5aa617586c 100644 --- a/Client/Ecosia/UI/MarketsController.swift +++ b/Client/Ecosia/UI/MarketsController.swift @@ -2,9 +2,9 @@ // 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 Core import UIKit import Common +import Ecosia final class Markets { private(set) static var all: [Market] = { diff --git a/Client/Ecosia/UI/MultiplyImpact/MultiplyImpact.swift b/Client/Ecosia/UI/MultiplyImpact/MultiplyImpact.swift index 37ffb6e5c2cb..91f13c49c178 100644 --- a/Client/Ecosia/UI/MultiplyImpact/MultiplyImpact.swift +++ b/Client/Ecosia/UI/MultiplyImpact/MultiplyImpact.swift @@ -3,11 +3,11 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core import UniformTypeIdentifiers import MobileCoreServices import LinkPresentation import Common +import Ecosia class MultiplyImpact: UIViewController, Themeable { diff --git a/Client/Ecosia/UI/NTP/AboutEcosia/AboutEcosiaSection.swift b/Client/Ecosia/UI/NTP/AboutEcosia/AboutEcosiaSection.swift index a9cde8ee00b6..db6df8d844ca 100644 --- a/Client/Ecosia/UI/NTP/AboutEcosia/AboutEcosiaSection.swift +++ b/Client/Ecosia/UI/NTP/AboutEcosia/AboutEcosiaSection.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core +import Ecosia enum AboutEcosiaSection: Int, CaseIterable { case diff --git a/Client/Ecosia/UI/NTP/AboutEcosia/NTPAboutEcosiaCell.swift b/Client/Ecosia/UI/NTP/AboutEcosia/NTPAboutEcosiaCell.swift index f0bfb32996ec..9e0ced1d4c86 100644 --- a/Client/Ecosia/UI/NTP/AboutEcosia/NTPAboutEcosiaCell.swift +++ b/Client/Ecosia/UI/NTP/AboutEcosia/NTPAboutEcosiaCell.swift @@ -4,6 +4,7 @@ import Foundation import Common +import Ecosia final class NTPAboutEcosiaCell: UICollectionViewCell, ReusableCell { struct UX { diff --git a/Client/Ecosia/UI/NTP/AboutEcosia/NTPAboutEcosiaCellViewModel.swift b/Client/Ecosia/UI/NTP/AboutEcosia/NTPAboutEcosiaCellViewModel.swift index b2a93ce4ba46..abe87f3e319d 100644 --- a/Client/Ecosia/UI/NTP/AboutEcosia/NTPAboutEcosiaCellViewModel.swift +++ b/Client/Ecosia/UI/NTP/AboutEcosia/NTPAboutEcosiaCellViewModel.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core +import Ecosia import Common typealias NTPAboutEcosiaCellDelegate = SharedHomepageCellDelegate & SharedHomepageCellLayoutDelegate diff --git a/Client/Ecosia/UI/NTP/ClimateImpactCounter/NTPSeedCounterCell.swift b/Client/Ecosia/UI/NTP/ClimateImpactCounter/NTPSeedCounterCell.swift index e58f7ddf89a2..f14924bb5105 100644 --- a/Client/Ecosia/UI/NTP/ClimateImpactCounter/NTPSeedCounterCell.swift +++ b/Client/Ecosia/UI/NTP/ClimateImpactCounter/NTPSeedCounterCell.swift @@ -4,7 +4,6 @@ import UIKit import SwiftUI -import Core import Common protocol NTPSeedCounterDelegate: NSObjectProtocol { diff --git a/Client/Ecosia/UI/NTP/ClimateImpactCounter/SeedCounterHiddenSettings.swift b/Client/Ecosia/UI/NTP/ClimateImpactCounter/SeedCounterHiddenSettings.swift index f3a27d356c6f..1c19c3287282 100644 --- a/Client/Ecosia/UI/NTP/ClimateImpactCounter/SeedCounterHiddenSettings.swift +++ b/Client/Ecosia/UI/NTP/ClimateImpactCounter/SeedCounterHiddenSettings.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core +import Ecosia final class AddOneSeedSetting: HiddenSetting { diff --git a/Client/Ecosia/UI/NTP/Customization/CustomizableNTPSettingConfig.swift b/Client/Ecosia/UI/NTP/Customization/CustomizableNTPSettingConfig.swift index e23f2eebbef7..33b7bb63fcbb 100644 --- a/Client/Ecosia/UI/NTP/Customization/CustomizableNTPSettingConfig.swift +++ b/Client/Ecosia/UI/NTP/Customization/CustomizableNTPSettingConfig.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core +import Ecosia enum CustomizableNTPSettingConfig: CaseIterable { case topSites diff --git a/Client/Ecosia/UI/NTP/DefaultBrowser.swift b/Client/Ecosia/UI/NTP/DefaultBrowser.swift index b4a17e858d72..a79aa9d9fcb4 100644 --- a/Client/Ecosia/UI/NTP/DefaultBrowser.swift +++ b/Client/Ecosia/UI/NTP/DefaultBrowser.swift @@ -4,6 +4,7 @@ import UIKit import Common +import Ecosia @available(iOS 14, *) protocol DefaultBrowserDelegate: AnyObject { diff --git a/Client/Ecosia/UI/NTP/Impact/ClimateImpactInfo.swift b/Client/Ecosia/UI/NTP/Impact/ClimateImpactInfo.swift index cd586349cb7e..84ac8c5c6051 100644 --- a/Client/Ecosia/UI/NTP/Impact/ClimateImpactInfo.swift +++ b/Client/Ecosia/UI/NTP/Impact/ClimateImpactInfo.swift @@ -3,7 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core enum ClimateImpactInfo: Equatable { case referral(value: Int) diff --git a/Client/Ecosia/UI/NTP/Impact/NTPImpactCell.swift b/Client/Ecosia/UI/NTP/Impact/NTPImpactCell.swift index 98f1ecd8bd3f..343c9716a1b8 100644 --- a/Client/Ecosia/UI/NTP/Impact/NTPImpactCell.swift +++ b/Client/Ecosia/UI/NTP/Impact/NTPImpactCell.swift @@ -3,7 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core import Common final class NTPImpactCell: UICollectionViewCell, Themeable, ReusableCell { diff --git a/Client/Ecosia/UI/NTP/Impact/NTPImpactCellViewModel.swift b/Client/Ecosia/UI/NTP/Impact/NTPImpactCellViewModel.swift index 0e2a8ac15954..d23189f61977 100644 --- a/Client/Ecosia/UI/NTP/Impact/NTPImpactCellViewModel.swift +++ b/Client/Ecosia/UI/NTP/Impact/NTPImpactCellViewModel.swift @@ -4,7 +4,7 @@ import Foundation import Shared -import Core +import Ecosia import Common protocol NTPImpactCellDelegate: AnyObject { diff --git a/Client/Ecosia/UI/NTP/Library/NTPLibaryCellViewModel.swift b/Client/Ecosia/UI/NTP/Library/NTPLibaryCellViewModel.swift index 1371db0bd3bc..a146aa6b1d68 100644 --- a/Client/Ecosia/UI/NTP/Library/NTPLibaryCellViewModel.swift +++ b/Client/Ecosia/UI/NTP/Library/NTPLibaryCellViewModel.swift @@ -4,7 +4,6 @@ import Foundation import Shared -import Core import Common protocol NTPLibraryDelegate: AnyObject { diff --git a/Client/Ecosia/UI/NTP/Library/NTPLibraryCell.swift b/Client/Ecosia/UI/NTP/Library/NTPLibraryCell.swift index 4d4f94028d40..778fc9b74ae9 100644 --- a/Client/Ecosia/UI/NTP/Library/NTPLibraryCell.swift +++ b/Client/Ecosia/UI/NTP/Library/NTPLibraryCell.swift @@ -5,6 +5,7 @@ import Shared import UIKit import Common +import Ecosia class NTPLibraryCell: UICollectionViewCell, Themeable, ReusableCell { diff --git a/Client/Ecosia/UI/NTP/Logo/NTPLogoCell.swift b/Client/Ecosia/UI/NTP/Logo/NTPLogoCell.swift index e3116eb2d0c0..7d0643da1285 100644 --- a/Client/Ecosia/UI/NTP/Logo/NTPLogoCell.swift +++ b/Client/Ecosia/UI/NTP/Logo/NTPLogoCell.swift @@ -3,7 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core import Common final class NTPLogoCell: UICollectionViewCell, ReusableCell, Themeable { diff --git a/Client/Ecosia/UI/NTP/NTPLayout.swift b/Client/Ecosia/UI/NTP/NTPLayout.swift index b094969c9a73..81fd8dcb2335 100644 --- a/Client/Ecosia/UI/NTP/NTPLayout.swift +++ b/Client/Ecosia/UI/NTP/NTPLayout.swift @@ -3,8 +3,8 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core import Shared +import Ecosia protocol NTPLayoutHighlightDataSource: AnyObject { func getSectionViewModel(shownSection: Int) -> HomepageViewModelProtocol? diff --git a/Client/Ecosia/UI/NTP/NTPTooltip.Highlight.swift b/Client/Ecosia/UI/NTP/NTPTooltip.Highlight.swift index 7ba0313b2e0c..0e7a2bf6114e 100644 --- a/Client/Ecosia/UI/NTP/NTPTooltip.Highlight.swift +++ b/Client/Ecosia/UI/NTP/NTPTooltip.Highlight.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core +import Ecosia extension NTPTooltip { enum Highlight { @@ -33,7 +33,7 @@ extension NTPTooltip { } } - class func highlight(for user: Core.User = User.shared) -> NTPTooltip.Highlight? { + class func highlight(for user: User = User.shared) -> NTPTooltip.Highlight? { guard !user.firstTime else { return nil } if user.referrals.isNewClaim { diff --git a/Client/Ecosia/UI/NTP/News/NTPNewsCell.swift b/Client/Ecosia/UI/NTP/News/NTPNewsCell.swift index 165fa90b8cf8..c0f204ee55cc 100644 --- a/Client/Ecosia/UI/NTP/News/NTPNewsCell.swift +++ b/Client/Ecosia/UI/NTP/News/NTPNewsCell.swift @@ -2,7 +2,7 @@ // 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 Core +import Ecosia import UIKit import Common diff --git a/Client/Ecosia/UI/NTP/News/NTPNewsCellViewModel.swift b/Client/Ecosia/UI/NTP/News/NTPNewsCellViewModel.swift index 2bb2cc30ca7b..d9afb6fea4b2 100644 --- a/Client/Ecosia/UI/NTP/News/NTPNewsCellViewModel.swift +++ b/Client/Ecosia/UI/NTP/News/NTPNewsCellViewModel.swift @@ -4,8 +4,8 @@ import Foundation import Shared -import Core import Common +import Ecosia protocol NTPNewsCellDelegate: AnyObject { func openSeeAllNews() diff --git a/Client/Ecosia/UI/NTP/News/NewsController.swift b/Client/Ecosia/UI/NTP/News/NewsController.swift index e5e0c2d56f54..783b0b02cf84 100644 --- a/Client/Ecosia/UI/NTP/News/NewsController.swift +++ b/Client/Ecosia/UI/NTP/News/NewsController.swift @@ -2,9 +2,9 @@ // 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 Core import UIKit import Common +import Ecosia final class NewsController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, Themeable { diff --git a/Client/Ecosia/UI/NTP/NudgeCards/NTPConfigurableNudgeCardCell.swift b/Client/Ecosia/UI/NTP/NudgeCards/NTPConfigurableNudgeCardCell.swift index 55c3bf0242d2..1a804ea6d9b8 100644 --- a/Client/Ecosia/UI/NTP/NudgeCards/NTPConfigurableNudgeCardCell.swift +++ b/Client/Ecosia/UI/NTP/NudgeCards/NTPConfigurableNudgeCardCell.swift @@ -3,7 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core import Common /// Reusable Nudge Card Cell that can be configured with any view model. diff --git a/Client/Ecosia/UI/NTP/NudgeCards/Newsletter/NTPNewsletterCardViewModel.swift b/Client/Ecosia/UI/NTP/NudgeCards/Newsletter/NTPNewsletterCardViewModel.swift index da92855639d5..c54935b76d21 100644 --- a/Client/Ecosia/UI/NTP/NudgeCards/Newsletter/NTPNewsletterCardViewModel.swift +++ b/Client/Ecosia/UI/NTP/NudgeCards/Newsletter/NTPNewsletterCardViewModel.swift @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation +import Ecosia final class NTPNewsletterCardViewModel: NTPConfigurableNudgeCardCellViewModel { override var title: String { diff --git a/Client/Ecosia/UI/Onboarding/Welcome.swift b/Client/Ecosia/UI/Onboarding/Welcome.swift index f41c62efe577..bad1c4f22f26 100644 --- a/Client/Ecosia/UI/Onboarding/Welcome.swift +++ b/Client/Ecosia/UI/Onboarding/Welcome.swift @@ -3,8 +3,8 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core import Common +import Ecosia protocol WelcomeDelegate: AnyObject { func welcomeDidFinish(_ welcome: Welcome) diff --git a/Client/Ecosia/UI/Onboarding/WelcomeTour.Step.swift b/Client/Ecosia/UI/Onboarding/WelcomeTour.Step.swift index 89ea65e0dbf8..6e931b87bf9b 100644 --- a/Client/Ecosia/UI/Onboarding/WelcomeTour.Step.swift +++ b/Client/Ecosia/UI/Onboarding/WelcomeTour.Step.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core +import Ecosia extension WelcomeTour { diff --git a/Client/Ecosia/UI/Onboarding/WelcomeTour.swift b/Client/Ecosia/UI/Onboarding/WelcomeTour.swift index f9a35bb99abd..cb41ef4effea 100644 --- a/Client/Ecosia/UI/Onboarding/WelcomeTour.swift +++ b/Client/Ecosia/UI/Onboarding/WelcomeTour.swift @@ -3,8 +3,8 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core import Common +import Ecosia protocol WelcomeTourDelegate: AnyObject { func welcomeTourDidFinish(_ tour: WelcomeTour) diff --git a/Client/Ecosia/UI/Onboarding/WelcomeTourAction.swift b/Client/Ecosia/UI/Onboarding/WelcomeTourAction.swift index 218a7fb5bb0d..ca2ca96d3183 100644 --- a/Client/Ecosia/UI/Onboarding/WelcomeTourAction.swift +++ b/Client/Ecosia/UI/Onboarding/WelcomeTourAction.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core +import Ecosia import Common final class WelcomeTourAction: UIView, Themeable { diff --git a/Client/Ecosia/UI/Onboarding/WelcomeTourTransparent.swift b/Client/Ecosia/UI/Onboarding/WelcomeTourTransparent.swift index 7f4c6126be8f..70ae96504aa6 100644 --- a/Client/Ecosia/UI/Onboarding/WelcomeTourTransparent.swift +++ b/Client/Ecosia/UI/Onboarding/WelcomeTourTransparent.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core +import Ecosia import Common final class WelcomeTourTransparent: UIView, Themeable { diff --git a/Client/Ecosia/UI/Theme/EcosiaTheme.swift b/Client/Ecosia/UI/Theme/EcosiaTheme.swift index bfec7cb8629e..ea9f3f1f3470 100644 --- a/Client/Ecosia/UI/Theme/EcosiaTheme.swift +++ b/Client/Ecosia/UI/Theme/EcosiaTheme.swift @@ -5,13 +5,7 @@ import UIKit import Common -extension LegacyTheme { - var isDark: Bool { - return type(of: self) == DarkTheme.self - } -} - -class EcosiaTheme { +public class EcosiaTheme { var primaryBrand: UIColor { .Light.Brand.primary} var secondaryBrand: UIColor { UIColor.Photon.Grey60 } var border: UIColor { .Light.border } @@ -93,7 +87,7 @@ class EcosiaTheme { var peach: UIColor { .init(rgb: 0xFFE6BF) } } -final class DarkEcosiaTheme: EcosiaTheme { +public final class DarkEcosiaTheme: EcosiaTheme { override var primaryBrand: UIColor { .Dark.Brand.primary} override var secondaryBrand: UIColor { .white } override var border: UIColor { .Dark.border } @@ -176,36 +170,3 @@ final class DarkEcosiaTheme: EcosiaTheme { override var homePanelBackground: UIColor { return .Dark.Background.secondary } override var peach: UIColor { .init(rgb: 0xCC7722) } } - -extension UIImage { - convenience init?(themed name: String) { - let suffix = LegacyThemeManager.instance.current.isDark ? "Dark" : "" - self.init(named: name + suffix) - } -} - -class EcosiaPrimaryButton: UIButton { - override var isSelected: Bool { - get { - return super.isSelected - } - set { - super.isSelected = newValue - update() - } - } - - override var isHighlighted: Bool { - get { - return super.isHighlighted - } - set { - super.isHighlighted = newValue - update() - } - } - - private func update() { - backgroundColor = (isSelected || isHighlighted) ? .legacyTheme.ecosia.primaryButtonActive : .legacyTheme.ecosia.primaryButton - } -} diff --git a/Client/Ecosia/UI/WhatsNew/DataProvider/WhatsNewLocalDataProvider.swift b/Client/Ecosia/UI/WhatsNew/DataProvider/WhatsNewLocalDataProvider.swift index 9fa54d814b8a..30ea6fcc3b11 100644 --- a/Client/Ecosia/UI/WhatsNew/DataProvider/WhatsNewLocalDataProvider.swift +++ b/Client/Ecosia/UI/WhatsNew/DataProvider/WhatsNewLocalDataProvider.swift @@ -4,8 +4,8 @@ import Foundation import Shared -import Core import Common +import Ecosia /// A local data provider for fetching What's New items based on app version updates. final class WhatsNewLocalDataProvider: WhatsNewDataProvider { diff --git a/Client/Ecosia/UI/WhatsNew/WhatsNewCell.swift b/Client/Ecosia/UI/WhatsNew/WhatsNewCell.swift index 7db7adf5ca49..75405f233b2a 100644 --- a/Client/Ecosia/UI/WhatsNew/WhatsNewCell.swift +++ b/Client/Ecosia/UI/WhatsNew/WhatsNewCell.swift @@ -3,7 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core import Common final class WhatsNewCell: UITableViewCell, Themeable { diff --git a/Client/Ecosia/UI/WhatsNew/WhatsNewViewController.swift b/Client/Ecosia/UI/WhatsNew/WhatsNewViewController.swift index 0c4190834f8f..1a1c287d1dff 100644 --- a/Client/Ecosia/UI/WhatsNew/WhatsNewViewController.swift +++ b/Client/Ecosia/UI/WhatsNew/WhatsNewViewController.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit -import Core +import Ecosia import Common protocol WhatsNewViewDelegate: AnyObject { diff --git a/Client/Experiments/Messaging/GleanPlumbMessageManager.swift b/Client/Experiments/Messaging/GleanPlumbMessageManager.swift index 2e393a888eb8..e3657fcf92aa 100644 --- a/Client/Experiments/Messaging/GleanPlumbMessageManager.swift +++ b/Client/Experiments/Messaging/GleanPlumbMessageManager.swift @@ -3,7 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation - import MozillaAppServices import Shared diff --git a/Client/Frontend/AuthenticationManager/SensitiveViewController.swift b/Client/Frontend/AuthenticationManager/SensitiveViewController.swift index 8ab43a3d4388..292a7b60d79e 100644 --- a/Client/Frontend/AuthenticationManager/SensitiveViewController.swift +++ b/Client/Frontend/AuthenticationManager/SensitiveViewController.swift @@ -3,7 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit - import Shared class SensitiveViewController: UIViewController { diff --git a/Client/Frontend/Autofill/CreditCard/CreditCardBottomSheet/CreditCardBottomSheetHeaderView.swift b/Client/Frontend/Autofill/CreditCard/CreditCardBottomSheet/CreditCardBottomSheetHeaderView.swift index e69ca77b4514..63f779fc347d 100644 --- a/Client/Frontend/Autofill/CreditCard/CreditCardBottomSheet/CreditCardBottomSheetHeaderView.swift +++ b/Client/Frontend/Autofill/CreditCard/CreditCardBottomSheet/CreditCardBottomSheetHeaderView.swift @@ -3,7 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation - import Common import Shared diff --git a/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift b/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift index a697584d3820..919f7e21c576 100644 --- a/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift +++ b/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift @@ -15,8 +15,7 @@ import Telemetry import Common import ComponentLibrary import Redux -// Ecosia: Import Core -import Core +import Ecosia class BrowserViewController: UIViewController, SearchBarLocationProvider, diff --git a/Client/Frontend/Browser/MainMenuActionHelper.swift b/Client/Frontend/Browser/MainMenuActionHelper.swift index 03bd44ef9d6d..e27c1df936b2 100644 --- a/Client/Frontend/Browser/MainMenuActionHelper.swift +++ b/Client/Frontend/Browser/MainMenuActionHelper.swift @@ -10,9 +10,10 @@ import Storage import UIKit import SwiftUI import Common -import Core + // Ecosia: Need SafariServices to enable "open in safari" action import SafariServices +import Ecosia protocol ToolBarActionMenuDelegate: AnyObject { func updateToolbarState() diff --git a/Client/Frontend/Browser/MetadataParserHelper.swift b/Client/Frontend/Browser/MetadataParserHelper.swift index f5d08b96768f..b4c8883503ab 100644 --- a/Client/Frontend/Browser/MetadataParserHelper.swift +++ b/Client/Frontend/Browser/MetadataParserHelper.swift @@ -6,8 +6,6 @@ import Foundation import Shared import Storage import WebKit -// Ecosia: -import Core class MetadataParserHelper: TabEventHandler { init() { diff --git a/Client/Frontend/Home/HomepageViewController.swift b/Client/Frontend/Home/HomepageViewController.swift index 4dd8af2d7645..8c8c7f751f2e 100644 --- a/Client/Frontend/Home/HomepageViewController.swift +++ b/Client/Frontend/Home/HomepageViewController.swift @@ -9,8 +9,7 @@ import Shared import Storage import Redux import UIKit -// Ecosia: Import Core -import Core +import Ecosia class HomepageViewController: UIViewController, diff --git a/Client/Frontend/Home/HomepageViewModel.swift b/Client/Frontend/Home/HomepageViewModel.swift index 9033ebc844d3..e5c381cc6cd9 100644 --- a/Client/Frontend/Home/HomepageViewModel.swift +++ b/Client/Frontend/Home/HomepageViewModel.swift @@ -5,7 +5,7 @@ import Common import MozillaAppServices import Shared -import Core +import Ecosia protocol HomepageViewModelDelegate: AnyObject { func reloadView() diff --git a/Client/Frontend/Home/TopSites/TopSitesViewModel.swift b/Client/Frontend/Home/TopSites/TopSitesViewModel.swift index a68bb9bcaf85..5617efa84cb1 100644 --- a/Client/Frontend/Home/TopSites/TopSitesViewModel.swift +++ b/Client/Frontend/Home/TopSites/TopSitesViewModel.swift @@ -6,8 +6,7 @@ import Common import Foundation import Shared import Storage -// Ecosia: importing Core -import Core +import Ecosia class TopSitesViewModel { struct UX { diff --git a/Client/Frontend/Library/Bookmarks/BookmarksPanel.swift b/Client/Frontend/Library/Bookmarks/BookmarksPanel.swift index b783c2cb1630..6e51c6d0d3e0 100644 --- a/Client/Frontend/Library/Bookmarks/BookmarksPanel.swift +++ b/Client/Frontend/Library/Bookmarks/BookmarksPanel.swift @@ -7,7 +7,7 @@ import UIKit import Storage import Shared import SiteImageView -import Core +import Ecosia let LocalizedRootBookmarkFolderStrings = [ BookmarkRoots.MenuFolderGUID: String.BookmarksFolderTitleMenu, diff --git a/Client/Frontend/Library/Bookmarks/BookmarksPanelViewModel.swift b/Client/Frontend/Library/Bookmarks/BookmarksPanelViewModel.swift index 8dbdb8ce3b33..23a99b2e8584 100644 --- a/Client/Frontend/Library/Bookmarks/BookmarksPanelViewModel.swift +++ b/Client/Frontend/Library/Bookmarks/BookmarksPanelViewModel.swift @@ -6,7 +6,7 @@ import Foundation import Common import Storage import Shared -import Core +import Ecosia class BookmarksPanelViewModel: NSObject { enum BookmarksSection: Int, CaseIterable { @@ -205,7 +205,7 @@ extension BookmarksPanelViewModel { } // MARK: - Private - private func getBookmarksForExport() async throws -> [Core.BookmarkItem] { + private func getBookmarksForExport() async throws -> [Ecosia.BookmarkItem] { return try await withCheckedThrowingContinuation { [weak self] continuation in guard let self = self else { return continuation.resume(returning: []) @@ -222,7 +222,7 @@ extension BookmarksPanelViewModel { self.bookmarkFolder = mobileFolder let bookmarkNodes = mobileFolder.fxChildren ?? [] - let items: [Core.BookmarkItem] = bookmarkNodes + let items: [Ecosia.BookmarkItem] = bookmarkNodes .compactMap { $0 as? BookmarkNodeData } .compactMap { bookmarkNode in self.exportNode(bookmarkNode) @@ -233,7 +233,7 @@ extension BookmarksPanelViewModel { } } - private func exportNode(_ node: BookmarkNodeData) -> Core.BookmarkItem? { + private func exportNode(_ node: BookmarkNodeData) -> Ecosia.BookmarkItem? { if let folder = node as? BookmarkFolderData { return .folder(folder.title, folder.children?.compactMap { exportNode($0) } ?? [], .empty) } else if let bookmark = node as? BookmarkItemData { diff --git a/Client/Frontend/Settings/AdvancedAccountSettingViewController.swift b/Client/Frontend/Settings/AdvancedAccountSettingViewController.swift index 95af66ff2ed0..3516517a4e44 100644 --- a/Client/Frontend/Settings/AdvancedAccountSettingViewController.swift +++ b/Client/Frontend/Settings/AdvancedAccountSettingViewController.swift @@ -4,7 +4,6 @@ import UIKit import Shared - import Account private class CustomFxAContentServerEnableSetting: BoolSetting { diff --git a/Client/Frontend/Settings/HomepageSettings/HomePageSettingViewController.swift b/Client/Frontend/Settings/HomepageSettings/HomePageSettingViewController.swift index e5cafe240159..b7035268e74b 100644 --- a/Client/Frontend/Settings/HomepageSettings/HomePageSettingViewController.swift +++ b/Client/Frontend/Settings/HomepageSettings/HomePageSettingViewController.swift @@ -5,8 +5,7 @@ import Foundation import Shared import Common -// Ecosia: import Core -import Core +import Ecosia class HomePageSettingViewController: SettingsTableViewController, FeatureFlaggable { // MARK: - Variables diff --git a/Client/Frontend/Settings/Main/DefaultBrowserSetting.swift b/Client/Frontend/Settings/Main/DefaultBrowserSetting.swift index 3f9809949946..a4bd427c9e2e 100644 --- a/Client/Frontend/Settings/Main/DefaultBrowserSetting.swift +++ b/Client/Frontend/Settings/Main/DefaultBrowserSetting.swift @@ -5,6 +5,7 @@ import Common import Foundation import Shared +import Ecosia class DefaultBrowserSetting: Setting { override var accessibilityIdentifier: String? { return "DefaultBrowserSettings" } diff --git a/Client/Frontend/Settings/SearchBar/SearchBarSettingsViewModel.swift b/Client/Frontend/Settings/SearchBar/SearchBarSettingsViewModel.swift index 7e4fef8798f4..766635e63a40 100644 --- a/Client/Frontend/Settings/SearchBar/SearchBarSettingsViewModel.swift +++ b/Client/Frontend/Settings/SearchBar/SearchBarSettingsViewModel.swift @@ -5,6 +5,7 @@ import Common import Foundation import Shared +import Ecosia enum SearchBarPosition: String, FlaggableFeatureOptions { case bottom diff --git a/Client/Frontend/TabContentsScripts/UserScriptManager.swift b/Client/Frontend/TabContentsScripts/UserScriptManager.swift index 1684c7ff18f3..e05daf65dec5 100644 --- a/Client/Frontend/TabContentsScripts/UserScriptManager.swift +++ b/Client/Frontend/TabContentsScripts/UserScriptManager.swift @@ -3,8 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import WebKit -// Ecosia -import Core +import Ecosia class UserScriptManager: FeatureFlaggable { // Scripts can use this to verify the *app* (not JS on the web) is calling into them. diff --git a/Client/Frontend/Theme/LegacyThemeManager/LegacyDarkTheme.swift b/Client/Frontend/Theme/LegacyThemeManager/LegacyDarkTheme.swift index 352e62a521c8..b29fcdfb91cd 100644 --- a/Client/Frontend/Theme/LegacyThemeManager/LegacyDarkTheme.swift +++ b/Client/Frontend/Theme/LegacyThemeManager/LegacyDarkTheme.swift @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import UIKit +import Ecosia private class DarkBrowserColor: BrowserColor { override var background: UIColor { return UIColor.Photon.DarkGrey60 } diff --git a/Client/Frontend/Theme/LegacyThemeManager/LegacyTheme.swift b/Client/Frontend/Theme/LegacyThemeManager/LegacyTheme.swift index 3f0f4fc19d6e..25b070637b9c 100644 --- a/Client/Frontend/Theme/LegacyThemeManager/LegacyTheme.swift +++ b/Client/Frontend/Theme/LegacyThemeManager/LegacyTheme.swift @@ -5,6 +5,7 @@ import Common import UIKit import Shared +import Ecosia protocol PrivateModeUI { func applyUIMode(isPrivate: Bool, theme: Theme) @@ -61,3 +62,10 @@ class LegacyNormalTheme: LegacyTheme { // Ecosia: Adapt theme var ecosia: EcosiaTheme { return EcosiaTheme() } } + +// Ecosia: Add `isDark` extension as previously part of the `EcosiaTheme` class +extension LegacyTheme { + var isDark: Bool { + return type(of: self) == DarkTheme.self + } +} diff --git a/Client/Info.plist b/Client/Info.plist index 0d44d6d3110f..f5125d76d94f 100644 --- a/Client/Info.plist +++ b/Client/Info.plist @@ -2,222 +2,190 @@ - AdjustAppToken - $(ADJUST_APP_TOKEN) - AppIdentifierPrefix - $(APP_IDENTIFIER_PREFIX) - BGTaskSchedulerPermittedIdentifiers - - org.mozilla.ios.sync.part1 - org.mozilla.ios.sync.part2 - org.mozilla.ios.surface.notification.refresh - org.mozilla.ios.firefox.suggest.ingest - - BRAZE_API_KEY - $(BRAZE_API_KEY) - CFBundleDevelopmentRegion - en + AdjustAppToken + $(ADJUST_APP_TOKEN) + AppIdentifierPrefix + $(APP_IDENTIFIER_PREFIX) + BGTaskSchedulerPermittedIdentifiers + + org.mozilla.ios.sync.part1 + org.mozilla.ios.sync.part2 + org.mozilla.ios.surface.notification.refresh + org.mozilla.ios.firefox.suggest.ingest + + BRAZE_API_KEY + $(BRAZE_API_KEY) + + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + 1 + UILaunchStoryboardName + EcosiaLaunchScreen.xib CFBundleDisplayName $(MOZ_BUNDLE_DISPLAY_NAME) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(MOZ_PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleURLName - com.ecosia.ecosiaapp - CFBundleURLSchemes - - $(MOZ_PUBLIC_URL_SCHEME) - $(MOZ_INTERNAL_URL_SCHEME) - http - https - - - - CFBundleVersion - 1 - CF_ACCESS_CLIENT_ID - $(CF_ACCESS_CLIENT_ID) - CF_ACCESS_CLIENT_SECRET - $(CF_ACCESS_CLIENT_SECRET) - INIntentsSupported - - QuickActionIntent - - ITSAppUsesNonExemptEncryption - - LSApplicationQueriesSchemes - - pocket - firefox-focus - firefox-klar - ymail - ms-outlook - airmail - italiaonline-mailto - mailru-mailto - mymail-mailto - readdle-spark - itms-books - org-appextension-feature-password-management - googlegmail - fastmail - protonmail - whatsapp - - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - - MozDevelopmentTeam - $(DEVELOPMENT_TEAM) - MozInternalURLScheme - $(MOZ_INTERNAL_URL_SCHEME) - MozPublicURLScheme - $(MOZ_PUBLIC_URL_SCHEME) - MozWallpaperURLScheme - $(MOZ_WALLPAPER_ASSET_URL) - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSCameraUsageDescription - Firefox uses your camera to scan QR codes and take photos and video. - NSFaceIDUsageDescription - Firefox requires Face ID to access your saved passwords and payment methods. - NSLocationWhenInUseUsageDescription - Websites you visit may request your location. - NSMicrophoneUsageDescription - Firefox uses your microphone to record and upload audio. - NSPhotoLibraryAddUsageDescription - This lets you save photos. - NSUserActivityTypes - - QuickActionIntent - QuickLinkSelectionIntent - com.ecosia.ecosiaapp.browsing - - NimbusURL - $(NIMBUS_URL) - PocketEnvironmentAPIKey - $(POCKET_API_KEY) - SentryCloudDSN - $(SENTRY_CLOUD_DSN) - UIAppFonts - - NewYorkMedium-Bold.otf - NewYorkMedium-BoldItalic.otf - NewYorkMedium-Regular.otf - NewYorkMedium-RegularItalic.otf - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneClassName - UIWindowScene - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - - UIApplicationShortcutItems - - - UIApplicationShortcutItemIconFile - plusLarge - UIApplicationShortcutItemTitle - New Tab - UIApplicationShortcutItemType - $(PRODUCT_BUNDLE_IDENTIFIER).NewTab - - - UIApplicationShortcutItemIconFile - privateSearch - UIApplicationShortcutItemTitle - New Private Tab - UIApplicationShortcutItemType - $(PRODUCT_BUNDLE_IDENTIFIER).NewPrivateTab - - - UIApplicationShortcutItemIconFile - menu-ScanQRCode - UIApplicationShortcutItemTitle - Scan QR Code - UIApplicationShortcutItemType - $(PRODUCT_BUNDLE_IDENTIFIER).QRCode - - - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - fetch - processing - remote-notification - - UIFileSharingEnabled - - UILaunchStoryboardName - EcosiaLaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UIStatusBarHidden - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - UTImportedTypeDeclarations - - - UTTypeConformsTo - - public.url - - UTTypeDescription - 1Password Fill Browser Action - UTTypeIdentifier - org.appextension.fill-browser-action - UTTypeSize64IconFile - - - + + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLName + com.ecosia.ecosiaapp + CFBundleURLSchemes + + $(MOZ_PUBLIC_URL_SCHEME) + $(MOZ_INTERNAL_URL_SCHEME) + http + https + + + + CF_ACCESS_CLIENT_ID + $(CF_ACCESS_CLIENT_ID) + CF_ACCESS_CLIENT_SECRET + $(CF_ACCESS_CLIENT_SECRET) + INIntentsSupported + + QuickActionIntent + + ITSAppUsesNonExemptEncryption + + LSApplicationQueriesSchemes + + pocket + firefox-focus + firefox-klar + ymail + ms-outlook + airmail + italiaonline-mailto + mailru-mailto + mymail-mailto + readdle-spark + itms-books + org-appextension-feature-password-management + googlegmail + fastmail + protonmail + whatsapp + + LSRequiresIPhoneOS + + MozDevelopmentTeam + $(DEVELOPMENT_TEAM) + MozInternalURLScheme + $(MOZ_INTERNAL_URL_SCHEME) + MozPublicURLScheme + $(MOZ_PUBLIC_URL_SCHEME) + MozWallpaperURLScheme + $(MOZ_WALLPAPER_ASSET_URL) + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSUserActivityTypes + + QuickActionIntent + QuickLinkSelectionIntent + com.ecosia.ecosiaapp.browsing + + NimbusURL + $(NIMBUS_URL) + PocketEnvironmentAPIKey + $(POCKET_API_KEY) + SentryCloudDSN + $(SENTRY_CLOUD_DSN) + UIAppFonts + + NewYorkMedium-Bold.otf + NewYorkMedium-BoldItalic.otf + NewYorkMedium-Regular.otf + NewYorkMedium-RegularItalic.otf + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UIApplicationShortcutItems + + + UIApplicationShortcutItemIconFile + plusLarge + UIApplicationShortcutItemTitle + New Tab + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).NewTab + + + UIApplicationShortcutItemIconFile + privateSearch + UIApplicationShortcutItemTitle + New Private Tab + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).NewPrivateTab + + + UIApplicationShortcutItemIconFile + menu-ScanQRCode + UIApplicationShortcutItemTitle + Scan QR Code + UIApplicationShortcutItemType + $(PRODUCT_BUNDLE_IDENTIFIER).QRCode + + + UIBackgroundModes + + fetch + processing + remote-notification + + UIFileSharingEnabled + + UIViewControllerBasedStatusBarAppearance + + UTImportedTypeDeclarations + + + UTTypeConformsTo + + public.url + + UTTypeDescription + 1Password Fill Browser Action + UTTypeIdentifier + org.appextension.fill-browser-action + UTTypeSize64IconFile + + + diff --git a/Client/IntroScreenManager.swift b/Client/IntroScreenManager.swift index 7588290fbdea..6e2d1d8876f6 100644 --- a/Client/IntroScreenManager.swift +++ b/Client/IntroScreenManager.swift @@ -4,8 +4,7 @@ import Foundation import Shared -// Ecosia: Import Core -import Core +import Ecosia struct IntroScreenManager { var prefs: Prefs diff --git a/Client/TabManagement/Legacy/LegacyTabManager.swift b/Client/TabManagement/Legacy/LegacyTabManager.swift index 7bc87d1ab4d9..b9d04ff583a4 100644 --- a/Client/TabManagement/Legacy/LegacyTabManager.swift +++ b/Client/TabManagement/Legacy/LegacyTabManager.swift @@ -7,8 +7,7 @@ import Foundation import WebKit import Storage import Shared -// Ecosia -import Core +import Ecosia import Combine // MARK: - TabManagerDelegate diff --git a/Client/TabManagement/TabManagerImplementation.swift b/Client/TabManagement/TabManagerImplementation.swift index 9684c344862e..6e39c6920749 100644 --- a/Client/TabManagement/TabManagerImplementation.swift +++ b/Client/TabManagement/TabManagerImplementation.swift @@ -7,6 +7,7 @@ import TabDataStore import Storage import Common import Shared +import Ecosia // This class subclasses the legacy tab manager temporarily so we can // gradually migrate to the new system diff --git a/Client/TabManagement/TabMigrationUtility.swift b/Client/TabManagement/TabMigrationUtility.swift index 46d107b794e4..1e239d200c46 100644 --- a/Client/TabManagement/TabMigrationUtility.swift +++ b/Client/TabManagement/TabMigrationUtility.swift @@ -5,6 +5,7 @@ import Common import Shared import TabDataStore +import Ecosia protocol TabMigrationUtility { var shouldRunMigration: Bool { get } diff --git a/Client/Ecosia/Analytics/Analytics+Configuration.swift b/Ecosia/Analytics/Analytics+Configuration.swift similarity index 98% rename from Client/Ecosia/Analytics/Analytics+Configuration.swift rename to Ecosia/Analytics/Analytics+Configuration.swift index 4fa023e8ed5a..0954aae55621 100644 --- a/Client/Ecosia/Analytics/Analytics+Configuration.swift +++ b/Ecosia/Analytics/Analytics+Configuration.swift @@ -4,7 +4,6 @@ import Foundation import SnowplowTracker -import Core extension Analytics { @@ -82,7 +81,7 @@ extension Analytics { /// Checks if a day has passed since the last check for a specific event. /// - Parameter identifier: A unique identifier used to store and retrieve the last check date from `UserDefaults`. /// - Returns: A Boolean value indicating whether a day has passed since the last check. If no previous check exists, returns `true` and records the current date. - static func hasDayPassedSinceLastCheck(for identifier: String) -> Bool { + public static func hasDayPassedSinceLastCheck(for identifier: String) -> Bool { let now = Date() let defaults = UserDefaults.standard diff --git a/Client/Ecosia/Analytics/Analytics.Values.swift b/Ecosia/Analytics/Analytics.Values.swift similarity index 78% rename from Client/Ecosia/Analytics/Analytics.Values.swift rename to Ecosia/Analytics/Analytics.Values.swift index 6dad521b9804..477cb2725e71 100644 --- a/Client/Ecosia/Analytics/Analytics.Values.swift +++ b/Ecosia/Analytics/Analytics.Values.swift @@ -5,7 +5,7 @@ import Foundation extension Analytics { - enum Category: String { + public enum Category: String { case activity, bookmarks, @@ -24,27 +24,27 @@ extension Analytics { settings } - enum Label: String { + public enum Label: String { case analytics, market, toolbar - enum Bookmarks: String { + public enum Bookmarks: String { case importFunctionality = "import_functionality", learnMore = "learn_more", `import` } - enum DefaultBrowser: String { + public enum DefaultBrowser: String { case deeplink = "default_browser_deeplink", promo = "default_browser_promo", settings = "default_browser_settings" } - enum Menu: String { + public enum Menu: String { case bookmarks, copyLink = "copy_link", @@ -63,7 +63,7 @@ extension Analytics { zoom } - enum MenuStatus: String { + public enum MenuStatus: String { case bookmark, darkMode = "dark_mode", @@ -71,12 +71,12 @@ extension Analytics { shortcut } - enum Migration: String { + public enum Migration: String { case tabs } - enum Navigation: String { + public enum Navigation: String { case inapp, financialReports = "financial_reports", @@ -87,12 +87,12 @@ extension Analytics { terms } - enum NewsletterCardExperiment: String { + public enum NewsletterCardExperiment: String { case ntpCard = "ntp_card" } - enum NTP: String { + public enum NTP: String { case about, climateCounter = "climate_counter", @@ -104,13 +104,13 @@ extension Analytics { topSites = "top_sites" } - enum Onboarding: String { + public enum Onboarding: String { case next, skip } - enum Referral: String { + public enum Referral: String { case invite, inviteScreen = "invite_screen", @@ -120,7 +120,7 @@ extension Analytics { } } - enum Action: String { + public enum Action: String { case change, click, @@ -134,13 +134,13 @@ extension Analytics { success, view - enum Activity: String { + public enum Activity: String { case launch, resume } - enum APNConsent: String { + public enum APNConsent: String { case allow, deny, @@ -148,40 +148,40 @@ extension Analytics { view } - enum Bookmarks: String { + public enum Bookmarks: String { case `import` } - enum BrazeIAM: String { + public enum BrazeIAM: String { case click, dismiss, view } - enum NewsletterCardExperiment: String { + public enum NewsletterCardExperiment: String { case click, dismiss, view } - enum NTPCustomization: String { + public enum NTPCustomization: String { case click, disable, enable } - enum Promo: String { + public enum Promo: String { case click, close, view } - enum Referral: String { + public enum Referral: String { case claim, click, @@ -190,14 +190,14 @@ extension Analytics { view } - enum SeedCounter: String { + public enum SeedCounter: String { case level, collect, click } - enum TopSite: String { + public enum TopSite: String { case click, openNewTab = "open_new_tab", @@ -208,19 +208,19 @@ extension Analytics { } } - enum Property: String { + public enum Property: String { case enable, disable, home - enum APNConsent: String { + public enum APNConsent: String { case home, onLaunchPrompt = "on_launch_prompt" } - enum Bookmarks: String { + public enum Bookmarks: String { case `import`, export, @@ -229,7 +229,7 @@ extension Analytics { error } - enum Library: String { + public enum Library: String { case bookmarks, downloads, @@ -237,7 +237,7 @@ extension Analytics { readingList = "reading_list" } - enum OnboardingPage: String, CaseIterable { + public enum OnboardingPage: String, CaseIterable { case start, profits, @@ -246,14 +246,14 @@ extension Analytics { transparentFinances = "transparent_finances" } - enum ShareContent: String { + public enum ShareContent: String { case ntp, web, file } - enum TopSite: String { + public enum TopSite: String { case `default`, mostVisited = "most_visited", diff --git a/Client/Ecosia/Analytics/Analytics.swift b/Ecosia/Analytics/Analytics.swift similarity index 84% rename from Client/Ecosia/Analytics/Analytics.swift rename to Ecosia/Analytics/Analytics.swift index 8773b803a034..c0509f64ab70 100644 --- a/Client/Ecosia/Analytics/Analytics.swift +++ b/Ecosia/Analytics/Analytics.swift @@ -4,7 +4,6 @@ import Foundation import SnowplowTracker -import Core open class Analytics { static let installSchema = "iglu:org.ecosia/ios_install_event/jsonschema/1-0-0" @@ -24,7 +23,7 @@ open class Analytics { Self.appResumeDailyTrackingPluginConfiguration]) } - static var shared = Analytics() + public static var shared = Analytics() private var tracker: TrackerController private let notificationCenter: AnalyticsUserNotificationCenterProtocol @@ -39,7 +38,7 @@ open class Analytics { self.notificationCenter = notificationCenter } - private func track(_ event: Event) { + private func track(_ event: SnowplowTracker.Event) { guard User.shared.sendAnonymousUsageData else { return } _ = tracker.track(event) } @@ -53,18 +52,18 @@ open class Analytics { return SelfDescribingJson(schema: abTestSchema, andDictionary: abTestContext) } - func reset() { + public func reset() { User.shared.analyticsId = .init() tracker = Self.tracker } // MARK: App events - func install() { + public func install() { track(SelfDescribing(schema: Self.installSchema, payload: ["app_v": Bundle.version as NSObject])) } - func activity(_ action: Action.Activity) { + public func activity(_ action: Action.Activity) { let event = Structured(category: Category.activity.rawValue, action: action.rawValue) .label(Analytics.Label.Navigation.inapp.rawValue) @@ -75,7 +74,7 @@ open class Analytics { } // MARK: Bookmarks - func bookmarksPerformImportExport(_ property: Property.Bookmarks) { + public func bookmarksPerformImportExport(_ property: Property.Bookmarks) { let event = Structured(category: Category.bookmarks.rawValue, action: Action.click.rawValue) .label(Label.Bookmarks.importFunctionality.rawValue) @@ -83,7 +82,7 @@ open class Analytics { track(event) } - func bookmarksEmptyLearnMoreClicked() { + public func bookmarksEmptyLearnMoreClicked() { let event = Structured(category: Category.bookmarks.rawValue, action: Action.click.rawValue) .label(Label.Bookmarks.learnMore.rawValue) @@ -91,7 +90,7 @@ open class Analytics { track(event) } - func bookmarksImportEnded(_ property: Property.Bookmarks) { + public func bookmarksImportEnded(_ property: Property.Bookmarks) { let event = Structured(category: Category.bookmarks.rawValue, action: Action.Bookmarks.import.rawValue) .label(Label.Bookmarks.import.rawValue) @@ -100,14 +99,14 @@ open class Analytics { } // MARK: Braze IAM - func brazeIAM(action: Action.BrazeIAM, messageOrButtonId: String?) { + public func brazeIAM(action: Action.BrazeIAM, messageOrButtonId: String?) { track(Structured(category: Category.brazeIAM.rawValue, action: action.rawValue) .property(messageOrButtonId)) } // MARK: Default Browser - func appOpenAsDefaultBrowser() { + public func appOpenAsDefaultBrowser() { let event = Structured(category: Category.external.rawValue, action: Action.receive.rawValue) .label(Label.DefaultBrowser.deeplink.rawValue) @@ -115,7 +114,7 @@ open class Analytics { track(event) } - func defaultBrowser(_ action: Action.Promo) { + public func defaultBrowser(_ action: Action.Promo) { let event = Structured(category: Category.browser.rawValue, action: action.rawValue) .label(Label.DefaultBrowser.promo.rawValue) @@ -124,21 +123,21 @@ open class Analytics { track(event) } - func defaultBrowserSettings() { + public func defaultBrowserSettings() { track(Structured(category: Category.browser.rawValue, action: Action.open.rawValue) .label(Label.DefaultBrowser.settings.rawValue)) } // MARK: Menu - func menuClick(_ item: Analytics.Label.Menu) { + public func menuClick(_ item: Analytics.Label.Menu) { let event = Structured(category: Category.menu.rawValue, action: Action.click.rawValue) .label(item.rawValue) track(event) } - func menuShare(_ content: Property.ShareContent) { + public func menuShare(_ content: Property.ShareContent) { let event = Structured(category: Category.menu.rawValue, action: Action.click.rawValue) .label(Label.Menu.share.rawValue) @@ -146,7 +145,7 @@ open class Analytics { track(event) } - func menuStatus(changed item: Analytics.Label.MenuStatus, to: Bool) { + public func menuStatus(changed item: Analytics.Label.MenuStatus, to: Bool) { let event = Structured(category: Category.menuStatus.rawValue, action: Action.click.rawValue) .label(item.rawValue) @@ -155,12 +154,12 @@ open class Analytics { } // MARK: Migration - func migration(_ success: Bool) { + public func migration(_ success: Bool) { track(Structured(category: Category.migration.rawValue, action: success ? Action.success.rawValue : Action.error.rawValue)) } - func migrationError(in migration: Label.Migration, message: String) { + public func migrationError(in migration: Label.Migration, message: String) { track(Structured(category: Category.migration.rawValue, action: Action.error.rawValue) .label(migration.rawValue) @@ -168,20 +167,20 @@ open class Analytics { } // MARK: Navigation - func navigation(_ action: Action, label: Label.Navigation) { + public func navigation(_ action: Action, label: Label.Navigation) { track(Structured(category: Category.navigation.rawValue, action: action.rawValue) .label(label.rawValue)) } - func navigationOpenNews(_ id: String) { + public func navigationOpenNews(_ id: String) { track(Structured(category: Category.navigation.rawValue, action: Action.open.rawValue) .label(Label.Navigation.news.rawValue) .property(id)) } - func navigationChangeMarket(_ new: String) { + public func navigationChangeMarket(_ new: String) { track(Structured(category: Category.navigation.rawValue, action: Action.change.rawValue) .label(Label.market.rawValue) @@ -189,20 +188,20 @@ open class Analytics { } // MARK: `NewsletterCardExperiment` - func newsletterCardExperiment(action: Action.NewsletterCardExperiment) { + public func newsletterCardExperiment(action: Action.NewsletterCardExperiment) { track(Structured(category: Category.newsletterExperiment.rawValue, action: action.rawValue) .label(Label.NewsletterCardExperiment.ntpCard.rawValue)) } // MARK: NTP - func ntpCustomisation(_ action: Action.NTPCustomization, label: Label.NTP) { + public func ntpCustomisation(_ action: Action.NTPCustomization, label: Label.NTP) { track(Structured(category: Category.ntp.rawValue, action: action.rawValue) .label(label.rawValue)) } - func ntpTopSite(_ action: Action.TopSite, property: Property.TopSite, position: NSNumber? = nil) { + public func ntpTopSite(_ action: Action.TopSite, property: Property.TopSite, position: NSNumber? = nil) { track(Structured(category: Category.ntp.rawValue, action: action.rawValue) .label(Label.NTP.topSites.rawValue) @@ -210,15 +209,14 @@ open class Analytics { .value(position)) } - func ntpLibraryItem(_ action: Action, property: Property.Library) { + public func ntpLibraryItem(_ action: Action, property: Property.Library) { track(Structured(category: Category.ntp.rawValue, action: action.rawValue) .label(Label.NTP.quickActions.rawValue) .property(property.rawValue)) } - func ntpSeedCounterExperiment(_ action: Action.SeedCounter, - value: NSNumber) { + public func ntpSeedCounterExperiment(_ action: Action.SeedCounter, value: NSNumber) { track(Structured(category: Category.ntp.rawValue, action: action.rawValue) .label(Label.NTP.climateCounter.rawValue) @@ -227,7 +225,7 @@ open class Analytics { } // MARK: Onboarding - func introDisplaying(page: Property.OnboardingPage?, at index: Int) { + public func introDisplaying(page: Property.OnboardingPage?, at index: Int) { guard let page else { return } @@ -238,7 +236,7 @@ open class Analytics { track(event) } - func introClick(_ label: Label.Onboarding, page: Property.OnboardingPage?, index: Int) { + public func introClick(_ label: Label.Onboarding, page: Property.OnboardingPage?, index: Int) { guard let page else { return } @@ -259,21 +257,21 @@ open class Analytics { } // MARK: Referrals - func referral(action: Action.Referral, label: Label.Referral? = nil) { + public func referral(action: Action.Referral, label: Label.Referral? = nil) { track(Structured(category: Category.invitations.rawValue, action: action.rawValue) .label(label?.rawValue)) } // MARK: Settings - func searchbarChanged(to position: String) { + public func searchbarChanged(to position: String) { track(Structured(category: Category.settings.rawValue, action: Action.change.rawValue) .label(Label.toolbar.rawValue) .property(position)) } - func sendAnonymousUsageDataSetting(enabled: Bool) { + public func sendAnonymousUsageDataSetting(enabled: Bool) { // This is the only place where the tracker should be directly // used since we want to send this just as the user opts out _ = tracker.track(Structured(category: Category.settings.rawValue, diff --git a/Client/Ecosia/Analytics/AnalyticsNotificationSettings.swift b/Ecosia/Analytics/AnalyticsNotificationSettings.swift similarity index 100% rename from Client/Ecosia/Analytics/AnalyticsNotificationSettings.swift rename to Ecosia/Analytics/AnalyticsNotificationSettings.swift diff --git a/Client/Ecosia/Braze/APNConsent.swift b/Ecosia/Braze/APNConsent.swift similarity index 92% rename from Client/Ecosia/Braze/APNConsent.swift rename to Ecosia/Braze/APNConsent.swift index d7d8a827546a..32f4abe6abc3 100644 --- a/Client/Ecosia/Braze/APNConsent.swift +++ b/Ecosia/Braze/APNConsent.swift @@ -3,9 +3,8 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core -struct APNConsent { +public struct APNConsent { private init() {} private static var toggleName: Unleash.Toggle.Name { @@ -17,7 +16,7 @@ struct APNConsent { Unleash.isEnabled(toggleName) && BrazeIntegrationExperiment.isEnabled } - static func requestIfNeeded() async { + public static func requestIfNeeded() async { guard isEnabled, BrazeService.shared.notificationAuthorizationStatus == .notDetermined else { return } diff --git a/Client/Ecosia/Braze/BrazeService.swift b/Ecosia/Braze/BrazeService.swift similarity index 87% rename from Client/Ecosia/Braze/BrazeService.swift rename to Ecosia/Braze/BrazeService.swift index d2d1dea81e38..20532069bd6d 100644 --- a/Client/Ecosia/Braze/BrazeService.swift +++ b/Ecosia/Braze/BrazeService.swift @@ -3,11 +3,12 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation +import UIKit +import UserNotifications import BrazeKit import BrazeUI -import Core -final class BrazeService: NSObject { +public final class BrazeService: NSObject { override private init() {} private var braze: Braze? @@ -16,18 +17,18 @@ final class BrazeService: NSObject { } private(set) var notificationAuthorizationStatus: UNAuthorizationStatus? private static var apiKey = EnvironmentFetcher.valueFromMainBundleOrProcessInfo(forKey: "BRAZE_API_KEY") ?? "" - static let shared = BrazeService() + public static let shared = BrazeService() enum Error: Swift.Error { case invalidConfiguration case generic(description: String) } - enum CustomEvent: String { + public enum CustomEvent: String { case newsletterCardClick = "newsletter_card_click" } - func initialize() async { + public func initialize() async { do { try await initBraze(userId: userId) await refreshAPNRegistrationIfNeeded() @@ -36,14 +37,14 @@ final class BrazeService: NSObject { } } - func registerDeviceToken(_ deviceToken: Data) { + public func registerDeviceToken(_ deviceToken: Data) { braze?.notifications.register(deviceToken: deviceToken) Task.detached(priority: .medium) { [weak self] in await self?.updateID(self?.userId) } } - func logCustomEvent(_ event: CustomEvent) { + public func logCustomEvent(_ event: CustomEvent) { self.braze?.logCustomEvent(name: event.rawValue) } @@ -144,15 +145,15 @@ extension BrazeService { extension BrazeService: BrazeInAppMessageUIDelegate { - func inAppMessage(_ ui: BrazeInAppMessageUI, didPresent message: Braze.InAppMessage, view: any InAppMessageView) { + public func inAppMessage(_ ui: BrazeInAppMessageUI, didPresent message: Braze.InAppMessage, view: any InAppMessageView) { Analytics.shared.brazeIAM(action: .view, messageOrButtonId: message.id) } - func inAppMessage(_ ui: BrazeInAppMessageUI, didDismiss message: Braze.InAppMessage, view: any InAppMessageView) { + public func inAppMessage(_ ui: BrazeInAppMessageUI, didDismiss message: Braze.InAppMessage, view: any InAppMessageView) { Analytics.shared.brazeIAM(action: .dismiss, messageOrButtonId: message.id) } - func inAppMessage(_ ui: BrazeInAppMessageUI, shouldProcess clickAction: Braze.InAppMessage.ClickAction, buttonId: String?, message: Braze.InAppMessage, view: any InAppMessageView) -> Bool { + public func inAppMessage(_ ui: BrazeInAppMessageUI, shouldProcess clickAction: Braze.InAppMessage.ClickAction, buttonId: String?, message: Braze.InAppMessage, view: any InAppMessageView) -> Bool { Analytics.shared.brazeIAM(action: .click, messageOrButtonId: buttonId) return true } diff --git a/Ecosia/Core/AdultFilter.swift b/Ecosia/Core/AdultFilter.swift new file mode 100644 index 000000000000..b9ce81a41e75 --- /dev/null +++ b/Ecosia/Core/AdultFilter.swift @@ -0,0 +1,12 @@ +// 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 Foundation + +public enum AdultFilter: String, Codable { + case + off = "n", + moderate = "i", + strict = "y" +} diff --git a/Ecosia/Core/AppDeviceInfo.swift b/Ecosia/Core/AppDeviceInfo.swift new file mode 100644 index 000000000000..0a0890530ffe --- /dev/null +++ b/Ecosia/Core/AppDeviceInfo.swift @@ -0,0 +1,48 @@ +// 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/ + +// 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 Foundation + +public struct AppDeviceInfo: Equatable { + + public let platform: String + public let bundleId: String + public let osVersion: String + public let deviceManufacturer: String + public let deviceModel: String + public let locale: String + public let country: String? + public let deviceBuildVersion: String? + public let appVersion: String + public let installReceipt: String? + public let adServicesAttributionToken: String? + + public init(platform: String, + bundleId: String, + osVersion: String, + deviceManufacturer: String, + deviceModel: String, + locale: String, + country: String? = nil, + deviceBuildVersion: String? = nil, + appVersion: String, + installReceipt: String? = nil, + adServicesAttributionToken: String? = nil) { + self.platform = platform + self.bundleId = bundleId + self.osVersion = osVersion + self.deviceManufacturer = deviceManufacturer + self.deviceModel = deviceModel + self.locale = locale + self.country = country + self.deviceBuildVersion = deviceBuildVersion + self.appVersion = appVersion + self.installReceipt = installReceipt + self.adServicesAttributionToken = adServicesAttributionToken + } +} diff --git a/Ecosia/Core/Bookmarks/Bookmark.swift b/Ecosia/Core/Bookmarks/Bookmark.swift new file mode 100644 index 000000000000..182f91a44b53 --- /dev/null +++ b/Ecosia/Core/Bookmarks/Bookmark.swift @@ -0,0 +1,33 @@ +// 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 Foundation + +public typealias Title = String +public typealias Url = String + +public struct BookmarkMetadata: Equatable { + let addedAt: Date? + let modifiedAt: Date? + + public static var empty: BookmarkMetadata { + BookmarkMetadata(addedAt: nil, modifiedAt: nil) + } + + internal var stringValue: String { + var returnValue = "" + if let addedAt = addedAt { + returnValue += " \(String.addDate)=\"\(Int(addedAt.timeIntervalSince1970))\"" + } + if let modifiedAt = modifiedAt { + returnValue += " \(String.lastModified)=\"\(Int(modifiedAt.timeIntervalSince1970))\"" + } + return returnValue + } +} + +public enum BookmarkItem: Equatable { + case folder(Title, [BookmarkItem], BookmarkMetadata) + case bookmark(Title, Url, BookmarkMetadata) +} diff --git a/Ecosia/Core/Bookmarks/BookmarkParser.swift b/Ecosia/Core/Bookmarks/BookmarkParser.swift new file mode 100644 index 000000000000..67b4a5a550a3 --- /dev/null +++ b/Ecosia/Core/Bookmarks/BookmarkParser.swift @@ -0,0 +1,98 @@ +// 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 Foundation +import SwiftSoup + +private typealias DateTag = String + +public enum BookmarkParserError: Error { + case noLeadingDL, noBody, cancelled +} + +public protocol BookmarkParseable { + func parseBookmarks() async throws -> [BookmarkItem] +} + +public class BookmarkParser: BookmarkParseable { + private let document: Document + private let dispatchQueue: DispatchQueue + + public init(html: String, dispatchQueue: DispatchQueue = .init(label: "org.ecosia.ios-core.bookmarks")) throws { + let document = try SwiftSoup.parse(html) + self.document = try document.normalizedDocumentIfRequired() + self.dispatchQueue = dispatchQueue + } + + public func parseBookmarks() async throws -> [BookmarkItem] { + try await withCheckedThrowingContinuation { continuation in + dispatchQueue.async { [weak self] in + guard let self = self else { + return continuation.resume(throwing: BookmarkParserError.cancelled) + } + do { + let result = try self.parse(element: try self.document.getLeadingDL()) + continuation.resume(with: .success(result)) + } catch { + continuation.resume(throwing: error) + } + } + } + } +} + +private extension BookmarkParser { + func parse(element: Element) throws -> [BookmarkItem] { + var items = [BookmarkItem]() + + let children = try element.getLeadingDL() + .children() + .filter({ try $0.select(.dt).hasText() }) /// only
is a valid bookmark/folder element + + for child in children { + let h3 = try child.select(.h3) + if let nextFolderItem = h3.first() { + guard let title = try? nextFolderItem.text() else { continue } + items.append(.folder(title, try parse(element: child), h3.extractBookmarkMetadata())) + continue /// item is a folder, don't process as bookmark + } + + let link = try child.select(.a) + let href = try link.attr(.href) + let title = try link.text() + + items.append(.bookmark(title, href, link.extractBookmarkMetadata())) + } + + return items + } +} + +private extension Elements { + func extractDate(_ tag: DateTag) -> Date? { + guard + let timeIntervalString = try? attr(tag), + let timeInterval = TimeInterval(timeIntervalString) + else { + return nil + } + return Date(timeIntervalSince1970: timeInterval) + } + + func extractBookmarkMetadata() -> BookmarkMetadata { + BookmarkMetadata( + addedAt: extractDate(.addDate), + modifiedAt: extractDate(.lastModified) + ) + } +} + +private extension Element { + func getLeadingDL() throws -> Element { + guard let leadingDL = try select(.dl).first() else { + throw BookmarkParserError.noLeadingDL + } + return leadingDL + } +} diff --git a/Ecosia/Core/Bookmarks/BookmarkSerializer.swift b/Ecosia/Core/Bookmarks/BookmarkSerializer.swift new file mode 100644 index 000000000000..06892f5f114f --- /dev/null +++ b/Ecosia/Core/Bookmarks/BookmarkSerializer.swift @@ -0,0 +1,85 @@ +// 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 Foundation +import SwiftSoup + +public enum BookmarkSerializerError: Error { + case cancelled +} + +public protocol BookmarkSerializable { + func serializeBookmarks(_ bookmarks: [BookmarkItem]) async throws -> String +} + +public class BookmarkSerializer: BookmarkSerializable { + private let dispatchQueue: DispatchQueue + + public init(dispatchQueue: DispatchQueue = .init(label: "org.ecosia.ios-core.bookmarks")) { + self.dispatchQueue = dispatchQueue + } + + public func serializeBookmarks(_ bookmarks: [BookmarkItem]) async throws -> String { + try await withCheckedThrowingContinuation { continuation in + dispatchQueue.async { [weak self] in + guard let self = self else { + return continuation.resume(throwing: BookmarkSerializerError.cancelled) + } + /// The trailing open

tag is part of the Netscape Bookmark file syntax + var html = """ + + + Bookmarks +

Bookmarks

+

+ + """ + + for bookmark in bookmarks { + html += self.bookmarkBody(for: bookmark, indentation: 2) + } + + html += """ + + \(String.indent(by: 1))

+ + """ + + continuation.resume(returning: html) + } + } + } + + func bookmarkBody(for bookmark: BookmarkItem, indentation: Int) -> String { + switch bookmark { + case let .bookmark(title, url, metadata): + return """ + \(String.indent(by: indentation))

\(Entities.escape(title)) + """ + case let .folder(title, children, metadata): + let start = """ + \(String.indent(by: indentation))
\(Entities.escape(title)) + \(String.indent(by: indentation))

+ + """ + + let body = children.map { + bookmarkBody(for: $0, indentation: indentation + 1) + }.joined(separator: "\n") + + let end = """ + + \(String.indent(by: indentation))

+ """ + + return start + body + end + } + } +} + +private extension String { + static func indent(by level: Int) -> String { + return String(repeating: "\t", count: level) + } +} diff --git a/Ecosia/Core/Bookmarks/Document+Safari.swift b/Ecosia/Core/Bookmarks/Document+Safari.swift new file mode 100644 index 000000000000..0ada78a429ed --- /dev/null +++ b/Ecosia/Core/Bookmarks/Document+Safari.swift @@ -0,0 +1,33 @@ +// 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 SwiftSoup + +extension Document { + var isSafariFormat: Bool { + if let leadingDL = try? select(.dl).first(), leadingDL.parents().size() > 2 { + return true + } + return false + } + + func normalizedDocumentIfRequired() throws -> Document { + isSafariFormat ? try normalizedSafariExport() : self + } +} + +private extension Document { + func normalizedSafariExport() throws -> Document { + guard let body = try select("body").first() else { + throw BookmarkParserError.noBody + } + + let newDocument = Document("") + let dlElement = try newDocument.appendElement("DL") + let bodyChildren = body.getChildNodes() + try dlElement.insertChildren(0, bodyChildren) + + return newDocument + } +} diff --git a/Ecosia/Core/Bookmarks/String+CssQuery.swift b/Ecosia/Core/Bookmarks/String+CssQuery.swift new file mode 100644 index 000000000000..135c99f6c2e7 --- /dev/null +++ b/Ecosia/Core/Bookmarks/String+CssQuery.swift @@ -0,0 +1,15 @@ +// 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 Foundation + +extension String { + static let dt = "DT" + static let dl = "DL" + static let h3 = "H3" + static let a = "A" + static let href = "href" + static let addDate = "ADD_DATE" + static let lastModified = "LAST_MODIFIED" +} diff --git a/Ecosia/Core/Bundle.swift b/Ecosia/Core/Bundle.swift new file mode 100644 index 000000000000..f058704429f4 --- /dev/null +++ b/Ecosia/Core/Bundle.swift @@ -0,0 +1,11 @@ +// 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 Foundation + +extension Bundle { + public static let version = marketing + "." + bundle + private static let marketing = main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "4.0.20" + private static let bundle = main.infoDictionary?["CFBundleVersion"] as? String ?? "840" +} diff --git a/Ecosia/Core/CloudFlareKeyProvider.swift b/Ecosia/Core/CloudFlareKeyProvider.swift new file mode 100644 index 000000000000..7bd2af2492f3 --- /dev/null +++ b/Ecosia/Core/CloudFlareKeyProvider.swift @@ -0,0 +1,12 @@ +// 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 Foundation + +struct CloudflareKeyProvider { + static let clientId = "CF-Access-Client-Id" + static let clientSecret = "CF-Access-Client-Secret" + + private init() {} +} diff --git a/Ecosia/Core/Cookie.swift b/Ecosia/Core/Cookie.swift new file mode 100644 index 000000000000..8888f50dfa90 --- /dev/null +++ b/Ecosia/Core/Cookie.swift @@ -0,0 +1,186 @@ +// 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 Foundation + +public enum Cookie: String { + case main + case consent + + // MARK: - Init + /// Initialize Cookie enum based on the name + init?(_ name: String) { + switch name { + case "ECFG": + self = .main + case "ECCC": + self = .consent + default: + return nil + } + } + + // MARK: - Main Specific Properties + + private struct MainCookieProperties { + static let userId = "cid" + static let suggestions = "as" + static let personalized = "pz" + static let customSettings = "cs" + static let adultFilter = "f" + static let marketCode = "mc" + static let treeCount = "t" + static let language = "l" + static let marketApplied = "ma" + static let marketReapplied = "mr" + static let deviceType = "dt" + static let firstSearch = "fs" + static let addon = "a" + } + + // MARK: - Common Properties + + var name: String { + switch self { + case .main: + return "ECFG" + case .consent: + return "ECCC" + } + } + + /// Values for incognito mode cookies. + static var incognitoValues: [String: String] { + var values = [String: String]() + values[MainCookieProperties.adultFilter] = User.shared.adultFilter.rawValue + values[MainCookieProperties.marketCode] = User.shared.marketCode.rawValue + values[MainCookieProperties.language] = Language.current.rawValue + values[MainCookieProperties.suggestions] = .init(NSNumber(value: User.shared.autoComplete).intValue) + values[MainCookieProperties.personalized] = .init(NSNumber(value: User.shared.personalized).intValue) + values[MainCookieProperties.marketApplied] = "1" + values[MainCookieProperties.marketReapplied] = "1" + values[MainCookieProperties.deviceType] = "mobile" + values[MainCookieProperties.firstSearch] = "0" + values[MainCookieProperties.addon] = "1" + return values + } + + /// Values for standard mode cookies. + static var standardValues: [String: String] { + var values = incognitoValues + values[MainCookieProperties.userId] = User.shared.id + values[MainCookieProperties.treeCount] = .init(User.shared.searchCount) + return values + } + + // MARK: - Functions + + /// Creates an incognito mode ECFG cookie. + /// - Parameter urlProvider: Provides the URL information. + /// - Returns: An HTTPCookie configured for incognito mode. + public static func makeIncognitoCookie(_ urlProvider: URLProvider = Environment.current.urlProvider) -> HTTPCookie { + HTTPCookie(properties: [.name: Cookie.main.name, + .domain: ".\(urlProvider.domain ?? "")", + .path: "/", + .value: Cookie.incognitoValues.map { $0.0 + "=" + $0.1 }.joined(separator: ":")])! + } + + /// Creates a standard mode ECFG cookie. + /// - Parameter urlProvider: Provides the URL information. + /// - Returns: An HTTPCookie configured for standard mode. + public static func makeStandardCookie(_ urlProvider: URLProvider = Environment.current.urlProvider) -> HTTPCookie { + HTTPCookie(properties: [.name: Cookie.main.name, + .domain: ".\(urlProvider.domain ?? "")", + .path: "/", + .value: Cookie.standardValues.map { $0.0 + "=" + $0.1 }.joined(separator: ":")])! + } + + public static func makeConsentCookie(_ urlProvider: URLProvider = Environment.current.urlProvider) -> HTTPCookie? { + guard let cookieConsentValue = User.shared.cookieConsentValue else { return nil } + return HTTPCookie(properties: [.name: Cookie.consent.name, + .domain: ".\(urlProvider.domain ?? "")", + .path: "/", + .value: cookieConsentValue]) + } + + /// Processes received cookies. + /// - Parameters: + /// - cookies: An array of HTTPCookie objects. + /// - urlProvider: Provides the URL information. + public static func received(_ cookies: [HTTPCookie], urlProvider: URLProvider = Environment.current.urlProvider) { + cookies.forEach { cookie in + guard let cookieType = Cookie(cookie.name), cookie.domain.contains(".\(urlProvider.domain ?? "")") else { return } + cookieType.extract(cookie) + } + } + + /// Processes received cookies from an HTTP response. + /// - Parameters: + /// - response: An HTTPURLResponse object. + /// - urlProvider: Provides the URL information. + public static func received(_ response: HTTPURLResponse, urlProvider: URLProvider = Environment.current.urlProvider) { + (response.allHeaderFields as? [String: String]).map { + HTTPCookie.cookies(withResponseHeaderFields: $0, for: urlProvider.root) + }.map { received($0, urlProvider: urlProvider) } + } + + /// Extracts and handles ECFG specific properties. + /// - Parameter properties: A dictionary of cookie properties. + private func extractECFG(_ properties: [String: String]) { + var user = User.shared + + properties[MainCookieProperties.userId].map { + user.id = $0 + } + + properties[MainCookieProperties.treeCount].flatMap(Int.init).map { + // tree count should only increase or be reset to 0 on logout + if $0 == 0 || $0 > user.searchCount { + user.searchCount = $0 + } + } + + properties[MainCookieProperties.marketCode].flatMap(Local.init).map { + user.marketCode = $0 + } + + properties[MainCookieProperties.adultFilter].flatMap(AdultFilter.init).map({ + user.adultFilter = $0 + }) + + properties[MainCookieProperties.personalized].flatMap(Int.init).flatMap(NSNumber.init).flatMap(Bool.init).map({ + user.personalized = $0 + }) + + properties[MainCookieProperties.suggestions].map({ + user.autoComplete = ($0 as NSString).boolValue + }) + + User.shared = user + } + + /// Extracts and handles ECCC specific properties. + /// - Parameter value: A string of cookie values expressed by a sequence of letters (e.g. `eampg`) + private func extractECCC(_ value: String) { + User.shared.cookieConsentValue = value + } + + /// Extracts properties from a cookie. + /// - Parameter cookie: An HTTPCookie object. + private func extract(_ cookie: HTTPCookie) { + + switch self { + case .main: + let properties = cookie.value.components(separatedBy: ":") + .map { $0.components(separatedBy: "=") } + .filter { $0.count == 2 } + .reduce(into: [:]) { result, item in + result[item[0]] = item[1] + } + extractECFG(properties) + case .consent: + extractECCC(cookie.value) + } + } +} diff --git a/Ecosia/Core/Date+TimestampProvider.swift b/Ecosia/Core/Date+TimestampProvider.swift new file mode 100644 index 000000000000..c2dcb0f03812 --- /dev/null +++ b/Ecosia/Core/Date+TimestampProvider.swift @@ -0,0 +1,11 @@ +// 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 Foundation + +extension Date: TimestampProvider { + public var currentTimestamp: TimeInterval { + return self.timeIntervalSince1970 + } +} diff --git a/Ecosia/Core/Encodable+Dictionary.swift b/Ecosia/Core/Encodable+Dictionary.swift new file mode 100644 index 000000000000..8676e6033f6b --- /dev/null +++ b/Ecosia/Core/Encodable+Dictionary.swift @@ -0,0 +1,11 @@ +// 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 Foundation + +extension Encodable { + public var dictionary: [String: String] { + (try? JSONSerialization.jsonObject(with: JSONEncoder().encode(self))).flatMap { $0 as? [String: String] } ?? [String: String]() + } +} diff --git a/Ecosia/Core/Environment/Environment.Auth.swift b/Ecosia/Core/Environment/Environment.Auth.swift new file mode 100644 index 000000000000..83ae61ac05a3 --- /dev/null +++ b/Ecosia/Core/Environment/Environment.Auth.swift @@ -0,0 +1,27 @@ +// 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 Foundation + +extension Environment { + struct Auth: Equatable { + let id: String + let secret: String + } + + var auth: Auth? { + switch self { + case .staging: + let keyId = "CF_ACCESS_CLIENT_ID" + let keySecret = "CF_ACCESS_CLIENT_SECRET" + + guard let id = EnvironmentFetcher.valueFromMainBundleOrProcessInfo(forKey: keyId), + let secret = EnvironmentFetcher.valueFromMainBundleOrProcessInfo(forKey: keySecret) else { return nil } + return Auth(id: id, secret: secret) + + default: + return nil + } + } +} diff --git a/Ecosia/Core/Environment/Environment.swift b/Ecosia/Core/Environment/Environment.swift new file mode 100644 index 000000000000..19c9cbe05efc --- /dev/null +++ b/Ecosia/Core/Environment/Environment.swift @@ -0,0 +1,33 @@ +// 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 Foundation + +public enum Environment: Equatable { + case production + case staging +} + +extension Environment { + + public static var current: Environment { + #if MOZ_CHANNEL_RELEASE + return .production + #else + return .staging + #endif + } +} + +extension Environment { + + public var urlProvider: URLProvider { + switch self { + case .production: + return .production + case .staging: + return .staging + } + } +} diff --git a/Ecosia/Core/Environment/URLProvider.swift b/Ecosia/Core/Environment/URLProvider.swift new file mode 100644 index 000000000000..cadd287c280d --- /dev/null +++ b/Ecosia/Core/Environment/URLProvider.swift @@ -0,0 +1,158 @@ +// 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 Foundation + +public enum URLProvider { + + case production + case staging + + public var root: URL { + switch self { + case .production: + return URL(string: "https://www.ecosia.org")! + case .staging: + return URL(string: "https://www.ecosia-staging.xyz")! + } + } + + public var domain: String? { + if let urlComponents = URLComponents(string: root.absoluteString) { + if let host = urlComponents.host { + let domain = host.replacingOccurrences(of: "www.", with: "") + return domain + } + } + return nil + } + + public var apiRoot: URL { + switch self { + case .production: + return URL(string: "https://api.ecosia.org")! + case .staging: + return URL(string: "https://api.ecosia-staging.xyz")! + } + } + + public var snowplow: String { + switch self { + case .production: + return "sp.ecosia.org" + case .staging: + return "org-ecosia-prod1.mini.snplow.net" + } + } + + var unleash: String { + switch self { + case .production: + return "prod" + case .staging: + return "staging" + } + } + + public var brazeEndpoint: String { + "sdk.fra-02.braze.eu" + } + + public var statistics: URL { + URL(string: "https://d2wfixp891z15b.cloudfront.net")! + } + + public var financialReportsData: URL { + URL(string: "https://s3.amazonaws.com/blog-en.ecosia.org/financial-reports/data.json")! + } + + public var privacy: URL { + URL(string: "https://www.ecosia.org/privacy")! + } + + public var faq: URL { + URL(string: "https://ecosia.helpscoutdocs.com/")! + } + + public var terms: URL { + URL(string: "https://docs.google.com/a/ecosia.org/document/d/1x31-MsVMcl17dK3k80IRPxIj5ZKQEHZPXRhU01H_Xfw/pub")! + } + + public var aboutCounter: URL { + URL(string: "https://ecosia.helpscoutdocs.com/article/369-impact-counter")! + } + + public var bookmarksHelp: URL { + URL(string: "https://ecosia.helpscoutdocs.com/article/458-import-export-bookmarks")! + } + + public var referHelp: URL { + URL(string: "https://ecosia.helpscoutdocs.com/article/358-refer-a-friend-ios-only")! + } + + public var financialReports: URL { + switch Language.current { + case .de: + return blog.appendingPathComponent("ecosia-finanzberichte-baumplanzbelege/") + case .fr: + return blog.appendingPathComponent("rapports-financiers-recus-de-plantations-arbres/") + default: + return blog.appendingPathComponent("ecosia-financial-reports-tree-planting-receipts/") + } + } + + public var blog: URL { + switch Language.current { + case .de: + return URL(string: "https://de.blog.ecosia.org/")! + case .fr: + return URL(string: "https://fr.blog.ecosia.org/")! + default: + return URL(string: "https://blog.ecosia.org/")! + } + } + + public var trees: URL { + switch Language.current { + case .de: + return blog.appendingPathComponent("tag/projekte/") + case .fr: + return blog.appendingPathComponent("tag/projets/") + default: + return blog.appendingPathComponent("tag/where-does-ecosia-plant-trees/") + } + } + + public var betaProgram: URL { + switch Language.current { + case .de: + return URL(string: "https://ecosia.typeform.com/to/catmFLuA")! + case .fr: + return URL(string: "https://ecosia.typeform.com/to/oaFZzT0F")! + default: + return URL(string: "https://ecosia.typeform.com/to/EeMLqL3X")! + } + } + + public var betaFeedback: URL { + switch Language.current { + case .de: + return URL(string: "https://ecosia.typeform.com/to/pIQ3uwp9")! + case .fr: + return URL(string: "https://ecosia.typeform.com/to/PRw7550n")! + default: + return URL(string: "https://ecosia.typeform.com/to/LlUGlFT9")! + } + } + + public var notifications: URL { + var components = URLComponents(url: URL(string: "https://api.ecosia.org/v1/notifications")!, resolvingAgainstBaseURL: false)! + components.queryItems = [ + .init(name: "language", value: Language.current.rawValue), + .init(name: "market", value: User.shared.marketCode.rawValue), + .init(name: "limit", value: "50") + ] + return components.url! + } +} diff --git a/Ecosia/Core/EnvironmentFetcher.swift b/Ecosia/Core/EnvironmentFetcher.swift new file mode 100644 index 000000000000..88408703d801 --- /dev/null +++ b/Ecosia/Core/EnvironmentFetcher.swift @@ -0,0 +1,28 @@ +// 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 Foundation + +public struct EnvironmentFetcher { + + private init() {} + + /// Fetches a string value associated with the specified key either from the Main Bundle Info Dictionary or the Process Info environment. + /// + /// - Parameters: + /// - key: The key for which to retrieve the associated string value. + /// - Returns: The string value associated with the key, or nil if not found. + public static func valueFromMainBundleOrProcessInfo(forKey key: String) -> String? { + // Attempt to retrieve the value from the Main Bundle Info Dictionary + // If not found, try to retrieve it from the Process Info environment + guard let value = Bundle.main.object(forInfoDictionaryKey: key) as? String + ?? ProcessInfo.processInfo.environment[key] else { + // Return nil if the value is not found in either location + return nil + } + + // Return the retrieved value + return value + } +} diff --git a/Ecosia/Core/Favourites.swift b/Ecosia/Core/Favourites.swift new file mode 100644 index 000000000000..a4b65ab94589 --- /dev/null +++ b/Ecosia/Core/Favourites.swift @@ -0,0 +1,17 @@ +// 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 Foundation + +public final class Favourites { + public var items = [Page]() { + didSet { + PageStore.save(favourites: items) + } + } + + public init() { + items = PageStore.favourites + } +} diff --git a/Ecosia/Core/FeatureManagement/FeatureManagementSessionInitializer.swift b/Ecosia/Core/FeatureManagement/FeatureManagementSessionInitializer.swift new file mode 100644 index 000000000000..97ac8bf14d69 --- /dev/null +++ b/Ecosia/Core/FeatureManagement/FeatureManagementSessionInitializer.swift @@ -0,0 +1,10 @@ +// 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 Foundation + +public protocol FeatureManagementSessionInitializer { + + func startSession() async throws -> T? +} diff --git a/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/AppUpdateRule.swift b/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/AppUpdateRule.swift new file mode 100644 index 000000000000..0d07febbb7d9 --- /dev/null +++ b/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/AppUpdateRule.swift @@ -0,0 +1,18 @@ +// 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 Foundation + +struct AppUpdateRule: RefreshingRule { + + private var currentAppVersion: String? + + init(appVersion: String) { + self.currentAppVersion = appVersion + } + + var shouldRefresh: Bool { + currentAppVersion != Unleash.model.appVersion + } +} diff --git a/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/DeviceRegionChangeRule.swift b/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/DeviceRegionChangeRule.swift new file mode 100644 index 000000000000..291e8dde4649 --- /dev/null +++ b/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/DeviceRegionChangeRule.swift @@ -0,0 +1,18 @@ +// 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 Foundation + +struct DeviceRegionChangeRule: RefreshingRule { + + private var currentRegion: String + + init(localeProvider: RegionLocatable = Locale.current) { + currentRegion = localeProvider.regionIdentifierLowercasedWithFallbackValue + } + + var shouldRefresh: Bool { + currentRegion != Unleash.model.deviceRegion + } +} diff --git a/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/RefreshingRule.swift b/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/RefreshingRule.swift new file mode 100644 index 000000000000..cf8a916559e8 --- /dev/null +++ b/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/RefreshingRule.swift @@ -0,0 +1,7 @@ +// 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/ + +protocol RefreshingRule { + var shouldRefresh: Bool { get } +} diff --git a/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/TimeBasedRefreshingRule.swift b/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/TimeBasedRefreshingRule.swift new file mode 100644 index 000000000000..7a976b60c23f --- /dev/null +++ b/Ecosia/Core/FeatureManagement/Unleash/RefreshingComponent/RefreshingRule/TimeBasedRefreshingRule.swift @@ -0,0 +1,20 @@ +// 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 Foundation + +struct TimeBasedRefreshingRule: RefreshingRule { + + let interval: TimeInterval + let timestampProvider: TimestampProvider + + init(interval: TimeInterval, timestampProvider: TimestampProvider = Date()) { + self.interval = interval + self.timestampProvider = timestampProvider + } + + var shouldRefresh: Bool { + return timestampProvider.currentTimestamp - Unleash.model.updated.timeIntervalSince1970 > interval + } +} diff --git a/Ecosia/Core/FeatureManagement/Unleash/Unleash+RefreshComponent.swift b/Ecosia/Core/FeatureManagement/Unleash/Unleash+RefreshComponent.swift new file mode 100644 index 000000000000..b88b7b0f27aa --- /dev/null +++ b/Ecosia/Core/FeatureManagement/Unleash/Unleash+RefreshComponent.swift @@ -0,0 +1,20 @@ +// 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 Foundation + +extension Unleash { + + /// Adds a refreshing rule to the `rules` array. + /// + /// This method checks if a rule of the same type already exists in the `rules` array + /// and adds the new rule only if it's not present. This prevents duplicate rules of the same type. + /// + /// - Parameter rule: The refreshing rule to be added. + static func addRule(_ rule: RefreshingRule) { + if !rules.contains(where: { type(of: $0) == type(of: rule) }) { + rules.append(rule) + } + } +} diff --git a/Ecosia/Core/FeatureManagement/Unleash/Unleash.Model.swift b/Ecosia/Core/FeatureManagement/Unleash/Unleash.Model.swift new file mode 100644 index 000000000000..32f284fbfafa --- /dev/null +++ b/Ecosia/Core/FeatureManagement/Unleash/Unleash.Model.swift @@ -0,0 +1,56 @@ +// 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 Foundation + +extension Unleash { + public struct Model: Codable { + public var id = UUID() + var toggles = Set() + var updated = Date(timeIntervalSince1970: 0) + var appVersion: String = "" + var deviceRegion: String = "" + public var etag: String = "" + + public subscript(_ name: Toggle.Name) -> Toggle? { + toggles.first { $0.name == name.rawValue } + } + } + + public struct Toggle: Codable, Hashable { + public enum Name: String { + case apnConsent = "mob_ios_apn_consent_on_launch_rollout" + case brazeIntegration = "mob_ios_braze_integration" + case configTest = "mob_ios_staging_config" + case seedCounterNTP = "mob_ios_seed_counter_ntp" + case newsletterCard = "mob_ios_newsletter_card" + } + + public let name: String + public let enabled: Bool + public let variant: Variant + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.name == rhs.name + } + + public func hash(into: inout Hasher) { + into.combine(name) + } + } + + public struct Variant: Codable { + public let name: String + public let enabled: Bool + public let payload: Payload? + } + + public struct Payload: Codable { + public let type, value: String + } + + struct FeatureResponse: Codable { + let toggles: [Toggle] + } +} diff --git a/Ecosia/Core/FeatureManagement/Unleash/Unleash.swift b/Ecosia/Core/FeatureManagement/Unleash/Unleash.swift new file mode 100644 index 000000000000..36735c84515f --- /dev/null +++ b/Ecosia/Core/FeatureManagement/Unleash/Unleash.swift @@ -0,0 +1,157 @@ +// 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 Foundation + +public protocol UnleashProtocol { + /// Checks if a toggle with the given name exists and is enabled. + /// - Parameter name: The name of the toggle. + /// - Returns: `true` if the toggle is enabled, `false` otherwise. + static func isEnabled(_ flag: Unleash.Toggle.Name) -> Bool +} + +public enum Unleash: UnleashProtocol { + + public typealias Context = [String: String] + + static var model = Model() + private static let queue = DispatchQueue(label: "com.ecosia.ModelManagerQueue") + static var rules: [RefreshingRule] = [] + static var currentDeviceRegion: String { + Locale.current.regionIdentifierLowercasedWithFallbackValue + } + + public static func queryParameters(appVersion: String) -> Context { + ["userId": model.id.uuidString, + "appName": "iOS", + "appVersion": appVersion, + "versionOnInstall": User.shared.versionOnInstall, + "environment": Environment.current.urlProvider.unleash, + "market": User.shared.marketCode.rawValue, + "deviceRegion": currentDeviceRegion, + "personalCounterSearches": "\(User.shared.searchCount)"] + } + + /// Starts the Unleash feature management session. + /// - Parameters: + /// - client: The HTTP client to use for network requests. Default is `URLSessionHTTPClient`. + /// - request: The base request to be used for the session. Default is `nil`. + /// - env: The environment for the session. Default is `.production`. + /// - force: Indicates whether to force a refresh even if the model is not expired. Default is `false`. + /// - Returns: The updated `Model` after starting the session. + /// - Throws: An error if the session fails to start or save the model. + public static func start(client: HTTPClient = URLSessionHTTPClient(), + request: BaseRequest? = nil, + env: Environment = .production, + appVersion: String) async throws -> Model { + return try await withCheckedThrowingContinuation({ continuation in + Self.queue.async { + Task { + do { + // Load from filesystem if not already happened + if model.updated.timeIntervalSince1970 == 0 { + await load().map({ Self.model = $0 }) + } + + // Call backend to refresh the model + Self.model = try await refresh(client: client, + request: request, + model: model, + env: env, + appVersion: appVersion) + + // Save the updated model to the filesystem + try await save(Self.model) + continuation.resume(returning: self.model) + } catch { + continuation.resume(throwing: error) + } + } + } + }) + } + + public static func isEnabled(_ name: Toggle.Name) -> Bool { + model[name]?.enabled ?? false + } + + /// Retrieves the variant of a toggle with the given name. + /// - Parameter name: The name of the toggle. + /// - Returns: The variant of the toggle if it exists, otherwise a default disabled variant. + public static func getVariant(_ name: Toggle.Name) -> Variant { + model[name]?.variant ?? Variant(name: "disabled", enabled: false, payload: nil) + } + + /// Refreshes the Unleash model by making a network request to the backend. + /// - Parameters: + /// - client: The HTTP client to use for network requests. Default is `URLSessionHTTPClient`. + /// - request: The base request to be used for refreshing the model. Default is `nil`. + /// - model: The current model. + /// - env: The environment for the session. + /// - appVersion: The package's hosting app version (`CFShortVersionString`) + /// - Returns: The latest available `Model` after refreshing. + /// - Throws: An error if the model fails to refresh or update. + static func refresh(client: HTTPClient = URLSessionHTTPClient(), + request: BaseRequest? = nil, + model: Model, + env: Environment, + appVersion: String) async throws -> Model { + + guard shouldRefresh else { + // Return the cached model if refresh is not required + return model + } + + // Create a request to refresh the model from the backend + let unleashRemoteRequest = UnleashStartRequest(etag: model.etag, queryParameters: Unleash.queryParameters(appVersion: appVersion)) + + // Initialize the Unleash feature management session + let unleashSessionInitializer = UnleashFeatureManagementSessionInitializer(client: client, + request: request ?? unleashRemoteRequest, + model: model) + + // Start the session and get the latest available model + var latestAvailableModel: Unleash.Model = try await unleashSessionInitializer.startSession()! + latestAvailableModel.updated = .init() + latestAvailableModel.appVersion = appVersion + latestAvailableModel.deviceRegion = currentDeviceRegion + return latestAvailableModel + } + + /// Resets the Unleash feature management session and returns the initial model. + /// - Parameters: + /// - client: The HTTP client to use for network requests. Default is `URLSessionHTTPClient`. + /// - env: The environment for the session. + /// - Returns: The initial `Model` after resetting the session. + /// - Throws: An error if the session fails to reset or save the model. + public static func reset(client: HTTPClient = URLSessionHTTPClient(), + env: Environment, + appVersion: String) async throws -> Model { + Self.model = .init() + try await save(Self.model) + let unleashRemoteRequest = UnleashStartRequest(etag: model.etag, queryParameters: Unleash.queryParameters(appVersion: appVersion)) + return try await start(client: client, + request: unleashRemoteRequest, + env: env, + appVersion: appVersion) + } + + /// Loads the model from the filesystem. + /// - Returns: The loaded `Model` if successful, otherwise `nil`. + static func load() async -> Model? { + try? JSONDecoder().decode(Model.self, from: .init(contentsOf: FileManager.unleash)) + } + + /// Saves the model to the filesystem. + /// - Parameter model: The model to be saved. + /// - Throws: An error if the model fails to be encoded or saved. + static func save(_ model: Model) async throws { + try JSONEncoder().encode(model).write(to: FileManager.unleash, options: .atomic) + } + + /// Determines whether the model should be refreshed based on its refreshing context providers. + static var shouldRefresh: Bool { + return rules.contains(where: { $0.shouldRefresh }) + } +} diff --git a/Ecosia/Core/FeatureManagement/Unleash/UnleashFeatureManagementSessionInitializer.swift b/Ecosia/Core/FeatureManagement/Unleash/UnleashFeatureManagementSessionInitializer.swift new file mode 100644 index 000000000000..3f0d6d7e6376 --- /dev/null +++ b/Ecosia/Core/FeatureManagement/Unleash/UnleashFeatureManagementSessionInitializer.swift @@ -0,0 +1,56 @@ +// 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 Foundation + +public final class UnleashFeatureManagementSessionInitializer: FeatureManagementSessionInitializer { + + private let client: HTTPClient + private let request: BaseRequest + private var model: Unleash.Model + var isSameModel: Bool = false + + public enum Error: Swift.Error { + case network + case noData + } + + public init(client: HTTPClient, request: BaseRequest, model: Unleash.Model) { + self.client = client + self.request = request + self.model = model + } + + public func startSession() async throws -> T? { + + let (data, response) = try await client.perform(request) + + guard let response else { + throw UnleashFeatureManagementSessionInitializer.Error.noData + } + + switch response.statusCode { + case 399...599: + throw UnleashFeatureManagementSessionInitializer.Error.network + case 304: // no changes reported by server -> return cached model + var updatedModel = model + updatedModel.updated = .init() + return updatedModel as? T + case 200: + break + default: + throw UnleashFeatureManagementSessionInitializer.Error.noData + } + + // read out Etag which identifies cached responses + (response.allHeaderFields["Etag"] as? String).map { model.etag = $0 } + + guard let remoteToggles = try? JSONDecoder().decode(Unleash.FeatureResponse.self, from: data) else { + return model as? T + } + + model.toggles = Set(remoteToggles.toggles) + return model as? T + } +} diff --git a/Ecosia/Core/FeatureManagement/Unleash/UnleashRefreshConfigurator.swift b/Ecosia/Core/FeatureManagement/Unleash/UnleashRefreshConfigurator.swift new file mode 100644 index 000000000000..bb693f685897 --- /dev/null +++ b/Ecosia/Core/FeatureManagement/Unleash/UnleashRefreshConfigurator.swift @@ -0,0 +1,31 @@ +// 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 Foundation + +public final class UnleashRefreshConfigurator { + + public init() {} + + @discardableResult + public func withAppUpdateCheckRule(appVersion: String) -> Self { + let appUpdateRule = AppUpdateRule(appVersion: appVersion) + Unleash.addRule(appUpdateRule) + return self + } + + @discardableResult + public func withTwentyFourHoursCacheExpirationRule() -> Self { + let timeRule = TimeBasedRefreshingRule(interval: TimeInterval.twentyFourHoursTimeInterval) + Unleash.addRule(timeRule) + return self + } + + @discardableResult + public func withDeviceRegionUpdateCheckRule(localeProvider: RegionLocatable = Locale.current) -> Self { + let regionRule = DeviceRegionChangeRule(localeProvider: localeProvider) + Unleash.addRule(regionRule) + return self + } +} diff --git a/Ecosia/Core/FeatureManagement/Unleash/UnleashStartRequest.swift b/Ecosia/Core/FeatureManagement/Unleash/UnleashStartRequest.swift new file mode 100644 index 000000000000..2fe4af52b834 --- /dev/null +++ b/Ecosia/Core/FeatureManagement/Unleash/UnleashStartRequest.swift @@ -0,0 +1,26 @@ +// 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 Foundation + +public struct UnleashStartRequest: BaseRequest { + + public var method: HTTPMethod { + .get + } + + public var path: String { + "/v2/toggles" + } + + var etag: String + + public var queryParameters: [String: String]? + + public var additionalHeaders: [String: String]? { + ["If-None-Match": etag] + } + + public var body: Data? +} diff --git a/Ecosia/Core/FileManager.swift b/Ecosia/Core/FileManager.swift new file mode 100644 index 000000000000..1c8503c11ce1 --- /dev/null +++ b/Ecosia/Core/FileManager.swift @@ -0,0 +1,25 @@ +// 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 Foundation + +extension FileManager { + static let pages = directory.appendingPathComponent("pages") + static let snapshots = directory.appendingPathComponent("snapshots") + static let user = directory.item("user") + static let tabs = pages.item("tabs") + static let currentTab = pages.item("currentTab") + static let favourites = pages.item("favourites") + static let history = pages.item("history") + static let unleash = directory.item("unleash") + static let news = directory.item("news") + + private static let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] +} + +private extension URL { + func item(_ name: String) -> URL { + appendingPathComponent(name + ".ecosia") + } +} diff --git a/Ecosia/Core/HTTPClient/HTTPClient.swift b/Ecosia/Core/HTTPClient/HTTPClient.swift new file mode 100644 index 000000000000..44c426305c26 --- /dev/null +++ b/Ecosia/Core/HTTPClient/HTTPClient.swift @@ -0,0 +1,12 @@ +// 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 Foundation + +public protocol HTTPClient { + + typealias Result = (Data, HTTPURLResponse?) + + func perform(_ request: BaseRequest) async throws -> Result +} diff --git a/Ecosia/Core/HTTPClient/HTTPMethod.swift b/Ecosia/Core/HTTPClient/HTTPMethod.swift new file mode 100644 index 000000000000..b36132eb8f50 --- /dev/null +++ b/Ecosia/Core/HTTPClient/HTTPMethod.swift @@ -0,0 +1,10 @@ +// 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 Foundation + +public enum HTTPMethod: String { + case get = "GET" + case post = "POST" +} diff --git a/Ecosia/Core/HTTPClient/Requestable/BaseRequest.swift b/Ecosia/Core/HTTPClient/Requestable/BaseRequest.swift new file mode 100644 index 000000000000..4318c2d9c79c --- /dev/null +++ b/Ecosia/Core/HTTPClient/Requestable/BaseRequest.swift @@ -0,0 +1,53 @@ +// 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 Foundation + +public protocol BaseRequest: Requestable {} + +enum RequestError: Error { + case invalidBaseURL + case invalidURLComponents +} + +public extension BaseRequest { + + var baseURL: URL { + environment.urlProvider.apiRoot + } + + var environment: Environment { + .current + } + + func makeURLRequest() throws -> URLRequest { + guard var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else { + throw RequestError.invalidBaseURL + } + urlComponents.path = path + if let queryParameters { + urlComponents.queryItems = queryParameters.map({ .init(name: $0.key, value: $0.value ) }) + } + guard let url = urlComponents.url else { + throw RequestError.invalidURLComponents + } + + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.cachePolicy = .reloadIgnoringLocalCacheData + request.httpBody = body + + if let auth = environment.auth { + request.setValue(auth.id, forHTTPHeaderField: CloudflareKeyProvider.clientId) + request.setValue(auth.secret, forHTTPHeaderField: CloudflareKeyProvider.clientSecret) + } + + if let additionalHeaders { + additionalHeaders.forEach({ request.setValue($0.value, forHTTPHeaderField: $0.key) }) + } + + return request + } +} diff --git a/Ecosia/Core/HTTPClient/Requestable/Requestable.swift b/Ecosia/Core/HTTPClient/Requestable/Requestable.swift new file mode 100644 index 000000000000..964681d2eaa1 --- /dev/null +++ b/Ecosia/Core/HTTPClient/Requestable/Requestable.swift @@ -0,0 +1,24 @@ +// 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 Foundation + +public protocol Requestable { + + var method: HTTPMethod { get } + + var baseURL: URL { get } + + var path: String { get } + + var environment: Environment { get } + + var queryParameters: [String: String]? { get set } + + var additionalHeaders: [String: String]? { get } + + var body: Data? { get set } + + func makeURLRequest() throws -> URLRequest +} diff --git a/Ecosia/Core/HTTPClient/URLSessionHTTPClient.swift b/Ecosia/Core/HTTPClient/URLSessionHTTPClient.swift new file mode 100644 index 000000000000..bef2c2e69248 --- /dev/null +++ b/Ecosia/Core/HTTPClient/URLSessionHTTPClient.swift @@ -0,0 +1,15 @@ +// 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 Foundation + +public final class URLSessionHTTPClient: HTTPClient { + + public init() {} + + public func perform(_ request: BaseRequest) async throws -> HTTPClient.Result { + let (data, response) = try await URLSession.shared.data(for: request.makeURLRequest()) + return (data, response as? HTTPURLResponse) + } +} diff --git a/Ecosia/Core/HTTPClient/URLSessionProtocol.swift b/Ecosia/Core/HTTPClient/URLSessionProtocol.swift new file mode 100644 index 000000000000..1c9ccf68d1fe --- /dev/null +++ b/Ecosia/Core/HTTPClient/URLSessionProtocol.swift @@ -0,0 +1,11 @@ +// 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 Foundation + +public protocol URLSessionProtocol { + func data(from url: URL) async throws -> (Data, URLResponse) +} + +extension URLSession: URLSessionProtocol { } diff --git a/Ecosia/Core/History.swift b/Ecosia/Core/History.swift new file mode 100644 index 000000000000..a2cc5b5a01d9 --- /dev/null +++ b/Ecosia/Core/History.swift @@ -0,0 +1,34 @@ +// 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 Foundation + +public final class History { + public var items: [(Date, Page)] { + get { dictionary.sorted { $0.0 < $1.0 }.map { ($0.0, $0.1) } } + set { dictionary = .init(uniqueKeysWithValues: newValue) } + } + + private(set) var dictionary = [Date: Page]() { + didSet { + PageStore.save(history: dictionary) + } + } + + public init() { + dictionary = PageStore.history + } + + public func add(_ page: Page) { + dictionary[Date()] = page + } + + public func delete(_ at: Date) { + dictionary.removeValue(forKey: at) + } + + public func deleteAll() { + dictionary = [:] + } +} diff --git a/Ecosia/Core/Images.swift b/Ecosia/Core/Images.swift new file mode 100644 index 000000000000..d265a8127a40 --- /dev/null +++ b/Ecosia/Core/Images.swift @@ -0,0 +1,67 @@ +// 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 Foundation + +public final class Images: Publisher { + public var subscriptions = [Subscription]() + var items = Set() + private let session: URLSession + + public init(_ session: URLSession) { + self.session = session + } + + public func load(_ subscriber: AnyObject, url: URL, closure: @escaping (Input) -> Void) { + subscribe(subscriber, closure: closure) + guard let item = items.first(where: { $0.url == url }) + else { + download(url) + return + } + send(item) + } + + public func cancellAll() { + session.getAllTasks { + $0.forEach { $0.cancel() } + } + } + + public func cancel(_ url: URL) { + session.getAllTasks { + $0.first { $0.originalRequest?.url == url }?.cancel() + } + } + + private func download(_ url: URL) { + session.dataTask(with: url) { data, _, _ in + DispatchQueue.main.async { [weak self] in + data.map { + let item = Item(url, $0) + self?.send(item) + self?.items.insert(item) + } + } + }.resume() + } + + public struct Item: Hashable { + public let url: URL + public let data: Data + + init(_ url: URL, _ data: Data) { + self.url = url + self.data = data + } + + public func hash(into: inout Hasher) { + into.combine(url) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.url == rhs.url + } + } +} diff --git a/Ecosia/Core/Language.swift b/Ecosia/Core/Language.swift new file mode 100644 index 000000000000..26b9c675b147 --- /dev/null +++ b/Ecosia/Core/Language.swift @@ -0,0 +1,45 @@ +// 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 Foundation + +public enum Language: String, Codable, CaseIterable { + case + de, + en, + es, + it, + fr, + nl, + sv + + public internal(set) static var current = make(for: .current) + + var locale: Local { + switch self { + case .de: return .de_de + case .en: return .en_us + case .es: return .es_es + case .it: return .it_it + case .fr: return .fr_fr + case .nl: return .nl_nl + case .sv: return .sv_se + } + } + + private static let queue = DispatchQueue(label: "\(Bundle.ecosia.bundleIdentifier!).LanguageQueue") + static func make(for locale: Locale) -> Self { + return queue.sync { + locale.withLanguage ?? .en + } + } +} + +private extension Locale { + var withLanguage: Ecosia.Language? { + languageCode.flatMap { + Ecosia.Language(rawValue: $0.lowercased()) + } + } +} diff --git a/Ecosia/Core/List.swift b/Ecosia/Core/List.swift new file mode 100644 index 000000000000..34472d6b332c --- /dev/null +++ b/Ecosia/Core/List.swift @@ -0,0 +1,22 @@ +// 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 Foundation + +struct List: Decodable where E: Decodable { + var items = [E]() + + init(from: Decoder) throws { + var root = try from.unkeyedContainer() + while !root.isAtEnd { + if let item = try? root.decode(E.self) { + items.append(item) + } else { + _ = try root.nestedContainer(keyedBy: Discard.self) + } + } + } +} + +private enum Discard: CodingKey { } diff --git a/Ecosia/Core/Local.swift b/Ecosia/Core/Local.swift new file mode 100644 index 000000000000..222d68f8a953 --- /dev/null +++ b/Ecosia/Core/Local.swift @@ -0,0 +1,92 @@ +// 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 Foundation + +public enum Local: String, Codable, CaseIterable { + case + es_ar = "es-ar", + en_au = "en-au", + de_at = "de-at", + fr_be = "fr-be", + nl_be = "nl-be", + pt_br = "pt-br", + bg_bg = "bg-bg", + en_ca = "en-ca", + fr_ca = "fr-ca", + es_cl = "es-cl", + zh_cn = "zh-cn", + es_co = "es-co", + hr_hr = "hr-hr", + da_dk = "da-dk", + fi_fi = "fi-fi", + fr_fr = "fr-fr", + de_de = "de-de", + zh_hk = "zh-hk", + en_in = "en-in", + en_id = "en-id", + en_ie = "en-ie", + it_it = "it-it", + ja_jp = "ja-jp", + lv_lv = "lv-lv", + lt_lt = "lt-lt", + en_my = "en-my", + es_mx = "es-mx", + nl_nl = "nl-nl", + en_nz = "en-nz", + nb_no = "nb-no", + es_pe = "es-pe", + en_ph = "en-ph", + pl_pl = "pl-pl", + pt_pt = "pt-pt", + ru_ru = "ru-ru", + ar_sa = "ar-sa", + en_sa = "en-sa", + en_sg = "en-sg", + sk_sk = "sk-sk", + en_za = "en-za", + ko_kr = "ko-kr", + es_es = "es-es", + sv_se = "sv-se", + de_ch = "de-ch", + fr_ch = "fr-ch", + zh_tw = "zh-tw", + en_th = "en-th", + th_th = "th-th", + tr_tr = "tr-tr", + uk_ua = "uk-ua", + en_gb = "en-gb", + en_us = "en-us", + es_us = "es-us", + es_ve = "es-ve", + en_vn = "en-vn", + vi_vn = "vi-vn", + cz_cz = "cz-cz", + ee_ee = "ee-ee", + gr_gr = "gr-gr", + ro_ro = "ro-ro", + en_ww = "en-ww" + + static func make(for locale: Locale) -> Self { + locale.local ?? locale.withCountry ?? locale.withRegion ?? .en_us + } +} + +private extension Locale { + var local: Local? { + Local(rawValue: identifier.lowercased()) + } + + var withCountry: Local? { + (self as NSLocale).countryCode.flatMap { code in + Local.allCases.first { $0.rawValue.hasSuffix(code.lowercased()) } + } + } + + var withRegion: Local? { + regionCode.flatMap { code in + Local.allCases.first { $0.rawValue.hasSuffix(code.lowercased()) } + } + } +} diff --git a/Ecosia/Core/Locale+Extensions.swift b/Ecosia/Core/Locale+Extensions.swift new file mode 100644 index 000000000000..abaf927c1cca --- /dev/null +++ b/Ecosia/Core/Locale+Extensions.swift @@ -0,0 +1,26 @@ +// 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 Foundation + +extension Locale { + + var identifierWithDashedLanguageAndRegion: String { + identifier.replacingOccurrences(of: "_", with: "-").lowercased() + } + + var regionIdentifier: String? { + if #available(iOS 16, macOS 13, *) { + return region?.identifier + } else { + return regionCode + } + } + + public var regionIdentifierLowercasedWithFallbackValue: String { + regionIdentifier?.lowercased() ?? "us" + } +} + +extension Locale: RegionLocatable {} diff --git a/Client/Ecosia/MMP/MMP.swift b/Ecosia/Core/MMP/MMP.swift similarity index 77% rename from Client/Ecosia/MMP/MMP.swift rename to Ecosia/Core/MMP/MMP.swift index 1234a10d7147..535a54eb8956 100644 --- a/Client/Ecosia/MMP/MMP.swift +++ b/Ecosia/Core/MMP/MMP.swift @@ -2,13 +2,23 @@ // 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/ +// 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 Foundation -import Core -import Shared import AdServices import Common -struct MMP { +public enum MMPEvent: String { + case onboardingStart = "onboarding_start" + case onboardingComplete = "onboarding_complete" + case firstSearch = "first_search" + case fifthSearch = "fifth_search" + case tenthSearch = "tenth_search" +} + +public struct MMP { private init() {} @@ -29,7 +39,7 @@ struct MMP { adServicesAttributionToken: AppInfo.adServicesAttributionToken) } - static func sendSession() { + public static func sendSession() { guard User.shared.sendAnonymousUsageData else { return } Task { @@ -42,7 +52,7 @@ struct MMP { } } - static func sendEvent(_ event: MMPEvent) { + public static func sendEvent(_ event: MMPEvent) { guard User.shared.sendAnonymousUsageData else { return } Task { @@ -55,7 +65,7 @@ struct MMP { } } - static func handleSearchEvent(_ count: Int) { + public static func handleSearchEvent(_ count: Int) { let eventMap: [Int: MMPEvent] = [1: .firstSearch, 5: .fifthSearch, 10: .tenthSearch] if let event = eventMap[count] { self.sendEvent(event) diff --git a/Ecosia/Core/MMP/MMPProvider.swift b/Ecosia/Core/MMP/MMPProvider.swift new file mode 100644 index 000000000000..c0fad972b527 --- /dev/null +++ b/Ecosia/Core/MMP/MMPProvider.swift @@ -0,0 +1,12 @@ +// 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 Foundation + +public protocol MMPProvider { + + func sendSessionInfo(appDeviceInfo: AppDeviceInfo) async throws + + func sendEvent(_ event: MMPEvent, appDeviceInfo: AppDeviceInfo) async throws +} diff --git a/Ecosia/Core/MMP/SKAdNetworkProtocol.swift b/Ecosia/Core/MMP/SKAdNetworkProtocol.swift new file mode 100644 index 000000000000..7b534cdbeb8a --- /dev/null +++ b/Ecosia/Core/MMP/SKAdNetworkProtocol.swift @@ -0,0 +1,39 @@ +// 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 Foundation +import StoreKit.SKAdNetwork + +#if os(iOS) + +public protocol SKAdNetworkProtocol { + @available(iOS, introduced: 11.3, deprecated: 15.4) + static func registerAppForAdNetworkAttribution() + @available(iOS, introduced: 14.0, deprecated: 15.4) + static func updateConversionValue(_ conversionValue: Int) + @available(iOS 15.4, *) + static func updatePostbackConversionValue(_ conversionValue: Int) async throws + @available(iOS 16.1, *) + static func updatePostbackConversionValue(_ fineValue: Int, coarseValue: Int?, lockWindow: Bool) async throws +} + +extension SKAdNetwork: SKAdNetworkProtocol { + @available(iOS 16.1, *) + public static func updatePostbackConversionValue(_ fineValue: Int, coarseValue: Int?, lockWindow: Bool) async throws { + var coarseConversionValue: CoarseConversionValue? + switch coarseValue { + case 0: coarseConversionValue = .low + case 1: coarseConversionValue = .medium + case 2: coarseConversionValue = .high + default: break + } + if let value = coarseConversionValue { + try await updatePostbackConversionValue(fineValue, coarseValue: value, lockWindow: lockWindow) + } else { + try await updatePostbackConversionValue(fineValue) + } + } +} + +#endif diff --git a/Ecosia/Core/MMP/Singular/Service/SingularConversionValueRequest.swift b/Ecosia/Core/MMP/Singular/Service/SingularConversionValueRequest.swift new file mode 100644 index 000000000000..9adea7302547 --- /dev/null +++ b/Ecosia/Core/MMP/Singular/Service/SingularConversionValueRequest.swift @@ -0,0 +1,53 @@ +// 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 Foundation + +struct SingularConversionValueRequest: BaseRequest { + + struct Parameters: Encodable { + let identifier: String + let platform: String + let bundleId: String + let eventName: String + let osVersion: String + let appVersion: String + + enum CodingKeys: String, CodingKey { + case identifier = "sing" + case platform = "p" + case bundleId = "i" + case eventName = "n" + case osVersion = "ve" + case appVersion = "app_v" + } + + init(identifier: String, eventName: String, appDeviceInfo: AppDeviceInfo) { + self.identifier = identifier + self.platform = appDeviceInfo.platform + self.bundleId = appDeviceInfo.bundleId + self.eventName = eventName + self.osVersion = appDeviceInfo.osVersion + self.appVersion = appDeviceInfo.appVersion + } + } + + var method: HTTPMethod { + .get + } + + var path: String { + "/v2/attribution/conversion-value" + } + + var queryParameters: [String: String]? + + var additionalHeaders: [String: String]? + + var body: Data? + + init(_ parameters: Parameters, skanParameters: [String: String]) { + self.queryParameters = parameters.dictionary.merging(skanParameters) { current, _ in current } + } +} diff --git a/Ecosia/Core/MMP/Singular/Service/SingularConversionValueResponse.swift b/Ecosia/Core/MMP/Singular/Service/SingularConversionValueResponse.swift new file mode 100644 index 000000000000..73e34508507b --- /dev/null +++ b/Ecosia/Core/MMP/Singular/Service/SingularConversionValueResponse.swift @@ -0,0 +1,21 @@ +// 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 Foundation + +struct SingularConversionValueResponse: Codable, Equatable { + let conversionValue: Int + let coarseValue: Int? + let lockWindow: Bool? + + enum CodingKeys: String, CodingKey { + case conversionValue = "conversion_value" + case coarseValue = "skan_updated_coarse_value" + case lockWindow = "skan_updated_lock_window_value" + } + + var isValid: Bool { + (0...63 ~= conversionValue) && (0...2 ~= coarseValue ?? 1) + } +} diff --git a/Ecosia/Core/MMP/Singular/Service/SingularEventRequest.swift b/Ecosia/Core/MMP/Singular/Service/SingularEventRequest.swift new file mode 100644 index 000000000000..23b7754268d7 --- /dev/null +++ b/Ecosia/Core/MMP/Singular/Service/SingularEventRequest.swift @@ -0,0 +1,50 @@ +// 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 Foundation + +#if os(iOS) + +struct SingularEventRequest: BaseRequest { + + struct Parameters: Encodable { + let identifier: String + let name: String + let platform: String + let bundleId: String + let osVersion: String + + enum CodingKeys: String, CodingKey { + case identifier = "sing" + case name = "n" + case platform = "p" + case bundleId = "i" + case osVersion = "ve" + } + } + + var method: HTTPMethod { + .get + } + + var path: String { + "/v2/attribution/event" + } + + var queryParameters: [String: String]? + + var additionalHeaders: [String: String]? + + var body: Data? + + init(identifier: String, name: String, info: AppDeviceInfo) { + self.queryParameters = Parameters(identifier: identifier, + name: name, + platform: info.platform, + bundleId: info.bundleId, + osVersion: info.osVersion).dictionary + } +} + +#endif diff --git a/Ecosia/Core/MMP/Singular/Service/SingularReponse.swift b/Ecosia/Core/MMP/Singular/Service/SingularReponse.swift new file mode 100644 index 000000000000..003d83a34b72 --- /dev/null +++ b/Ecosia/Core/MMP/Singular/Service/SingularReponse.swift @@ -0,0 +1,19 @@ +// 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 Foundation + +struct SingularResponse: Codable { + let status: String + let errorReason: String? + + enum CodingKeys: String, CodingKey { + case status + case errorReason = "reason" + } + + var isOK: Bool { + status == "ok" + } +} diff --git a/Ecosia/Core/MMP/Singular/Service/SingularService.swift b/Ecosia/Core/MMP/Singular/Service/SingularService.swift new file mode 100644 index 000000000000..9a683d4718b1 --- /dev/null +++ b/Ecosia/Core/MMP/Singular/Service/SingularService.swift @@ -0,0 +1,64 @@ +// 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 Foundation + +#if os(iOS) + +protocol SingularNotificationRequest: BaseRequest {} +extension SingularSessionInfoSendRequest: SingularNotificationRequest {} +extension SingularEventRequest: SingularNotificationRequest {} + +protocol SingularServiceProtocol { + func sendNotification(request: SingularNotificationRequest) async throws + func getConversionValue(request: SingularConversionValueRequest) async throws -> SingularConversionValueResponse +} + +final class SingularService: SingularServiceProtocol { + + enum Error: Swift.Error { + case network + case dataReturnedError(reason: String) + case noConversionValueReturned + } + + let client: HTTPClient + + init(client: HTTPClient = URLSessionHTTPClient()) { + self.client = client + } + + func sendNotification(request: SingularNotificationRequest) async throws { + let (data, response) = try await client.perform(request) + guard let response, response.statusCode == 200 else { + throw SingularService.Error.network + } + + let dataResult = try JSONDecoder().decode(SingularResponse.self, from: data) + + guard dataResult.isOK, dataResult.errorReason == nil else { + throw SingularService.Error.dataReturnedError(reason: dataResult.errorReason ?? "") + } + } + + func getConversionValue(request: SingularConversionValueRequest) async throws -> SingularConversionValueResponse { + let (data, response) = try await client.perform(request) + + guard let response, response.statusCode == 200 else { + throw SingularService.Error.network + } + + do { + return try JSONDecoder().decode(SingularConversionValueResponse.self, from: data) + } catch DecodingError.keyNotFound { + let fallbackResponse = try JSONDecoder().decode(SingularResponse.self, from: data) + if !fallbackResponse.isOK { + throw Error.dataReturnedError(reason: fallbackResponse.errorReason ?? "") + } + throw Error.noConversionValueReturned // Likely due to no conversion model active for the app + } + } +} + +#endif diff --git a/Ecosia/Core/MMP/Singular/Service/SingularSessionInfoSendRequest.swift b/Ecosia/Core/MMP/Singular/Service/SingularSessionInfoSendRequest.swift new file mode 100644 index 000000000000..6725fb10b6f7 --- /dev/null +++ b/Ecosia/Core/MMP/Singular/Service/SingularSessionInfoSendRequest.swift @@ -0,0 +1,79 @@ +// 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 Foundation + +#if os(iOS) + +struct SingularSessionInfoSendRequest: BaseRequest { + + struct Parameters: Encodable { + let identifier: String + let platform: String + let bundleId: String + let osVersion: String + let deviceManufacturer: String + let deviceModel: String + let locale: String + let country: String? + let deviceBuildVersion: String? + let appVersion: String + let installReceipt: String? + let attributionToken: String? + + enum CodingKeys: String, CodingKey { + case identifier = "sing" + case platform = "p" + case bundleId = "i" + case osVersion = "ve" + case deviceManufacturer = "ma" + case deviceModel = "mo" + case locale = "lc" + case country = "country" + case deviceBuildVersion = "bd" + case appVersion = "app_v" + case installReceipt = "install_receipt" + case attributionToken = "attribution_token" + } + } + + var method: HTTPMethod { + .get + } + + var path: String { + "/v2/attribution/launch" + } + + var queryParameters: [String: String]? + + var additionalHeaders: [String: String]? + + var body: Data? + + init(identifier: String, info: AppDeviceInfo, skanParameters: [String: String]?) { + var deviceBuildVersion: String? + if let deviceBuildVersionString = info.deviceBuildVersion { + deviceBuildVersion = #"Build\\#(deviceBuildVersionString)"# + } + var parameters = Parameters(identifier: identifier, + platform: info.platform, + bundleId: info.bundleId, + osVersion: info.osVersion, + deviceManufacturer: info.deviceManufacturer, + deviceModel: info.deviceModel, + locale: info.locale, + country: info.country, + deviceBuildVersion: deviceBuildVersion, + appVersion: info.appVersion, + installReceipt: info.installReceipt, + attributionToken: info.adServicesAttributionToken).dictionary + if let skanParameters = skanParameters { + parameters = parameters.merging(skanParameters) { (current, _) in current } + } + self.queryParameters = parameters + } +} + +#endif diff --git a/Ecosia/Core/MMP/Singular/Singular.swift b/Ecosia/Core/MMP/Singular/Singular.swift new file mode 100644 index 000000000000..86d04fc88841 --- /dev/null +++ b/Ecosia/Core/MMP/Singular/Singular.swift @@ -0,0 +1,62 @@ +// 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 Foundation + +#if os(iOS) + +public struct Singular: MMPProvider { + + let singularService: SingularServiceProtocol + let skanHelper: SingularAdNetworkHelperProtocol? + + init(singularService: SingularServiceProtocol = SingularService(), + skanHelper: SingularAdNetworkHelperProtocol? = SingularAdNetworkHelper()) { + self.singularService = singularService + self.skanHelper = skanHelper + } + + /// Initializer for Singular as MMPProvider. + /// - Parameters: + /// - includeSKAN: If true, all required logic for SKAdNetwork will be executed (e.g. register on first session or fetch updated conversion values from singular server before any event) + public init(includeSKAN: Bool) { + self.singularService = SingularService() + self.skanHelper = includeSKAN ? SingularAdNetworkHelper() : nil + } + + /// Reports a session to the Singular service. + /// - Parameters: + /// - appDeviceInfo: The device info parameters being set to Singular session endpoint. + public func sendSessionInfo(appDeviceInfo: AppDeviceInfo) async throws { + let sessionIdentifier = User.shared.analyticsId.uuidString + + var skanParameters: [String: String]? + if let skanHelper = skanHelper { + if skanHelper.isRegistered { + try? await skanHelper.fetchFromSingularServerAndUpdate(forEvent: .session, + sessionIdentifier: sessionIdentifier, + appDeviceInfo: appDeviceInfo) + } else { + try? await skanHelper.registerAppForAdNetworkAttribution() + } + + skanParameters = skanHelper.persistedValuesDictionary + } + + let request = SingularSessionInfoSendRequest(identifier: sessionIdentifier, info: appDeviceInfo, skanParameters: skanParameters) + try await singularService.sendNotification(request: request) + } + + /// Reports an event to the Singular service. + /// - Parameters: + /// - event: MMPEvent in question out of the supported cases + /// - appDeviceInfo: The device info parameters being set to Singular session endpoint. + public func sendEvent(_ event: MMPEvent, appDeviceInfo: AppDeviceInfo) async throws { + let sessionIdentifier = User.shared.analyticsId.uuidString + let request = SingularEventRequest(identifier: sessionIdentifier, name: event.rawValue, info: appDeviceInfo) + try await singularService.sendNotification(request: request) + } +} + +#endif diff --git a/Ecosia/Core/MMP/Singular/SingularAdNetworkHelper.swift b/Ecosia/Core/MMP/Singular/SingularAdNetworkHelper.swift new file mode 100644 index 000000000000..456108e6855b --- /dev/null +++ b/Ecosia/Core/MMP/Singular/SingularAdNetworkHelper.swift @@ -0,0 +1,197 @@ +// 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 Foundation +import StoreKit.SKAdNetwork + +#if os(iOS) + +protocol SingularAdNetworkHelperProtocol { + var persistedValuesDictionary: [String: String] { get } + var isRegistered: Bool { get } + + func registerAppForAdNetworkAttribution() async throws + func fetchFromSingularServerAndUpdate(forEvent event: SingularEvent, sessionIdentifier: String, appDeviceInfo: AppDeviceInfo) async throws +} + +struct SingularAdNetworkHelper: SingularAdNetworkHelperProtocol { + + enum PersistedObject: Hashable, CaseIterable { + static var allCases: [SingularAdNetworkHelper.PersistedObject] { + return [ + .firstSkanCallTimestamp, .lastSkanCallTimestamp, .conversionValue, .previousFineValue, + .coarseValue(window: .first), .coarseValue(window: .second), .coarseValue(window: .third), + .previousCoarseValue(window: .first), .previousCoarseValue(window: .second), .previousCoarseValue(window: .third), + .windowLockTimestamp(window: .first), .windowLockTimestamp(window: .second), .windowLockTimestamp(window: .third), + .errorCode + ] + } + + case firstSkanCallTimestamp + case lastSkanCallTimestamp + case conversionValue + case coarseValue(window: SkanWindow) + case previousFineValue // Only expected to exist for first window + case previousCoarseValue(window: SkanWindow) + case windowLockTimestamp(window: SkanWindow) + case errorCode + + var key: String { + switch self { + case .firstSkanCallTimestamp: return "first_skan_call_timestamp" + case .lastSkanCallTimestamp: return "last_skan_call_timestamp" + case .conversionValue: return "current_conversion_value" + case .coarseValue(let window): return "\(window.rawValue)_coarse_value" + case .previousFineValue: return "prev_fine_value" + case .previousCoarseValue(let window): return "\(window.rawValue)_prev_coarse_value" + case .windowLockTimestamp(let window): return "\(window.rawValue)_window_lock_timestamp" + case .errorCode: return "skan_error_code" + } + } + + var queryKey: String { + switch self { + case .firstSkanCallTimestamp: return "skan_first_call_to_skadnetwork_timestamp" + case .lastSkanCallTimestamp: return "skan_last_call_to_skadnetwork_timestamp" + case .conversionValue: return "skan_current_conversion_value" + case .coarseValue(let window): return "\(window.rawValue)_coarse" + case .previousFineValue: return "prev_fine_value" + case .previousCoarseValue(let window): return "\(window.rawValue)_prev_coarse_value" + case .windowLockTimestamp(let window): return "\(window.rawValue)_window_lock" + case .errorCode: return "_skerror" + } + } + } + + enum SkanWindow: String { + case first = "p0" + case second = "p1" + case third = "p2" + case over + + static let firstSkanWindowInSec = 3600 * 24 * 2 + static let secondSkanWindowInSec = 3600 * 24 * 7 + static let thirdSkanWindowInSec = 3600 * 24 * 35 + + init(timeDiff: Int) { + switch timeDiff { + case 0...SkanWindow.firstSkanWindowInSec: self = .first + case SkanWindow.firstSkanWindowInSec...SkanWindow.secondSkanWindowInSec: self = .second + case SkanWindow.secondSkanWindowInSec...SkanWindow.thirdSkanWindowInSec: self = .third + default: self = .over + } + } + } + + enum Error: Swift.Error { + case invalidConversionValues + } + + private var currentTimestamp: Int { + return Int(timestampProvider.currentTimestamp) + } + var persistedValuesDictionary: [String: String] { + var dictionary = [String: String]() + PersistedObject.allCases.forEach { obj in + if let value = getUserDefaultsInteger(for: obj) { + dictionary[obj.queryKey] = String(value) + } + } + return dictionary + } + var isRegistered: Bool { + return getUserDefaultsInteger(for: .firstSkanCallTimestamp) != nil + } + + private let skan: SKAdNetworkProtocol.Type + private let objectPersister: ObjectPersister + private let timestampProvider: TimestampProvider + private let singularService: SingularServiceProtocol + + init(skan: SKAdNetworkProtocol.Type = SKAdNetwork.self, + objectPersister: ObjectPersister = UserDefaults.standard, + timestampProvider: TimestampProvider = Date(), + singularService: SingularServiceProtocol = SingularService()) { + self.skan = skan + self.objectPersister = objectPersister + self.timestampProvider = timestampProvider + self.singularService = singularService + } + + func registerAppForAdNetworkAttribution() async throws { + guard !isRegistered else { return } + + if #available(iOS 15.4, *) { + do { + try await skan.updatePostbackConversionValue(0) + } catch { + setUserDefaults(for: .errorCode, value: (error as NSError).code) + } + } else { + skan.registerAppForAdNetworkAttribution() + } + setUserDefaults(for: .firstSkanCallTimestamp, value: currentTimestamp) + setUserDefaults(for: .lastSkanCallTimestamp, value: currentTimestamp) + persistUpdatedValues(fineValue: 0, coarseValue: nil) + } + + func fetchFromSingularServerAndUpdate(forEvent event: SingularEvent = .session, sessionIdentifier: String, appDeviceInfo: AppDeviceInfo) async throws { + let firstCallTimestamp = getUserDefaultsInteger(for: .firstSkanCallTimestamp) ?? 0 + guard SkanWindow(timeDiff: currentTimestamp - firstCallTimestamp) != .over else { + return + } + + let request = SingularConversionValueRequest(.init(identifier: sessionIdentifier, eventName: event.rawValue, appDeviceInfo: appDeviceInfo), + skanParameters: persistedValuesDictionary) + let response = try await singularService.getConversionValue(request: request) + if !response.isValid { + throw Error.invalidConversionValues + } + + let conversionValue = response.conversionValue + let coarseValue = response.coarseValue + let lockWindow = response.lockWindow ?? false + if #available(iOS 16.1, *) { + try? await skan.updatePostbackConversionValue(conversionValue, coarseValue: coarseValue, lockWindow: lockWindow) + } else if #available(iOS 15.4, *) { + try? await skan.updatePostbackConversionValue(conversionValue) + } else if #available(iOS 14, *) { + skan.updateConversionValue(conversionValue) + } + persistUpdatedValues(fineValue: conversionValue, + coarseValue: coarseValue, + lockWindow: lockWindow) + } + + private func persistUpdatedValues(fineValue: Int, coarseValue: Int?, lockWindow: Bool = false) { + let window = SkanWindow(timeDiff: currentTimestamp - (getUserDefaultsInteger(for: .firstSkanCallTimestamp) ?? 0)) + guard window != .over else { return } + + if window == .first { + let persistedFineValue: Int? = getUserDefaultsInteger(for: .conversionValue) + setUserDefaults(for: .conversionValue, value: fineValue) + setUserDefaults(for: .previousFineValue, value: persistedFineValue) + } + + let persistedCoarseValue: Int? = getUserDefaultsInteger(for: .coarseValue(window: window)) + setUserDefaults(for: .coarseValue(window: window), value: coarseValue) + setUserDefaults(for: .previousCoarseValue(window: window), value: persistedCoarseValue) + + if lockWindow { + setUserDefaults(for: .windowLockTimestamp(window: window), value: currentTimestamp) + } + + setUserDefaults(for: .lastSkanCallTimestamp, value: currentTimestamp) + } + + private func setUserDefaults(for object: PersistedObject, value: Int?) { + objectPersister.set(value, forKey: object.key) + } + + private func getUserDefaultsInteger(for object: PersistedObject) -> Int? { + return objectPersister.object(forKey: object.key) as? Int + } +} + +#endif diff --git a/Ecosia/Core/MMP/Singular/SingularEvent.swift b/Ecosia/Core/MMP/Singular/SingularEvent.swift new file mode 100644 index 000000000000..63b3af9da5bc --- /dev/null +++ b/Ecosia/Core/MMP/Singular/SingularEvent.swift @@ -0,0 +1,9 @@ +// 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 Foundation + +enum SingularEvent: String { + case session = "__SESSION__" +} diff --git a/Ecosia/Core/Market.swift b/Ecosia/Core/Market.swift new file mode 100644 index 000000000000..38eaafd3c6db --- /dev/null +++ b/Ecosia/Core/Market.swift @@ -0,0 +1,28 @@ +// 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 Foundation + +// Reference: https://github.com/ecosia/core/blob/main/common/js/universal/markets.js + +public struct Market: Decodable { + public let value: Local + public let label: String + public let languageInLabel: Bool + + public init(from decoder: Decoder) throws { + let root = try decoder.container(keyedBy: CodingKeys.self) + value = try root.decode(Local.self, forKey: .value) + label = try root.decode(String.self, forKey: .label) + if let stringValue = try? root.decode(String.self, forKey: .languageInLabel) { + languageInLabel = Bool(stringValue) ?? false + } else { + languageInLabel = false + } + } + + private enum CodingKeys: String, CodingKey { + case value, label, languageInLabel + } +} diff --git a/Ecosia/Core/News/News.swift b/Ecosia/Core/News/News.swift new file mode 100644 index 000000000000..9a2ebc92d555 --- /dev/null +++ b/Ecosia/Core/News/News.swift @@ -0,0 +1,84 @@ +// 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 Foundation + +public final class News: StatePublisher { + public var subscriptions = [Subscription<[NewsModel]>]() + public var state: [NewsModel]? { + items.sorted { $0.publishDate > $1.publishDate } + } + private let dispatch = DispatchQueue(label: "", qos: .utility) + private let characters = ["'": "'"] + + private var decoder: JSONDecoder { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .iso8601 + return decoder + } + + private(set) var items = Set() { + didSet { + DispatchQueue.main.async { [weak self ] in + self?.state.map { self?.send($0) } + } + } + } + + public init() { + dispatch.async { [weak self] in + self?.restore() + } + } + + var needsUpdate: Bool { + guard !items.isEmpty else { return true } + return Calendar.current.dateComponents([.day], from: User.shared.news, to: .init()).day! >= 1 + } + + public func load(session: URLSession, force: Bool = false) { + guard needsUpdate || force else { return } + session.dataTask(with: Environment.current.urlProvider.notifications) { [weak self] data, _, _ in + self?.dispatch.async { + guard + let data = data, + let new = try? self?.decoder.decode([NewsModel].self, from: data) + else { + return + } + let cleaned = new.compactMap { self?.clean($0) } + self?.items = .init(cleaned + (self?.items ?? [])) + self?.save() + } + }.resume() + } + + private func restore() { + dispatch.async { [weak self] in + if let news = try? JSONDecoder().decode([NewsModel].self, from: .init(contentsOf: FileManager.news)) { + self?.items = .init(news.filter { $0.language == Language.current }) + } + } + } + + private func save() { + dispatch.async { [weak self] in + guard let items = self?.items, !items.isEmpty else { return } + do { + try JSONEncoder().encode(items).write(to: FileManager.news, options: .atomic) + User.shared.news = Date() + } catch {} + } + } + + private func clean(_ item: NewsModel) -> NewsModel { + var item = item + item.text = item.text.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression) + item.text = characters.reduce(item.text) { text, char in + text.replacingOccurrences(of: char.0, with: char.1) + } + return item + } +} diff --git a/Ecosia/Core/News/NewsModel.swift b/Ecosia/Core/News/NewsModel.swift new file mode 100644 index 000000000000..aa1f0c87a4d2 --- /dev/null +++ b/Ecosia/Core/News/NewsModel.swift @@ -0,0 +1,23 @@ +// 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 Foundation + +public struct NewsModel: Codable, Hashable { + let id: Int + public internal(set) var text: String + public let language: Language + public let publishDate: Date + public let imageUrl: URL + public let targetUrl: URL + public let trackingName: String + + public func hash(into: inout Hasher) { + into.combine(id) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } +} diff --git a/Ecosia/Core/ObjectPersister.swift b/Ecosia/Core/ObjectPersister.swift new file mode 100644 index 000000000000..194e013a6960 --- /dev/null +++ b/Ecosia/Core/ObjectPersister.swift @@ -0,0 +1,10 @@ +// 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 Foundation + +protocol ObjectPersister { + func set(_ value: Any?, forKey: String) + func object(forKey: String) -> Any? +} diff --git a/Ecosia/Core/Pages/Page.swift b/Ecosia/Core/Pages/Page.swift new file mode 100644 index 000000000000..b098ca28b3a1 --- /dev/null +++ b/Ecosia/Core/Pages/Page.swift @@ -0,0 +1,15 @@ +// 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 Foundation + +public struct Page: Codable { + public let url: URL + public let title: String + + public init(url: URL, title: String) { + self.url = url + self.title = title + } +} diff --git a/Ecosia/Core/Pages/PageStore.swift b/Ecosia/Core/Pages/PageStore.swift new file mode 100644 index 000000000000..e5e4512c5615 --- /dev/null +++ b/Ecosia/Core/Pages/PageStore.swift @@ -0,0 +1,64 @@ +// 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 Foundation + +struct PageStore { + static let queue = DispatchQueue(label: "", qos: .utility) + + private init() { } + + static func save(tabs: [Tab]) { + queue.async { + createDirectory() + try? JSONEncoder().encode(tabs).write(to: FileManager.tabs, options: .atomic) + } + } + + static func save(currentTab: Int?) { + queue.async { + if let currentTab = currentTab { + createDirectory() + try? JSONEncoder().encode(currentTab).write(to: FileManager.currentTab, options: .atomic) + } else if FileManager.default.fileExists(atPath: FileManager.currentTab.path) { + try? FileManager.default.removeItem(at: FileManager.currentTab) + } + } + } + + static func save(favourites: [Page]) { + queue.async { + createDirectory() + try? JSONEncoder().encode(favourites).write(to: FileManager.favourites, options: .atomic) + } + } + + static func save(history: [Date: Page]) { + queue.async { + createDirectory() + try? JSONEncoder().encode(history).write(to: FileManager.history, options: .atomic) + } + } + + static var tabs: [Tab] { + (try? JSONDecoder().decode([Tab].self, from: .init(contentsOf: FileManager.tabs))) ?? [] + } + + static var currentTab: Int? { + try? JSONDecoder().decode(Int.self, from: .init(contentsOf: FileManager.currentTab)) + } + + static var favourites: [Page] { + (try? JSONDecoder().decode([Page].self, from: .init(contentsOf: FileManager.favourites))) ?? [] + } + + static var history: [Date: Page] { + (try? JSONDecoder().decode([Date: Page].self, from: .init(contentsOf: FileManager.history))) ?? [:] + } + + private static func createDirectory() { + guard !FileManager.default.fileExists(atPath: FileManager.pages.path) else { return } + try? FileManager.default.createDirectory(at: FileManager.pages, withIntermediateDirectories: true) + } +} diff --git a/Ecosia/Core/Publisher.swift b/Ecosia/Core/Publisher.swift new file mode 100644 index 000000000000..8c8d701e5f21 --- /dev/null +++ b/Ecosia/Core/Publisher.swift @@ -0,0 +1,42 @@ +// 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 Foundation + +public protocol Publisher: AnyObject { + associatedtype Input + var subscriptions: [Subscription] { get set } +} + +public extension Publisher { + func send(_ input: Input) { + subscriptions.removeAll { $0.subscriber == nil } + subscriptions.forEach { $0.closure(input) } + } + + func subscribe(_ subscriber: AnyObject, closure: @escaping (Input) -> Void) { + guard !subscriptions.contains(where: { $0.subscriber === subscriber }) else { return } + subscriptions.append(.init(subscriber: subscriber, closure: closure)) + } + + func unsubscribe(_ subscriber: AnyObject) { + subscriptions.removeAll { $0.subscriber === subscriber } + } +} + +public struct Subscription { + weak var subscriber: AnyObject? + let closure: (Input) -> Void +} + +public protocol StatePublisher: Publisher { + var state: Input? { get } +} + +public extension StatePublisher { + func subscribeAndReceive(_ subscriber: AnyObject, closure: @escaping (Input) -> Void) { + subscribe(subscriber, closure: closure) + state.map(closure) + } +} diff --git a/Ecosia/Core/Referrals/ReferralClaimRequest.swift b/Ecosia/Core/Referrals/ReferralClaimRequest.swift new file mode 100644 index 000000000000..d3d4d4530986 --- /dev/null +++ b/Ecosia/Core/Referrals/ReferralClaimRequest.swift @@ -0,0 +1,32 @@ +// 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 Foundation + +struct ReferralClaimRequest: BaseRequest { + struct Claim: Codable { + let referrer: String + let claim: String + + private enum CodingKeys: String, CodingKey { + case + referrer = "referral_code", + claim = "claim_code" + } + } + + var method: HTTPMethod { .post } + + var path: String { "/v1/referrals/claim/" } + + var queryParameters: [String: String]? + + var additionalHeaders: [String: String]? + + var body: Data? + + init(referrer: String, claim: String) { + self.body = try? JSONEncoder().encode(Claim(referrer: referrer, claim: claim)) + } +} diff --git a/Ecosia/Core/Referrals/ReferralCreateCodeRequest.swift b/Ecosia/Core/Referrals/ReferralCreateCodeRequest.swift new file mode 100644 index 000000000000..a791a8fc9c33 --- /dev/null +++ b/Ecosia/Core/Referrals/ReferralCreateCodeRequest.swift @@ -0,0 +1,17 @@ +// 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 Foundation + +struct ReferralCreateCodeRequest: BaseRequest { + var method: HTTPMethod { .post } + + var path: String { "/v1/referrals/referral/" } + + var queryParameters: [String: String]? + + var additionalHeaders: [String: String]? + + var body: Data? +} diff --git a/Ecosia/Core/Referrals/ReferralRefreshCodeRequest.swift b/Ecosia/Core/Referrals/ReferralRefreshCodeRequest.swift new file mode 100644 index 000000000000..1bddc0751b89 --- /dev/null +++ b/Ecosia/Core/Referrals/ReferralRefreshCodeRequest.swift @@ -0,0 +1,22 @@ +// 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 Foundation + +struct ReferralRefreshCodeRequest: BaseRequest { + var method: HTTPMethod { .get } + + var path: String { "/v1/referrals/referral/\(code)" } + + var queryParameters: [String: String]? + + var additionalHeaders: [String: String]? + + var body: Data? + + let code: String + init(code: String) { + self.code = code + } +} diff --git a/Ecosia/Core/Referrals/Referrals.Model.swift b/Ecosia/Core/Referrals/Referrals.Model.swift new file mode 100644 index 000000000000..2a53eeead6bd --- /dev/null +++ b/Ecosia/Core/Referrals/Referrals.Model.swift @@ -0,0 +1,62 @@ +// 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 Foundation + +extension Referrals { + public enum Error: Int, Swift.Error { + case noConnection = -1 + case badRequest = 400 + case notFound = 404 + case alreadyUsed = 409 + case invalidCode = 422 + case genericError = 500 + } + + public struct Model: Codable, Equatable { + public static let treesPerReferred = 1 + public var code: String? + public var claims = 0 + public var isClaimed = false + public var isNewClaim = false + public var pendingClaim: String? + var updated: Date = .distantPast + var knownClaims = 0 + + public var newClaims: Int { + return claims - knownClaims + } + + public var count: Int { + return claims + (isClaimed ? 1 : 0) + } + + public mutating func accept() { + knownClaims = claims + isNewClaim = false + } + } + + struct CodeInfo: Codable { + let code: String + let claims: Int + + public init(from decoder: Decoder) throws { + let root = try decoder.container(keyedBy: CodingKeys.self) + code = try root.decode(String.self, forKey: .code) + claims = (try? root.decode(Int.self, forKey: .claims)) ?? 0 + } + + public init(code: String, claims: Int) { + self.code = code + self.claims = claims + } + + private enum CodingKeys: String, CodingKey { + case + code, + claims = "claims_count" + } + } +} diff --git a/Ecosia/Core/Referrals/Referrals.swift b/Ecosia/Core/Referrals/Referrals.swift new file mode 100644 index 000000000000..2f0a2b53bdd2 --- /dev/null +++ b/Ecosia/Core/Referrals/Referrals.swift @@ -0,0 +1,182 @@ +// 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 Foundation + +/// The `Referrals` class is responsible for handling referral-related operations, +/// including refreshing referral codes, creating new codes, and claiming referrals. +public class Referrals: Publisher { + + /// Subscriptions to the referral model updates. + public var subscriptions = [Subscription]() + + /// Deeplink to enter the Referral's claim + public static let deepLinkPath = "ecosia://invite/" + + /// Link shown in the Modal screen as well as the invite message + public static let sharingLinkRoot = "https://ecosia.co/app?referrer=" + + /// The HTTP client used for performing network requests. + let client: HTTPClient + + /// Initializes a new instance of `Referrals` with the specified HTTP client. + /// - Parameter client: The HTTP client to use. Defaults to `URLSessionHTTPClient`. + public init(client: HTTPClient = URLSessionHTTPClient()) { + self.client = client + } + + /// Indicates whether the referral information needs to be updated. + var needsUpdate: Bool { + return Calendar.current.dateComponents([.hour], from: User.shared.referrals.updated, to: .init()).hour! >= 24 + } + + /// Indicates whether the referral information is currently being refreshed. + var isRefreshing = false + + /// Refreshes the referral information. + /// - Parameters: + /// - force: A Boolean value indicating whether to force a refresh. Defaults to `false`. + /// - createCode: A Boolean value indicating whether to create a new code if one does not exist. Defaults to `false`. + /// - Throws: An error if the refresh operation fails. + public func refresh(force: Bool = false, createCode: Bool = false) async throws { + // Check if code is present + guard let code = User.shared.referrals.code else { + // only fetch new code if desired + guard createCode else { + return + } + + let info = try await self.fetchCode() + await self.update(info) + return + } + + // only refresh if forced or needed + guard force || needsUpdate else { + return + } + + guard !isRefreshing else { + return + } + isRefreshing = true + defer { + self.isRefreshing = false + } + + // Refresh count for given code + do { + let info = try await self.refreshCode(code) + await self.update(info) + } catch { + await self.updateErrorDate() + throw error + } + } + + /// Creates a new referral code. + /// - Returns: The created referral code information. + /// - Throws: An error if the creation operation fails. + func createCode() async throws -> CodeInfo { + let request = ReferralCreateCodeRequest() + let (data, response) = try await client.perform(request) + guard response != nil else { + throw Referrals.Error.noConnection + } + return try JSONDecoder().decode(CodeInfo.self, from: data) + } + + /// Fetches the referral code information. + /// - Returns: The fetched referral code information. + /// - Throws: An error if the fetch operation fails. + func fetchCode() async throws -> CodeInfo { + // pretend success if we have a code already + if let code = User.shared.referrals.code { + return .init(code: code, claims: User.shared.referrals.claims) + } + return try await createCode() + } + + /// Refreshes the referral code information. + /// - Parameter code: The referral code to refresh. + /// - Returns: The refreshed referral code information. + /// - Throws: An error if the refresh operation fails. + func refreshCode(_ code: String) async throws -> CodeInfo { + let request = ReferralRefreshCodeRequest(code: code) + let (data, response) = try await client.perform(request) + guard let response = response else { + throw Referrals.Error.noConnection + } + switch response.statusCode { + case Referrals.Error.notFound.rawValue: + return try await createCode() + case 200: + return try JSONDecoder().decode(CodeInfo.self, from: data) + default: + let error: Referrals.Error? = .init(rawValue: response.statusCode) + throw error ?? .genericError + } + } + + /// Claims a referral using the specified referrer. + /// - Parameter referrer: The referrer identifier. + /// - Throws: An error if the claim operation fails. + public func claim(referrer: String) async throws { + var code: String + if let storedCode = User.shared.referrals.code { + code = storedCode + } else { + let info = try await self.fetchCode() + await self.update(info) + code = info.code + } + try await self.claim(referrer: referrer, claim: code) + await self.storeClaim() + } + + /// Claims a referral using the specified referrer and claim code. + /// - Parameters: + /// - referrer: The referrer identifier. + /// - claim: The claim code. + /// - Throws: An error if the claim operation fails. + private func claim(referrer: String, claim: String) async throws { + let request = ReferralClaimRequest(referrer: referrer, claim: claim) + let (_, response) = try await client.perform(request) + guard let response = response else { + throw Referrals.Error.noConnection + } + guard response.statusCode == 201 else { + let error: Referrals.Error? = .init(rawValue: response.statusCode) + throw error ?? .genericError + } + } + + /// Updates the date on error to refresh the cooldown period. + @MainActor + private func updateErrorDate() { + User.shared.referrals.updated = Date() + } + + /// Updates the referral information. + /// - Parameter info: The referral code information to update. + @MainActor + private func update(_ info: CodeInfo) { + var referrals = User.shared.referrals + referrals.code = info.code + referrals.claims = info.claims + referrals.updated = Date() + User.shared.referrals = referrals + send(referrals) + } + + /// Stores the claim information and updates the referral state. + @MainActor + private func storeClaim() { + var referrals = User.shared.referrals + referrals.isClaimed = true + referrals.isNewClaim = true + User.shared.referrals = referrals + send(referrals) + } +} diff --git a/Ecosia/Core/RegionLocatable.swift b/Ecosia/Core/RegionLocatable.swift new file mode 100644 index 000000000000..2075520d954e --- /dev/null +++ b/Ecosia/Core/RegionLocatable.swift @@ -0,0 +1,9 @@ +// 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/ + +/// Utilized mainly for the Unleash refresh logic and accommodate testability +/// see: `DeviceRegionChangeProvider.swift` +public protocol RegionLocatable { + var regionIdentifierLowercasedWithFallbackValue: String { get } +} diff --git a/Ecosia/Core/Scheme.swift b/Ecosia/Core/Scheme.swift new file mode 100644 index 000000000000..0a2f0764a71e --- /dev/null +++ b/Ecosia/Core/Scheme.swift @@ -0,0 +1,37 @@ +// 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 Foundation + +public enum Scheme: String { + case + http, + https, + gmsg, + other + + public enum Policy { + case + allow, + cancel + } + + var policy: Policy { + switch self { + case .gmsg: + return .cancel + default: + return .allow + } + } + + var isBrowser: Bool { + switch self { + case .http, .https: + return true + default: + return false + } + } +} diff --git a/Ecosia/Core/SearchesCounter.swift b/Ecosia/Core/SearchesCounter.swift new file mode 100644 index 000000000000..7be72c248078 --- /dev/null +++ b/Ecosia/Core/SearchesCounter.swift @@ -0,0 +1,20 @@ +// 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 Foundation + +public final class SearchesCounter: StatePublisher { + public var subscriptions = [Subscription]() + public var state: Int? { + return User.shared.searchCount + } + + public init() { + NotificationCenter.default.addObserver(self, selector: #selector(searchesCounterChanged), name: .searchesCounterChanged, object: nil) + } + + @objc private func searchesCounterChanged() { + send(state!) + } +} diff --git a/Ecosia/Core/Statistics/FinancialReports.swift b/Ecosia/Core/Statistics/FinancialReports.swift new file mode 100644 index 000000000000..0978700f1328 --- /dev/null +++ b/Ecosia/Core/Statistics/FinancialReports.swift @@ -0,0 +1,46 @@ +// 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 Foundation + +public final class FinancialReports { + public struct Report: Decodable, Equatable { + public internal(set) var totalIncome: Double + public internal(set) var numberOfTreesFinanced: Double + } + + public static let shared = FinancialReports() + public internal(set) var latestMonth: Date = .init(timeIntervalSince1970: 1685577600) + public internal(set) var latestReport: Report = .init(totalIncome: 3206010, + numberOfTreesFinanced: 961642) + + public var localizedMonthAndYear: String { + let formatter = DateFormatter() + formatter.dateFormat = "MMMM yyyy" + formatter.locale = Locale.current + formatter.timeZone = TimeZone(abbreviation: "GMT") + + let localizedMonth = formatter.string(from: latestMonth) + return localizedMonth + } + + init() { } + + public func fetchAndUpdate(urlSession: URLSessionProtocol = URLSession.shared) async throws { + let (data, _) = try await urlSession.data(from: Environment.current.urlProvider.financialReportsData) + + let response = try JSONSerialization.jsonObject(with: data) as? [String: Any] + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-M" + dateFormatter.timeZone = .init(abbreviation: "UTC") + let months = response?.keys.compactMap { dateFormatter.date(from: $0) } ?? [] + let latestMonth = months.reduce(Date.distantPast) { $0 > $1 ? $0 : $1 } + let latestKey = dateFormatter.string(from: latestMonth) + let latestObject = response?[latestKey] as? [String: Any] ?? [:] + + self.latestMonth = latestMonth + self.latestReport = try JSONDecoder().decode(Report.self, from: JSONSerialization.data(withJSONObject: latestObject)) + } +} diff --git a/Ecosia/Core/Statistics/InvestmentsProjection.swift b/Ecosia/Core/Statistics/InvestmentsProjection.swift new file mode 100644 index 000000000000..d6028b632bd6 --- /dev/null +++ b/Ecosia/Core/Statistics/InvestmentsProjection.swift @@ -0,0 +1,27 @@ +// 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 Foundation + +public final class InvestmentsProjection: Publisher { + public static let shared = InvestmentsProjection() + public var subscriptions = [Subscription]() + let timer = DispatchSource.makeTimerSource(queue: .main) + + init() { + timer.activate() + timer.setEventHandler { [weak self] in + guard let count = self?.totalInvestedAt(Date()) else { return } + self?.send(count) + } + let secondsToOneEuro = max(1/Statistics.shared.investmentPerSecond, 1) + timer.schedule(deadline: .now(), repeating: secondsToOneEuro) + } + + public func totalInvestedAt(_ date: Date) -> Int { + let statistics = Statistics.shared + let deltaTimeInSeconds = date.timeIntervalSince(statistics.totalInvestmentsLastUpdated) + return .init(deltaTimeInSeconds * statistics.investmentPerSecond + statistics.totalInvestments) + } +} diff --git a/Ecosia/Core/Statistics/Statistics.swift b/Ecosia/Core/Statistics/Statistics.swift new file mode 100644 index 000000000000..3a34fce8d3f2 --- /dev/null +++ b/Ecosia/Core/Statistics/Statistics.swift @@ -0,0 +1,79 @@ +// 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 Foundation + +public final class Statistics { + public struct Response: Decodable { + var results: [Result] + } + public struct Result: Decodable { + var name: String + var value: String + var lastUpdated: String? + + enum StatisticName: String, Decodable { + case treesPlanted = "Total Trees Planted" + case timePerTree = "Time per tree (seconds)" + case searchesPerTree = "Searches per tree" + case activeUsers = "Active Users" + case eurToUsdMultiplier = "EUR=>USD" + case investmentPerSecond = "Investments amount per second" + case totalInvestments = "Total investments amount" + } + + func statisticName() -> StatisticName? { StatisticName(rawValue: name) } + + func doubleValue() -> Double? { Double(value) } + + func lastUpdatedDate() -> Date? { + guard let dateString = lastUpdated else { return nil } + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.date(from: dateString) + } + } + + public static let shared = Statistics() + public internal(set) var treesPlanted = Double(113016418) + public internal(set) var treesPlantedLastUpdated = Date(timeIntervalSince1970: 1604671200) + public internal(set) var timePerTree = Double(1.3) + public internal(set) var searchesPerTree = Double(50) + public internal(set) var activeUsers = Double(20000000) + public internal(set) var eurToUsdMultiplier = Double(1.08) + public internal(set) var investmentPerSecond = Double(0.35) + public internal(set) var totalInvestments = Double(76776000) + public internal(set) var totalInvestmentsLastUpdated = Date(timeIntervalSince1970: 1685404800) + + init() { } + + public func fetchAndUpdate(urlSession: URLSessionProtocol = URLSession.shared) async throws { + let (data, _) = try await urlSession.data(from: Environment.current.urlProvider.statistics) + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let response = try decoder.decode(Response.self, from: data) + response.results.forEach { statistic in + switch statistic.statisticName() { + case .treesPlanted: + if let value = statistic.doubleValue(), + let date = statistic.lastUpdatedDate() { + treesPlanted = value + treesPlantedLastUpdated = date + } + case .timePerTree: timePerTree = statistic.doubleValue() ?? timePerTree + case .searchesPerTree: searchesPerTree = statistic.doubleValue() ?? searchesPerTree + case .activeUsers: activeUsers = statistic.doubleValue() ?? activeUsers + case .eurToUsdMultiplier: eurToUsdMultiplier = statistic.doubleValue() ?? eurToUsdMultiplier + case .investmentPerSecond: investmentPerSecond = statistic.doubleValue() ?? investmentPerSecond + case .totalInvestments: + if let value = statistic.doubleValue(), + let date = statistic.lastUpdatedDate() { + totalInvestments = value + totalInvestmentsLastUpdated = date + } + case nil: break + } + } + } +} diff --git a/Ecosia/Core/Statistics/TreesProjection.swift b/Ecosia/Core/Statistics/TreesProjection.swift new file mode 100644 index 000000000000..378a6dcb5919 --- /dev/null +++ b/Ecosia/Core/Statistics/TreesProjection.swift @@ -0,0 +1,26 @@ +// 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 Foundation + +public final class TreesProjection: Publisher { + public static let shared = TreesProjection() + public var subscriptions = [Subscription]() + let timer = DispatchSource.makeTimerSource(queue: .main) + + init() { + timer.activate() + timer.setEventHandler { [weak self] in + guard let count = self?.treesAt(Date()) else { return } + self?.send(count) + } + timer.schedule(deadline: .now(), repeating: Statistics.shared.timePerTree) + } + + public func treesAt(_ date: Date) -> Int { + let statistics = Statistics.shared + let timeSinceLastUpdate = date.timeIntervalSince(statistics.treesPlantedLastUpdated) + return .init(timeSinceLastUpdate / statistics.timePerTree + statistics.treesPlanted - 1) + } +} diff --git a/Ecosia/Core/Tabs/Tab.swift b/Ecosia/Core/Tabs/Tab.swift new file mode 100644 index 000000000000..d755c7520f21 --- /dev/null +++ b/Ecosia/Core/Tabs/Tab.swift @@ -0,0 +1,21 @@ +// 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 Foundation + +public struct Tab: Codable, Identifiable { + public var page: Page? + public let id: UUID + + public init(page: Page?) { + self.page = page + id = .init() + } +} + +public extension Tab { + var snapshot: Data? { + return try? Data(contentsOf: FileManager.snapshots.appendingPathComponent(id.uuidString)) + } +} diff --git a/Ecosia/Core/Tabs/Tabs.swift b/Ecosia/Core/Tabs/Tabs.swift new file mode 100644 index 000000000000..e92ef7bfa418 --- /dev/null +++ b/Ecosia/Core/Tabs/Tabs.swift @@ -0,0 +1,100 @@ +// 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 Foundation + +public final class Tabs { + public var current: Int? { + didSet { + PageStore.save(currentTab: current) + } + } + + public private(set) var items = [Tab]() { + didSet { + PageStore.save(tabs: items) + } + } + + let queue = DispatchQueue(label: "", qos: .utility) + + public init() { + items = PageStore.tabs + if let current = PageStore.currentTab { + self.current = current < items.count ? current : nil + } + } + + public func new(_ url: URL?) { + var items = self.items + items.removeAll { $0.page == nil } + let new = Tab(page: url.map { .init(url: $0, title: "") }) + current = items.count + items.append(new) + self.items = items + } + + public func close(_ id: UUID) { + guard let index = items.firstIndex(where: { $0.id == id }) else { return } + if current != nil { + if current == index { + current = nil + } else if index < current! { + current = current! - 1 + } + } + deleteSnapshot(items[index].id) + items.remove(at: index) + } + + public func clear() { + items = [] + current = nil + queue.async { + if FileManager.default.fileExists(atPath: FileManager.snapshots.path) { + try? FileManager.default.removeItem(at: FileManager.snapshots) + } + } + } + + public func update(_ tab: UUID, page: Page) { + items.firstIndex { $0.id == tab }.map { + items[$0].page = page + } + } + + public func page(_ tab: UUID) -> Page? { + items.first { $0.id == tab }?.page + } + + public func image(_ id: UUID, completion: @escaping (Data?) -> Void) { + queue.async { + let data = try? Data(contentsOf: FileManager.snapshots.appendingPathComponent(id.uuidString)) + DispatchQueue.main.async { + completion(data) + } + } + } + + public func save(_ image: Data, with: UUID) { + queue.async { + if !FileManager.default.fileExists(atPath: FileManager.snapshots.path) { + var url = FileManager.snapshots + var resources = URLResourceValues() + resources.isExcludedFromBackup = true + try? url.setResourceValues(resources) + try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + } + try? image.write(to: FileManager.snapshots.appendingPathComponent(with.uuidString), options: .atomic) + } + } + + func deleteSnapshot(_ id: UUID) { + queue.async { + if FileManager.default.fileExists(atPath: FileManager.snapshots.appendingPathComponent(id.uuidString).path) { + try? FileManager.default.removeItem(at: FileManager.snapshots.appendingPathComponent(id.uuidString)) + } + } + } +} diff --git a/Ecosia/Core/TimeInterval+Extensions.swift b/Ecosia/Core/TimeInterval+Extensions.swift new file mode 100644 index 000000000000..091cbf1676e4 --- /dev/null +++ b/Ecosia/Core/TimeInterval+Extensions.swift @@ -0,0 +1,10 @@ +// 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 Foundation + +extension TimeInterval { + + static let twentyFourHoursTimeInterval: TimeInterval = 24*60*60 +} diff --git a/Ecosia/Core/TimestampProvider.swift b/Ecosia/Core/TimestampProvider.swift new file mode 100644 index 000000000000..8c4ddc5f5bfa --- /dev/null +++ b/Ecosia/Core/TimestampProvider.swift @@ -0,0 +1,9 @@ +// 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 Foundation + +protocol TimestampProvider { + var currentTimestamp: TimeInterval { get } +} diff --git a/Ecosia/Core/URL+Extensions.swift b/Ecosia/Core/URL+Extensions.swift new file mode 100644 index 000000000000..43a591963ca7 --- /dev/null +++ b/Ecosia/Core/URL+Extensions.swift @@ -0,0 +1,84 @@ +// 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 Foundation + +extension URL { + + public enum Key: String { + case + query = "q", + typeTag = "tt", + userId = "_sp" + } + + public static func ecosiaSearchWithQuery(_ query: String, urlProvider: URLProvider = Environment.current.urlProvider) -> URL { + var components = URLComponents(url: urlProvider.root, resolvingAgainstBaseURL: false)! + components.path = "/search" + components.queryItems = [item(key: .query, value: query), item(key: .typeTag, value: "iosapp")] + return components.url! + } + + /// Check whether the URL being browsed will present the SERP out of a search or a search suggestion + public func isEcosiaSearchQuery(_ urlProvider: URLProvider = Environment.current.urlProvider) -> Bool { + guard isEcosia(urlProvider), + let components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { + return false + } + return components.path == "/search" + } + + /// Check whether the URL should be Ecosified. At the moment this is true for every Ecosia URL. + public func shouldEcosify(_ urlProvider: URLProvider = Environment.current.urlProvider) -> Bool { + return isEcosia(urlProvider) + } + + public func ecosified(isIncognitoEnabled: Bool, urlProvider: URLProvider = Environment.current.urlProvider) -> URL { + guard isEcosia(urlProvider), + var components = components + else { return self } + components.queryItems?.removeAll(where: { $0.name == Key.userId.rawValue }) + var items = components.queryItems ?? .init() + /* + The `sendAnonymousUsageData` is set by the native UX component in settings + that determines whether the app would send the events to Snowplow. + To align the business logic, this parameter will also function as a condition + that decides whether we would send our AnalyticsID as query paramter for + searches. In this scenario thuogh, the naming is a bit misleanding, thus + checking for the negative evaluation of it. + */ + let shouldAnonymizeUserId = isIncognitoEnabled || + !User.shared.hasAnalyticsCookieConsent || + !User.shared.sendAnonymousUsageData + let userId = shouldAnonymizeUserId ? UUID(uuid: UUID_NULL).uuidString : User.shared.analyticsId.uuidString + items.append(Self.item(key: .userId, value: userId)) + components.queryItems = items + return components.url! + } + + public var policy: Scheme.Policy { + (scheme + .flatMap(Scheme.init(rawValue:)) ?? .other) + .policy + } + + private subscript(_ key: Key) -> String? { + components?.queryItems?.first { $0.name == key.rawValue }?.value + } + + private func isEcosia(_ urlProvider: URLProvider = Environment.current.urlProvider) -> Bool { + guard let domain = urlProvider.domain else { return false } + let isBrowser = scheme.flatMap(Scheme.init(rawValue:))?.isBrowser == true + let hasURLProviderDomainSuffix = host?.hasSuffix(domain) == true + return isBrowser && hasURLProviderDomainSuffix + } + + private var components: URLComponents? { + URLComponents(url: self, resolvingAgainstBaseURL: false) + } + + private static func item(key: Key, value: String) -> URLQueryItem { + .init(name: key.rawValue, value: value) + } +} diff --git a/Ecosia/Core/URLRequest+Extensions.swift b/Ecosia/Core/URLRequest+Extensions.swift new file mode 100644 index 000000000000..82a49f768132 --- /dev/null +++ b/Ecosia/Core/URLRequest+Extensions.swift @@ -0,0 +1,22 @@ +// 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 Foundation + +extension URLRequest { + + public mutating func withAuthParameters(environment: Environment = Environment.current) -> URLRequest { + if let auth = environment.auth { + setValue(auth.id, forHTTPHeaderField: CloudflareKeyProvider.clientId) + setValue(auth.secret, forHTTPHeaderField: CloudflareKeyProvider.clientSecret) + } + return self + } + + /// This function provides an additional HTTP request header when loading SERP through native UI (i.e. submitting a search) + /// to help SERP decide which market to serve. + public mutating func addLanguageRegionHeader() { + setValue(Locale.current.identifierWithDashedLanguageAndRegion, forHTTPHeaderField: "x-ecosia-app-language-region") + } +} diff --git a/Ecosia/Core/User.swift b/Ecosia/Core/User.swift new file mode 100644 index 000000000000..5ce1a7c028ad --- /dev/null +++ b/Ecosia/Core/User.swift @@ -0,0 +1,273 @@ +// 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 Foundation +import UserNotifications +import Combine + +extension Notification.Name { + static let searchesCounterChanged = Notification.Name("searchesCounterChanged") + public static let searchSettingsChanged = Notification.Name("searchSettingsChanged") +} + +public struct User: Codable, Equatable { + public static var shared = User() { + didSet { + guard shared != oldValue else { return } + shared.save() + + if shared.hasNewSearchSetting(compared: oldValue) { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .searchSettingsChanged, object: nil) + } + } + } + } + + // MARK: Search Settings + public var marketCode = Local.make(for: .current) + public var adultFilter = AdultFilter.moderate + public var autoComplete = true + public var personalized = false + + // MARK: Privacy settings + public var sendAnonymousUsageData = true + public internal(set) var cookieConsentValue: String? + public var hasAnalyticsCookieConsent: Bool { + guard let cookieConsentValue else { + return false + } + return cookieConsentValue.contains("a") + } + + // MARK: NTP Customization + public var showTopSites = true + public var topSitesRows = 4 + public var showClimateImpact = true + public var showEcosiaNews = true + public var showAboutEcosia = true + + // MARK: Install + public var install = Date() + public var analyticsId = UUID() + public var versionOnInstall = "0.0.0" + public var firstTime = true + + // MARK: Other + public var news = Date.distantPast + public var migrated = false + public var referrals = Referrals.Model() + public internal(set) var id: String? + public var whatsNewItemsVersionsShown = Set() + public internal(set) var analyticsUserState = AnalyticsStateContext() + + public var searchCount = 0 { + didSet { + guard oldValue != searchCount else { return } + DispatchQueue.main.async { + NotificationCenter.default.post(name: .searchesCounterChanged, object: nil) + } + } + } + + var state = [String: String]() + + private enum CodingKeys: String, CodingKey { + case + install, + versionOnInstall, + news, + analyticsId, + marketCode, + adultFilter, + autoComplete, + firstTime, + personalized, + sendAnonymousUsageData, + topSitesRows, + showClimateImpact, + showEcosiaNews, + showAboutEcosia, + migrated, + referrals, + id, + state, + cookieConsentValue, + whatsNewItemsVersionsShown, + analyticsUserState + // Reusing previous decoding keys + case searchCount = "treeCount" + case showTopSites = "topSites" + } + + public init(from decoder: Decoder) throws { + let root = try decoder.container(keyedBy: CodingKeys.self) + install = (try? root.decode(Date.self, forKey: .install)) ?? .init() + versionOnInstall = (try? root.decode(String.self, forKey: .versionOnInstall)) ?? "0.0.0" + news = (try? root.decode(Date.self, forKey: .news)) ?? .distantPast + analyticsId = (try? root.decode(UUID.self, forKey: .analyticsId)) ?? .init() + marketCode = (try? root.decode(Local.self, forKey: .marketCode)) ?? Local.make(for: .current) + adultFilter = (try? root.decode(AdultFilter.self, forKey: .adultFilter)) ?? .moderate + autoComplete = (try? root.decode(Bool.self, forKey: .autoComplete)) ?? true + firstTime = (try? root.decode(Bool.self, forKey: .firstTime)) ?? true + personalized = (try? root.decode(Bool.self, forKey: .personalized)) ?? false + sendAnonymousUsageData = (try? root.decode(Bool.self, forKey: .sendAnonymousUsageData)) ?? true + topSitesRows = (try? root.decode(Int.self, forKey: .topSitesRows)) ?? 4 + showTopSites = (try? root.decode(Bool.self, forKey: .showTopSites)) ?? true + showClimateImpact = (try? root.decode(Bool.self, forKey: .showClimateImpact)) ?? true + showEcosiaNews = (try? root.decode(Bool.self, forKey: .showEcosiaNews)) ?? true + showAboutEcosia = (try? root.decode(Bool.self, forKey: .showAboutEcosia)) ?? true + migrated = (try? root.decode(Bool.self, forKey: .migrated)) ?? false + referrals = (try? root.decode(Referrals.Model.self, forKey: .referrals)) ?? .init() + id = try? root.decode(String.self, forKey: .id) + searchCount = (try? root.decode(Int.self, forKey: .searchCount)) ?? 0 + state = (try? root.decode([String: String].self, forKey: .state)) ?? [:] + cookieConsentValue = try? root.decode(String.self, forKey: .cookieConsentValue) + whatsNewItemsVersionsShown = (try? root.decode(Set.self, forKey: .whatsNewItemsVersionsShown)) ?? [] + analyticsUserState = (try? root.decode(AnalyticsStateContext.self, forKey: .analyticsUserState)) ?? .init() + } + + init() { + if let stored = self.stored { + id = stored.id + adultFilter = stored.adultFilter + marketCode = stored.marketCode + searchCount = stored.searchCount + autoComplete = stored.autoComplete + firstTime = stored.firstTime + analyticsId = stored.analyticsId + personalized = stored.personalized + sendAnonymousUsageData = stored.sendAnonymousUsageData + migrated = stored.migrated + state = stored.state + news = stored.news + topSitesRows = stored.topSitesRows + showTopSites = stored.showTopSites + showClimateImpact = stored.showClimateImpact + showEcosiaNews = stored.showEcosiaNews + showAboutEcosia = stored.showAboutEcosia + referrals = stored.referrals + install = stored.install + versionOnInstall = stored.versionOnInstall + cookieConsentValue = stored.cookieConsentValue + whatsNewItemsVersionsShown = stored.whatsNewItemsVersionsShown + analyticsUserState = stored.analyticsUserState + } else { + save() + } + } + + private var stored: User? { + try? JSONDecoder().decode(User.self, from: .init(contentsOf: FileManager.user)) + } + + static let queue = DispatchQueue(label: "", qos: .utility) + private func save() { + let user = self + User.queue.async { + try? JSONEncoder().encode(user).write(to: FileManager.user, options: .atomic) + } + } +} + +// MARK: Helper methods +extension User { + + public var showsReferralSpotlight: Bool { + guard install < Calendar.current.date(byAdding: .day, value: -3, to: .init())! else { return false } + return state[Key.referralSpotlight.rawValue].map(Bool.init) != false + } + + public var showsInactiveTabsTooltip: Bool { + state[Key.inactiveTabsTooltip.rawValue].map(Bool.init) != false + } + + public var showsBookmarksImportExportTooltip: Bool { + state[Key.bookmarksImportExportTooltipShown.rawValue].map(Bool.init) != false + } + + public var shouldShowImpactIntro: Bool { + state[Key.impactIntro.rawValue].map(Bool.init) != false + } + + public mutating func hideImpactIntro() { + state[Key.impactIntro.rawValue] = "\(false)" + } + + public mutating func showImpactIntro() { + state[Key.impactIntro.rawValue] = "\(true)" + } + + public mutating func hideReferralSpotlight() { + state[Key.referralSpotlight.rawValue] = "\(false)" + } + + public mutating func showInactiveTabsTooltip() { + state[Key.inactiveTabsTooltip.rawValue] = "\(true)" + } + + public mutating func hideInactiveTabsTooltip() { + state[Key.inactiveTabsTooltip.rawValue] = "\(false)" + } + + public mutating func hideBookmarksImportExportTooltip() { + state[Key.bookmarksImportExportTooltipShown.rawValue] = "\(false)" + } + + enum Key: String { + case + referralSpotlight, + impactIntro = "counterIntro", // Reusing previous key + inactiveTabsTooltip, + bookmarksImportExportTooltipShown, + isNewUserSinceBookmarksImportExportHasBeenShipped + } +} + +// MARK: Search Setting Helper +extension User { + private struct SearchSetting: Equatable { + let marketCode: Local + let adultFilter: AdultFilter + let autoComplete: Bool + let personalized: Bool + } + + private var searchSetting: SearchSetting { + .init(marketCode: marketCode, adultFilter: adultFilter, autoComplete: autoComplete, personalized: personalized) + } + + func hasNewSearchSetting(compared to: User) -> Bool { + searchSetting != to.searchSetting + } +} + +// MARK: User state context +extension User { + + /// Mimics the high level push notification states + public enum PushNotificationState: String, Codable { + case enabled + case disabled + case notDetermined = "not_determined" + } + + /// The values to pass into the Analytics User State Dedicated context + public struct AnalyticsStateContext: Codable, Equatable { + var pushNotificationState: PushNotificationState = .notDetermined + + enum CodingKeys: String, CodingKey { + case pushNotificationState = "push_notification_state" + } + } + + /// Updates the Analytics' User State given the current App's User Notification's status + public mutating func updatePushNotificationUserStateWithAnalytics(from status: UNAuthorizationStatus) { + analyticsUserState.pushNotificationState = switch status { + case .authorized, .ephemeral, .provisional: .enabled + case .denied: .disabled + default: .notDetermined + } + } +} diff --git a/Ecosia/Core/UserDefaults+ObjectPersister.swift b/Ecosia/Core/UserDefaults+ObjectPersister.swift new file mode 100644 index 000000000000..6db3b98f47e2 --- /dev/null +++ b/Ecosia/Core/UserDefaults+ObjectPersister.swift @@ -0,0 +1,7 @@ +// 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 Foundation + +extension UserDefaults: ObjectPersister {} diff --git a/Ecosia/Core/adr/0_2021-04-27_Architectural-decision-record-template.md b/Ecosia/Core/adr/0_2021-04-27_Architectural-decision-record-template.md new file mode 100644 index 000000000000..1f4864f1fba6 --- /dev/null +++ b/Ecosia/Core/adr/0_2021-04-27_Architectural-decision-record-template.md @@ -0,0 +1,12 @@ +# Architectural Decison Record Template + +- In the context of: `functional requirement` (story, use case or arch. component) +- facing: `non-functional requirement` (for instance a desired quality) +- we decided: `decision outcome` (arguably the most important part) +- and neglected `alternatives not chosen` (not to be forgotten!), +- to achieve: `benefits` (the full or partial satisfaction of requirement(s)) +- accepting that: `drawbacks and other consequences` ( for instance impact on other properties/context and effort/cost (both short term and long term)). + +## Related Code + +- [Package.swift](../Package.swift) diff --git a/Ecosia/Core/adr/1_2021-04-27_File-based-user-data-persistence.md b/Ecosia/Core/adr/1_2021-04-27_File-based-user-data-persistence.md new file mode 100644 index 000000000000..b5db001de414 --- /dev/null +++ b/Ecosia/Core/adr/1_2021-04-27_File-based-user-data-persistence.md @@ -0,0 +1,12 @@ +# File based user data persistence + +- In the context of: `having persistence for user data and settings` +- facing: `loading user data via UserDefaults delayed app launch, lacked the ability to scale and were flaky in unit tests` +- we decided: `to store user data in JSON-encoded files` +- and neglected: `the use of UserDefaults` +- to achieve: `ease of use, extensibility, speed of access, consistency and predictable unit test results` +- accepting that: `we need to implement persistence (storing, loading, caching) of the json-files ourselves` + +## Related Code + +- [User.swift](../Sources/User.swift) diff --git a/Ecosia/Core/adr/2_2023-06-29_Environment_as_getter.md b/Ecosia/Core/adr/2_2023-06-29_Environment_as_getter.md new file mode 100644 index 000000000000..ae354f855451 --- /dev/null +++ b/Ecosia/Core/adr/2_2023-06-29_Environment_as_getter.md @@ -0,0 +1,13 @@ +# Environment as getter + +- In the context of: `making the Environment getter only from the Browser app` ([Jira ticket](https://ecosia.atlassian.net/browse/MOB-1817)) +- facing: `the possibility of update the Environment at any point in the app` +- we decided: `to add a PRODUCTION macro on Release only` +- and neglected `any other workaround and/or code change that would break the SOLID principles`, +- to achieve: `The smallest yet robust code changes between the Core module and the Browser app. As we know, SPM currently provides us with only .debug and .release build configurations. There are many proposals to make it "flavored" but, so far, nothing hasn't even reached beta. The way SPM chooses which configuration to pick, ironically, is purely empirical. It basically picks .debug only in case the hosting build configuration contains Development or Debug (case-insensitive). This PRODUCTION macro is being read within the Core package, and decide which Environment assign to it. Depending on the Environment a set of protocol-based URLs mimicking the older architecture is loaded in memory.` +- accepting that: `unconventionally recognised Build Configuration prefixes have been added to the TestFlight and AppCenter configs in the browser app (e.g.: Development_)` + +## Related Code + +- [Package.swift](../Package.swift) +- [Environment.swift](../Sources/Environment/Environment.swift) diff --git a/Ecosia/Core/adr/3_2024-11-01_Analytics-Events-Tests b/Ecosia/Core/adr/3_2024-11-01_Analytics-Events-Tests new file mode 100644 index 000000000000..51489c833d9b --- /dev/null +++ b/Ecosia/Core/adr/3_2024-11-01_Analytics-Events-Tests @@ -0,0 +1,15 @@ +# Analytics Events Testing + +- In the context of: `testing wether Analytics events are properly tracked troughout the app` ([Jira ticket](https://ecosia.atlassian.net/browse/MOB-2979)) +- facing: `previous issues caused by regression` +- we decided: `to unit test wherever Analytics is called, replacing our shared singleton with a "spy".` +- and neglected `following a protocol-oriented approach, which would require injecting dependencies on every class.`, +- to achieve: `decoupled tests that are easy to maintain and require no change in the classes that use Analytics.` +- accepting that: `making our shared Analytics singleton a variable brings risks and is usually a bad programming practice. To mitigate that, we also set up a SwiftLint rule that flags updating the shared instance outside tests as an error.` + +## Related Code + +- [Pull Request with Initial setup](https://github.com/ecosia/ios-browser/pull/799) +- [AnalyticsSpyTests](https://github.com/ecosia/ios-browser/blob/3bcc8af58bf24e2eb718d27f4b5fd6955db09ad8/EcosiaTests/Analytics/AnalyticsSpyTests.swift) +- [Variable Analytics shared singletion](https://github.com/ecosia/ios-browser/blob/3bcc8af58bf24e2eb718d27f4b5fd6955db09ad8/Client/Ecosia/Analytics/Analytics.swift#L22) +- [SwiftLint custom rule](https://github.com/ecosia/ios-browser/blob/3bcc8af58bf24e2eb718d27f4b5fd6955db09ad8/.swiftlint.yml#L100-L116) diff --git a/Ecosia/Ecosia.h b/Ecosia/Ecosia.h new file mode 100644 index 000000000000..84cd6108c21b --- /dev/null +++ b/Ecosia/Ecosia.h @@ -0,0 +1,15 @@ +// 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 + +//! Project version number for Ecosia. +FOUNDATION_EXPORT double EcosiaVersionNumber; + +//! Project version string for Ecosia. +FOUNDATION_EXPORT const unsigned char EcosiaVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Client/Ecosia/Entitlements/AppExtensions/Ecosia.entitlements b/Ecosia/Entitlements/AppExtensions/Ecosia.entitlements similarity index 100% rename from Client/Ecosia/Entitlements/AppExtensions/Ecosia.entitlements rename to Ecosia/Entitlements/AppExtensions/Ecosia.entitlements diff --git a/Client/Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements b/Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements similarity index 100% rename from Client/Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements rename to Ecosia/Entitlements/AppExtensions/EcosiaBeta.entitlements diff --git a/Client/Ecosia/Entitlements/Ecosia.entitlements b/Ecosia/Entitlements/Ecosia.entitlements similarity index 100% rename from Client/Ecosia/Entitlements/Ecosia.entitlements rename to Ecosia/Entitlements/Ecosia.entitlements diff --git a/Client/Ecosia/Entitlements/EcosiaBeta.entitlements b/Ecosia/Entitlements/EcosiaBeta.entitlements similarity index 100% rename from Client/Ecosia/Entitlements/EcosiaBeta.entitlements rename to Ecosia/Entitlements/EcosiaBeta.entitlements diff --git a/Client/Ecosia/Experiments/Unleash/BrazeIntegrationExperiment.swift b/Ecosia/Experiments/Unleash/BrazeIntegrationExperiment.swift similarity index 96% rename from Client/Ecosia/Experiments/Unleash/BrazeIntegrationExperiment.swift rename to Ecosia/Experiments/Unleash/BrazeIntegrationExperiment.swift index bbda45d3d0c6..31e74c262eaf 100644 --- a/Client/Ecosia/Experiments/Unleash/BrazeIntegrationExperiment.swift +++ b/Ecosia/Experiments/Unleash/BrazeIntegrationExperiment.swift @@ -3,7 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core struct BrazeIntegrationExperiment { diff --git a/Client/Ecosia/Experiments/Unleash/NewsletterCardExperiment.swift b/Ecosia/Experiments/Unleash/NewsletterCardExperiment.swift similarity index 78% rename from Client/Ecosia/Experiments/Unleash/NewsletterCardExperiment.swift rename to Ecosia/Experiments/Unleash/NewsletterCardExperiment.swift index b464d9dc263b..58ad64de23af 100644 --- a/Client/Ecosia/Experiments/Unleash/NewsletterCardExperiment.swift +++ b/Ecosia/Experiments/Unleash/NewsletterCardExperiment.swift @@ -3,21 +3,20 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core -struct NewsletterCardExperiment { +public struct NewsletterCardExperiment { private init() {} - static var isEnabled: Bool { + public static var isEnabled: Bool { Unleash.isEnabled(.newsletterCard) } - static var shouldShowCard: Bool { + public static var shouldShowCard: Bool { isEnabled && !isDismissed } /// Send onboarding card view analytics event, but just the first time it's called. - static func trackExperimentImpression() { + public static func trackExperimentImpression() { let trackExperimentImpressionKey = "newsletterCardExperimentImpression" guard !UserDefaults.standard.bool(forKey: trackExperimentImpressionKey) else { return @@ -29,16 +28,16 @@ struct NewsletterCardExperiment { // MARK: Dismissed private static let dismissedKey = "newsletterCardExperimentDismissed" - static var isDismissed: Bool { + public static var isDismissed: Bool { UserDefaults.standard.bool(forKey: dismissedKey) } - static func setDismissed() { + public static func setDismissed() { UserDefaults.standard.set(true, forKey: dismissedKey) } /// Should only be used in Debug! - static func unsetDismissed() { + public static func unsetDismissed() { UserDefaults.standard.removeObject(forKey: dismissedKey) } } diff --git a/Client/Ecosia/Extensions/AppInfo+Ecosia.swift b/Ecosia/Extensions/AppInfo+Ecosia.swift similarity index 100% rename from Client/Ecosia/Extensions/AppInfo+Ecosia.swift rename to Ecosia/Extensions/AppInfo+Ecosia.swift diff --git a/Ecosia/Extensions/Bundle+Ecosia.swift b/Ecosia/Extensions/Bundle+Ecosia.swift new file mode 100644 index 000000000000..db0cd16eb208 --- /dev/null +++ b/Ecosia/Extensions/Bundle+Ecosia.swift @@ -0,0 +1,11 @@ +// 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 Foundation + +extension Bundle { + public static var ecosia: Bundle { + Bundle(identifier: "com.ecosia.framework.Ecosia")! + } +} diff --git a/Client/Ecosia/Extensions/DeviceInfo+Ecosia.swift b/Ecosia/Extensions/DeviceInfo+Ecosia.swift similarity index 99% rename from Client/Ecosia/Extensions/DeviceInfo+Ecosia.swift rename to Ecosia/Extensions/DeviceInfo+Ecosia.swift index 934e2c6a8f7e..04b9ce994ac6 100644 --- a/Client/Ecosia/Extensions/DeviceInfo+Ecosia.swift +++ b/Ecosia/Extensions/DeviceInfo+Ecosia.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Shared +import UIKit import Common extension DeviceInfo { diff --git a/Client/Ecosia/Fake/FakeNimbus.swift b/Ecosia/Fake/FakeNimbus.swift similarity index 100% rename from Client/Ecosia/Fake/FakeNimbus.swift rename to Ecosia/Fake/FakeNimbus.swift diff --git a/Client/Ecosia/Fake/FakeSentry.swift b/Ecosia/Fake/FakeSentry.swift similarity index 100% rename from Client/Ecosia/Fake/FakeSentry.swift rename to Ecosia/Fake/FakeSentry.swift diff --git a/Client/Ecosia/Fake/FakeTelemetry.swift b/Ecosia/Fake/FakeTelemetry.swift similarity index 100% rename from Client/Ecosia/Fake/FakeTelemetry.swift rename to Ecosia/Fake/FakeTelemetry.swift diff --git a/Client/Ecosia/FeatureManagement/FeatureManagement.swift b/Ecosia/FeatureManagement/FeatureManagement.swift similarity index 93% rename from Client/Ecosia/FeatureManagement/FeatureManagement.swift rename to Ecosia/FeatureManagement/FeatureManagement.swift index eb9d8a8fcf55..20cea51ac34d 100644 --- a/Client/Ecosia/FeatureManagement/FeatureManagement.swift +++ b/Ecosia/FeatureManagement/FeatureManagement.swift @@ -4,9 +4,8 @@ import Foundation import Common -import Core -struct FeatureManagement { +public struct FeatureManagement { // MARK: - Initialization @@ -15,7 +14,7 @@ struct FeatureManagement { // MARK: - Configuration /// Fetches the feature configuration asynchronously. - static func fetchConfiguration() async { + public static func fetchConfiguration() async { do { try await start() } catch { diff --git a/Client/Ecosia/Helpers/AppInfoProvider/AppVersionInfoProvider.swift b/Ecosia/Helpers/AppInfoProvider/AppVersionInfoProvider.swift similarity index 86% rename from Client/Ecosia/Helpers/AppInfoProvider/AppVersionInfoProvider.swift rename to Ecosia/Helpers/AppInfoProvider/AppVersionInfoProvider.swift index cf509602a293..6158ef7f8e52 100644 --- a/Client/Ecosia/Helpers/AppInfoProvider/AppVersionInfoProvider.swift +++ b/Ecosia/Helpers/AppInfoProvider/AppVersionInfoProvider.swift @@ -4,6 +4,6 @@ import Foundation -protocol AppVersionInfoProvider { +public protocol AppVersionInfoProvider { var version: String { get } } diff --git a/Client/Ecosia/Helpers/AppInfoProvider/DefaultAppVersionInfoProvider.swift b/Ecosia/Helpers/AppInfoProvider/DefaultAppVersionInfoProvider.swift similarity index 64% rename from Client/Ecosia/Helpers/AppInfoProvider/DefaultAppVersionInfoProvider.swift rename to Ecosia/Helpers/AppInfoProvider/DefaultAppVersionInfoProvider.swift index 0fc4c91ed236..6b71cc62ed5e 100644 --- a/Client/Ecosia/Helpers/AppInfoProvider/DefaultAppVersionInfoProvider.swift +++ b/Ecosia/Helpers/AppInfoProvider/DefaultAppVersionInfoProvider.swift @@ -2,12 +2,14 @@ // 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 Shared +import Foundation import Common -struct DefaultAppVersionInfoProvider: AppVersionInfoProvider { +public struct DefaultAppVersionInfoProvider: AppVersionInfoProvider { - var version: String { + public init() { } + + public var version: String { return AppInfo.ecosiaAppVersion } } diff --git a/Client/Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType+Extensions.swift b/Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType+Extensions.swift similarity index 90% rename from Client/Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType+Extensions.swift rename to Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType+Extensions.swift index 13dca5d22eda..6c39cc534f1e 100644 --- a/Client/Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType+Extensions.swift +++ b/Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType+Extensions.swift @@ -2,7 +2,7 @@ // 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 Core +import Foundation /// This extension provides functionality to evaluate the current Ecosia installation type based on the provided app version information. extension EcosiaInstallType { @@ -17,7 +17,7 @@ extension EcosiaInstallType { /// /// - Warning: Ensure that `User.shared.firstTime` and `versionProvider.version` are correctly initialized before calling this function. /// - static func evaluateCurrentEcosiaInstallType(withVersionProvider versionProvider: AppVersionInfoProvider = DefaultAppVersionInfoProvider(), storeUpgradeVersion: Bool = false) { + public static func evaluateCurrentEcosiaInstallType(withVersionProvider versionProvider: AppVersionInfoProvider = DefaultAppVersionInfoProvider(), storeUpgradeVersion: Bool = false) { if User.shared.firstTime && EcosiaInstallType.get() == .unknown { diff --git a/Client/Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType.swift b/Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType.swift similarity index 93% rename from Client/Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType.swift rename to Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType.swift index 99ae657668e7..42c8732ce7fb 100644 --- a/Client/Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType.swift +++ b/Ecosia/Helpers/EcosiaInstallType/EcosiaInstallType.swift @@ -5,7 +5,7 @@ import Foundation /// Represents the type of Ecosia installation. -enum EcosiaInstallType: String { +public enum EcosiaInstallType: String { /// Represents a fresh installation of Ecosia. case fresh @@ -29,7 +29,7 @@ enum EcosiaInstallType: String { /// Retrieves the current Ecosia install type from UserDefaults. /// /// - Returns: The current Ecosia install type. If not found, returns `.unknown`. - static func get() -> EcosiaInstallType { + public static func get() -> EcosiaInstallType { guard let rawValue = UserDefaults.standard.string(forKey: Self.installTypeKey), let type = EcosiaInstallType(rawValue: rawValue) else { return unknown } @@ -47,7 +47,7 @@ enum EcosiaInstallType: String { /// Retrieves the persisted current version of Ecosia from UserDefaults. /// /// - Returns: The persisted current version. If not found, returns an empty string. - static func persistedCurrentVersion() -> String { + public static func persistedCurrentVersion() -> String { guard let currentVersion = UserDefaults.standard.string(forKey: Self.currentInstalledVersionKey) else { return "" } return currentVersion } diff --git a/Client/Ecosia/Helpers/Version/Version+Extensions.swift b/Ecosia/Helpers/Version/Version+Extensions.swift similarity index 99% rename from Client/Ecosia/Helpers/Version/Version+Extensions.swift rename to Ecosia/Helpers/Version/Version+Extensions.swift index 0d72cd7f9dd5..7ba87dcc8e84 100644 --- a/Client/Ecosia/Helpers/Version/Version+Extensions.swift +++ b/Ecosia/Helpers/Version/Version+Extensions.swift @@ -2,7 +2,7 @@ // 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 Shared +import Foundation import Common /// Extension handling previous version retrieval and saving current version. diff --git a/Client/Ecosia/Helpers/Version/Version.swift b/Ecosia/Helpers/Version/Version.swift similarity index 87% rename from Client/Ecosia/Helpers/Version/Version.swift rename to Ecosia/Helpers/Version/Version.swift index 622cde9cb142..e8469bae0bbf 100644 --- a/Client/Ecosia/Helpers/Version/Version.swift +++ b/Ecosia/Helpers/Version/Version.swift @@ -7,7 +7,7 @@ import Foundation /// Represents a semantic version of an app. /// /// A semantic version is typically represented as a series of numbers separated by dots, e.g., "1.0.0". -struct Version: CustomStringConvertible { +public struct Version: CustomStringConvertible { var major: Int var minor: Int @@ -16,7 +16,7 @@ struct Version: CustomStringConvertible { /// Initializes a new `Version` from a string representation. /// /// - Parameter versionString: A string containing the semantic version, e.g., "1.0.0". - init?(_ versionString: String) { + public init?(_ versionString: String) { let components = versionString.split(separator: ".") guard components.count == 3, let major = Int(components[0]), @@ -31,7 +31,7 @@ struct Version: CustomStringConvertible { } /// A string representation of the `Version`. - var description: String { + public var description: String { return "\(major).\(minor).\(patch)" } } @@ -45,7 +45,7 @@ extension Version: Comparable { /// - rhs: Another `Version`. /// /// - Returns: `true` if both instances represent the same version, `false` otherwise. - static func == (lhs: Version, rhs: Version) -> Bool { + public static func == (lhs: Version, rhs: Version) -> Bool { return lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch } @@ -56,7 +56,7 @@ extension Version: Comparable { /// - rhs: Another `Version`. /// /// - Returns: `true` if the instance on the left should come before the one on the right, `false` otherwise. - static func < (lhs: Version, rhs: Version) -> Bool { + public static func < (lhs: Version, rhs: Version) -> Bool { if lhs.major != rhs.major { return lhs.major < rhs.major } @@ -72,7 +72,7 @@ extension Version: Hashable { /// Adds this value to the given hasher. /// /// - Parameter hasher: The hasher to use when combining the components of this instance. - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(major) hasher.combine(minor) hasher.combine(patch) diff --git a/Ecosia/Info.plist b/Ecosia/Info.plist new file mode 100644 index 000000000000..59d4351087bf --- /dev/null +++ b/Ecosia/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 18.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/Client/Ecosia/L10N/Scripts/Clean.swift b/Ecosia/L10N/Scripts/Clean.swift similarity index 100% rename from Client/Ecosia/L10N/Scripts/Clean.swift rename to Ecosia/L10N/Scripts/Clean.swift diff --git a/Client/Ecosia/L10N/Scripts/Validate.swift b/Ecosia/L10N/Scripts/Validate.swift similarity index 100% rename from Client/Ecosia/L10N/Scripts/Validate.swift rename to Ecosia/L10N/Scripts/Validate.swift diff --git a/Client/Ecosia/L10N/String.swift b/Ecosia/L10N/String.swift similarity index 98% rename from Client/Ecosia/L10N/String.swift rename to Ecosia/L10N/String.swift index 0913a4c34657..55dcd475ffe2 100644 --- a/Client/Ecosia/L10N/String.swift +++ b/Ecosia/L10N/String.swift @@ -3,22 +3,21 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core extension String { - static func localized(_ key: Key) -> String { + public static func localized(_ key: Key) -> String { localized(key.rawValue) } - static func localized(_ string: String) -> String { + public static func localized(_ string: String) -> String { NSLocalizedString(string, tableName: "Ecosia", comment: "") } - static func localizedPlural(_ key: Key, num: Int) -> String { + public static func localizedPlural(_ key: Key, num: Int) -> String { String(format: NSLocalizedString(key.rawValue, tableName: "Plurals", comment: ""), num) } - enum Key: String { + public enum Key: String { case allRegions = "All regions" case autocomplete = "Autocomplete" case climateImpact = "Climate Impact" diff --git a/Client/Ecosia/L10N/de.lproj/Ecosia.strings b/Ecosia/L10N/de.lproj/Ecosia.strings similarity index 100% rename from Client/Ecosia/L10N/de.lproj/Ecosia.strings rename to Ecosia/L10N/de.lproj/Ecosia.strings diff --git a/Client/Ecosia/L10N/de.lproj/Plurals.stringsdict b/Ecosia/L10N/de.lproj/Plurals.stringsdict similarity index 100% rename from Client/Ecosia/L10N/de.lproj/Plurals.stringsdict rename to Ecosia/L10N/de.lproj/Plurals.stringsdict diff --git a/Client/Ecosia/L10N/en.lproj/Ecosia.strings b/Ecosia/L10N/en.lproj/Ecosia.strings similarity index 100% rename from Client/Ecosia/L10N/en.lproj/Ecosia.strings rename to Ecosia/L10N/en.lproj/Ecosia.strings diff --git a/Client/Ecosia/L10N/en.lproj/Plurals.stringsdict b/Ecosia/L10N/en.lproj/Plurals.stringsdict similarity index 100% rename from Client/Ecosia/L10N/en.lproj/Plurals.stringsdict rename to Ecosia/L10N/en.lproj/Plurals.stringsdict diff --git a/Client/Ecosia/L10N/es.lproj/Ecosia.strings b/Ecosia/L10N/es.lproj/Ecosia.strings similarity index 100% rename from Client/Ecosia/L10N/es.lproj/Ecosia.strings rename to Ecosia/L10N/es.lproj/Ecosia.strings diff --git a/Client/Ecosia/L10N/es.lproj/Plurals.stringsdict b/Ecosia/L10N/es.lproj/Plurals.stringsdict similarity index 100% rename from Client/Ecosia/L10N/es.lproj/Plurals.stringsdict rename to Ecosia/L10N/es.lproj/Plurals.stringsdict diff --git a/Client/Ecosia/L10N/fr.lproj/Ecosia.strings b/Ecosia/L10N/fr.lproj/Ecosia.strings similarity index 100% rename from Client/Ecosia/L10N/fr.lproj/Ecosia.strings rename to Ecosia/L10N/fr.lproj/Ecosia.strings diff --git a/Client/Ecosia/L10N/fr.lproj/Plurals.stringsdict b/Ecosia/L10N/fr.lproj/Plurals.stringsdict similarity index 100% rename from Client/Ecosia/L10N/fr.lproj/Plurals.stringsdict rename to Ecosia/L10N/fr.lproj/Plurals.stringsdict diff --git a/Client/Ecosia/L10N/it.lproj/Ecosia.strings b/Ecosia/L10N/it.lproj/Ecosia.strings similarity index 100% rename from Client/Ecosia/L10N/it.lproj/Ecosia.strings rename to Ecosia/L10N/it.lproj/Ecosia.strings diff --git a/Client/Ecosia/L10N/it.lproj/Plurals.stringsdict b/Ecosia/L10N/it.lproj/Plurals.stringsdict similarity index 100% rename from Client/Ecosia/L10N/it.lproj/Plurals.stringsdict rename to Ecosia/L10N/it.lproj/Plurals.stringsdict diff --git a/Client/Ecosia/L10N/nl.lproj/Ecosia.strings b/Ecosia/L10N/nl.lproj/Ecosia.strings similarity index 100% rename from Client/Ecosia/L10N/nl.lproj/Ecosia.strings rename to Ecosia/L10N/nl.lproj/Ecosia.strings diff --git a/Client/Ecosia/L10N/nl.lproj/Plurals.stringsdict b/Ecosia/L10N/nl.lproj/Plurals.stringsdict similarity index 100% rename from Client/Ecosia/L10N/nl.lproj/Plurals.stringsdict rename to Ecosia/L10N/nl.lproj/Plurals.stringsdict diff --git a/Ecosia/README.md b/Ecosia/README.md new file mode 100644 index 000000000000..3f33c5e049a1 --- /dev/null +++ b/Ecosia/README.md @@ -0,0 +1,14 @@ +# Ecosia framework + +The Ecosia Framework aims to be a wrapper of all our Ecosia isolated implementation and logic. +Some of the Ecosia codebase still lives under the main project `Client/Ecosia` but the goal is to bring as much codebase as possible as part of this dedicated framework. + +## Architectural Decision records + +Architectural Decision Records (or ADRs) are a method of documenting important decisions a software team makes, and why the decisions were made. They are similar, but complementary to RFCs (requests for comment). The purpose of ADRs is to build up a terse and easily searchable log of decisions so future generations of engineers can understand why our systems are the way they are. For more information, see the [ADR site](https://adr.github.io).\ +The are listed in numbered order in the [adr](Ecosia/Core/adr/) directory and should follow this [Y-statement-format](Ecosia/Core/adr/0_2021-04-27_Architectural-decision-record-template.md). + +### Example + +- [1_2021-04-27_File-based-user-data-persistence.md](Core/adr/1_2021-04-27_File-based-user-data-persistence.md) +- [2_2023-06-29_Environment_as_getter.md](Core/adr/2_2023-06-29_Environment_as_getter.md) \ No newline at end of file diff --git a/Client/Ecosia/markets.json b/Ecosia/markets.json similarity index 100% rename from Client/Ecosia/markets.json rename to Ecosia/markets.json diff --git a/EcosiaTests/Analytics/AnalyticsSpyTests.swift b/EcosiaTests/Analytics/AnalyticsSpyTests.swift index ed8f11e77e89..a4d92e9e2958 100644 --- a/EcosiaTests/Analytics/AnalyticsSpyTests.swift +++ b/EcosiaTests/Analytics/AnalyticsSpyTests.swift @@ -3,10 +3,10 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import XCTest -import Core import Storage import SnowplowTracker @testable import Client +@testable import Ecosia // MARK: - AnalyticsSpy diff --git a/EcosiaTests/Analytics/AnalyticsTests.swift b/EcosiaTests/Analytics/AnalyticsTests.swift index 21bd7c9d7f15..bfaad90f5f6e 100644 --- a/EcosiaTests/Analytics/AnalyticsTests.swift +++ b/EcosiaTests/Analytics/AnalyticsTests.swift @@ -6,6 +6,7 @@ import Foundation import XCTest @testable import Client +@testable import Ecosia final class AnalyticsTests: XCTestCase { diff --git a/EcosiaTests/BrazeServiceTests.swift b/EcosiaTests/BrazeServiceTests.swift index 9f6128aa2e42..0d3b642ed12c 100644 --- a/EcosiaTests/BrazeServiceTests.swift +++ b/EcosiaTests/BrazeServiceTests.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import XCTest -@testable import Client +@testable import Ecosia final class BrazeServiceTests: XCTestCase { var brazeService: BrazeService! diff --git a/EcosiaTests/BundleImageFetcherTests.swift b/EcosiaTests/BundleImageFetcherTests.swift new file mode 100644 index 000000000000..fbb8ace37321 --- /dev/null +++ b/EcosiaTests/BundleImageFetcherTests.swift @@ -0,0 +1,36 @@ +// 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 SiteImageView +@testable import Ecosia + +final class BundleImageFetcherTests: XCTestCase { + + var urlProvider: URLProvider = .staging + + func testFinancialReportsURLs() { + + let def = Language.current + Language.current = .en + EcosiaURLProvider.Language.current = .en + XCTAssertNotNil(urlProvider.financialReports) + XCTAssertEqual(urlProvider.financialReports.absoluteString, EcosiaURLProvider.financialReportsURL.absoluteString) + + Language.current = .fr + EcosiaURLProvider.Language.current = .fr + XCTAssertNotNil(urlProvider.financialReports) + XCTAssertEqual(urlProvider.financialReports.absoluteString, EcosiaURLProvider.financialReportsURL.absoluteString) + + Language.current = .de + EcosiaURLProvider.Language.current = .de + XCTAssertNotNil(urlProvider.financialReports) + XCTAssertEqual(urlProvider.financialReports.absoluteString, EcosiaURLProvider.financialReportsURL.absoluteString) + } + + func testPrivacyURLs() { + XCTAssertNotNil(urlProvider.privacy) + XCTAssertEqual(urlProvider.privacy.absoluteString, EcosiaURLProvider.privacyURL.absoluteString) + } +} diff --git a/EcosiaTests/Core/BookmarkParserTests.swift b/EcosiaTests/Core/BookmarkParserTests.swift new file mode 100644 index 000000000000..c62fc25d62c2 --- /dev/null +++ b/EcosiaTests/Core/BookmarkParserTests.swift @@ -0,0 +1,37 @@ +@testable import Ecosia +import XCTest + +final class BookmarkParserTests: XCTestCase { + func testParsesFolderAndBookmarks_Chrome() async throws { + let html = BookmarkFixtures.html(.chrome).value + + let importer = try BookmarkParser(html: html) + let bookmarks = try await importer.parseBookmarks() + + let expectedResult = BookmarkFixtures.debugString(.chrome).value + + XCTAssertEqual(bookmarks.debugDescription, expectedResult) + } + + func testParsesFolderAndBookmarks_Firefox() async throws { + let html = BookmarkFixtures.html(.firefox).value + + let importer = try BookmarkParser(html: html) + let bookmarks = try await importer.parseBookmarks() + + let expectedResult = BookmarkFixtures.debugString(.firefox).value + + XCTAssertEqual(bookmarks.debugDescription, expectedResult) + } + + func testParsesFolderAndBookmarks_Safari() async throws { + let html = BookmarkFixtures.html(.safari).value + + let importer = try BookmarkParser(html: html) + let bookmarks = try await importer.parseBookmarks() + + let expectedResult = BookmarkFixtures.debugString(.safari).value + + XCTAssertEqual(bookmarks.debugDescription, expectedResult) + } +} diff --git a/EcosiaTests/Core/BookmarkSerializerTests.swift b/EcosiaTests/Core/BookmarkSerializerTests.swift new file mode 100644 index 000000000000..652c9b512b5d --- /dev/null +++ b/EcosiaTests/Core/BookmarkSerializerTests.swift @@ -0,0 +1,27 @@ +@testable import Ecosia +import XCTest + +final class BookmarkSerializerTests: XCTestCase { + func test_SerializesBookmarks() async throws { + let bookmarks: [BookmarkItem] = [ + .folder("Favorites", [ + .bookmark("One", "https://example.com/one", .empty), + .bookmark("Two", "https://example.com/two", .empty), + .folder("My Folder #1", [ + .bookmark("Three", "https://example.com/three", .init(addedAt: .init(timeIntervalSince1970: 123), modifiedAt: nil)) + ], .empty), + .folder("My Folder #2", [ + .bookmark("Four", "https://example.com/four", .init(addedAt: .init(timeIntervalSince1970: 456), modifiedAt: nil)), + .folder("My Subfolder #1", [ + .bookmark("Five", "https://example.com/five", .init(addedAt: .init(timeIntervalSince1970: 789), modifiedAt: .init(timeIntervalSince1970: 987))) + ], .empty), + ], .empty) + ], .empty) + ] + + let html = try await BookmarkSerializer().serializeBookmarks(bookmarks) + .filter { !$0.isWhitespace } + + XCTAssertEqual(html, BookmarkFixtures.ecosiaExportedHtml.filter { !$0.isWhitespace }) + } +} diff --git a/EcosiaTests/Core/BookmarkTests.swift b/EcosiaTests/Core/BookmarkTests.swift new file mode 100644 index 000000000000..4a1383db621a --- /dev/null +++ b/EcosiaTests/Core/BookmarkTests.swift @@ -0,0 +1,20 @@ +@testable import Ecosia +import XCTest + +final class BookmarkTests: XCTestCase { + func test_BookmarkSerializer_Parser() async throws { + let input: [BookmarkItem] = [ + .bookmark("One", "https://example.com/one", .empty), + .folder("My first folder 😆", [ + .bookmark("Two &!/{}", "https://example.com/two", .empty), + .bookmark("Three ÖÄÜ'*", "https://example.com/three", .empty) + ], .empty) + ] + + let html = try await BookmarkSerializer().serializeBookmarks(input) + + let output = try await BookmarkParser(html: html).parseBookmarks() + + XCTAssertEqual(input, output) + } +} diff --git a/EcosiaTests/Core/Bundle+EcosiaTests.swift b/EcosiaTests/Core/Bundle+EcosiaTests.swift new file mode 100644 index 000000000000..b5f340e9e187 --- /dev/null +++ b/EcosiaTests/Core/Bundle+EcosiaTests.swift @@ -0,0 +1,11 @@ +// 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 Foundation + +extension Bundle { + static var ecosiaTests: Bundle { + Bundle(identifier: "com.ecosia.framework.EcosiaTests")! + } +} diff --git a/EcosiaTests/Core/CookieTests.swift b/EcosiaTests/Core/CookieTests.swift new file mode 100644 index 000000000000..1a233c7580bb --- /dev/null +++ b/EcosiaTests/Core/CookieTests.swift @@ -0,0 +1,250 @@ +@testable import Ecosia +import XCTest + +final class CookieTests: XCTestCase { + + var urlProvider: URLProvider = .production + + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + func testDefaults() { + XCTAssertEqual("ECFG", Cookie.makeStandardCookie(urlProvider).name) + XCTAssertEqual(".ecosia.org", Cookie.makeStandardCookie(urlProvider).domain) + XCTAssertEqual("/", Cookie.makeStandardCookie(urlProvider).path) + } + + func testIncognitoValuesNoPersonalData() { + User.shared.searchCount = 1234 + User.shared.id = "neverland" + + let incognitoDict = Cookie.makeIncognitoCookie(urlProvider).value.components(separatedBy: ":").map { $0.components(separatedBy: "=") }.reduce(into: [String: String]()) { + guard $1.count == 2 else { return } + $0[$1[0]] = $1[1] + } + XCTAssertNil(incognitoDict["cid"]) + XCTAssertNil(incognitoDict["t"]) + + let standardDict = Cookie.makeStandardCookie(urlProvider).value.components(separatedBy: ":").map { $0.components(separatedBy: "=") }.reduce(into: [String: String]()) { + guard $1.count == 2 else { return } + $0[$1[0]] = $1[1] + } + XCTAssertNotNil(standardDict["cid"]) + XCTAssertNotNil(standardDict["t"]) + } + + func testMakeConsentCookieReturnsCookieWhenValueStoredInUser() { + User.shared.cookieConsentValue = "eampg" + + XCTAssertNotNil(Cookie.makeConsentCookie(urlProvider)) + XCTAssertEqual(Cookie.makeConsentCookie(urlProvider)?.name, Cookie.consent.name) + } + + func testMakeConsentCookieDoesNotReturnCookieWhenValueNotStoredInUser() { + User.shared.cookieConsentValue = nil + + XCTAssertNil(Cookie.makeConsentCookie(urlProvider)) + } + + func testDefaultsAddingUser() { + User.shared.searchCount = 1234 + User.shared.marketCode = .es_cl + User.shared.adultFilter = .off + User.shared.autoComplete = false + User.shared.id = "neverland" + User.shared.personalized = true + + let dictionary = Cookie.makeStandardCookie(urlProvider).value.components(separatedBy: ":").map { $0.components(separatedBy: "=") }.reduce(into: [String: String]()) { + guard $1.count == 2 else { return } + $0[$1[0]] = $1[1] + } + XCTAssertEqual(Language.current.rawValue, dictionary["l"]) + XCTAssertEqual("1234", dictionary["t"]) + XCTAssertEqual("n", dictionary["f"]) + XCTAssertEqual("es-cl", dictionary["mc"]) + XCTAssertEqual("0", dictionary["as"]) + XCTAssertEqual("1", dictionary["ma"]) + XCTAssertEqual("1", dictionary["mr"]) + XCTAssertEqual("mobile", dictionary["dt"]) + XCTAssertEqual("0", dictionary["fs"]) + XCTAssertEqual("neverland", dictionary["cid"]) + XCTAssertEqual("1", dictionary["a"]) + XCTAssertEqual("1", dictionary["pz"]) + } + + func testDefaultsNoUserId() { + User.shared.id = nil + + let dictionary = Cookie.makeStandardCookie(urlProvider).value.components(separatedBy: ":").map { $0.components(separatedBy: "=") }.reduce(into: [String: String]()) { + guard $1.count == 2 else { return } + $0[$1[0]] = $1[1] + } + XCTAssertEqual(Language.current.rawValue, dictionary["l"]) + XCTAssertNil(dictionary["cid"]) + } + + func testReceivedInvalidDomain() { + User.shared.searchCount = 3 + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.it", .path: "/", .value: "as=0:f=s:mc=en-uk:t=9999"])!]) + + let dictionary = Cookie.makeStandardCookie(urlProvider).value.components(separatedBy: ":").map { $0.components(separatedBy: "=") }.reduce(into: [String: String]()) { + guard $1.count == 2 else { return } + $0[$1[0]] = $1[1] + } + XCTAssertEqual("3", dictionary["t"]) + } + + func testReceiving() { + User.shared.searchCount = 0 + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=0:f=s:mc=en-uk:t=9999:cid=loremipsum"])!]) + + let dictionary = Cookie.makeStandardCookie(urlProvider).value.components(separatedBy: ":").map { $0.components(separatedBy: "=") }.reduce(into: [String: String]()) { + guard $1.count == 2 else { return } + $0[$1[0]] = $1[1] + } + XCTAssertEqual("9999", dictionary["t"]) + XCTAssertEqual("loremipsum", dictionary["cid"]) + } + + func testReceivingUpdatesCounter() { + User.shared.searchCount = 0 + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=0:f=s:mc=en-uk:t=9999"])!]) + + XCTAssertEqual(9999, User.shared.searchCount) + } + + func testReceivingUpdatesCounterToZero() { + User.shared.searchCount = 5 + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=0:f=s:mc=en-uk:t=0"])!]) + + XCTAssertEqual(0, User.shared.searchCount) + } + + func testReceivingUpdatesCounterOnlyIncrease() { + User.shared.searchCount = 5 + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=0:f=s:mc=en-uk:t=4"])!]) + + XCTAssertEqual(5, User.shared.searchCount) + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=0:f=s:mc=en-uk:t=6"])!]) + XCTAssertEqual(6, User.shared.searchCount) + } + + func testReceivingUpdatesUserId() { + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=0:f=s:mc=en-uk:t=9999:cid=lorem"])!]) + + XCTAssertEqual("lorem", User.shared.id) + } + + func testReceivingTreeCount() { + User.shared.searchCount = 0 + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "t=9999"])!]) + XCTAssertEqual(9999, User.shared.searchCount) + } + + func testReceivingAdultFilter() { + User.shared.adultFilter = .off + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "f=i"])!]) + XCTAssert(User.shared.adultFilter == .moderate) + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "f=y"])!]) + XCTAssert(User.shared.adultFilter == .strict) + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "f=n"])!]) + XCTAssert(User.shared.adultFilter == .off) + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "f=foo"])!]) + XCTAssert(User.shared.adultFilter == .off) + } + + func testReceivingMarketCode() { + User.shared.marketCode = .en_us + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "mc=en-gb"])!]) + XCTAssert(User.shared.marketCode == .en_gb) + } + + func testReceivingAutocomplete() { + User.shared.autoComplete = false + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=1"])!]) + XCTAssertTrue(User.shared.autoComplete) + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=0"])!]) + XCTAssertFalse(User.shared.autoComplete) + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=foo"])!]) + XCTAssertFalse(User.shared.autoComplete) + } + + func testReceivingPersonalisedSearch() { + User.shared.personalized = false + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "pz=1"])!]) + XCTAssertTrue(User.shared.personalized) + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "pz=0"])!]) + XCTAssertFalse(User.shared.personalized) + + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "pz=foo"])!]) + XCTAssertFalse(User.shared.personalized) + } + + func testRemoveUnknownProperties() { + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=0:name=test"])!]) + + let dictionary = Cookie.makeStandardCookie(urlProvider).value.components(separatedBy: ":").map { $0.components(separatedBy: "=") }.reduce(into: [String: String]()) { + guard $1.count == 2 else { return } + $0[$1[0]] = $1[1] + } + XCTAssertNil(dictionary["name"]) + } + + func testReceivingMaintainsUserId() { + User.shared.id = "hello" + extractReceivedCookies([HTTPCookie(properties: [.name: "ECFG", .domain: ".ecosia.org", .path: "/", .value: "as=0:f=s:mc=en-uk:t=9999"])!]) + + XCTAssertEqual("hello", User.shared.id) + } + + func testReceivedInvalidName() { + User.shared.searchCount = 3 + extractReceivedCookies([HTTPCookie(properties: [.name: "Facebook", .domain: ".ecosia.org", .path: "/", .value: "as=0:f=s:mc=en-uk:t=9999"])!]) + + let dictionary = Cookie.makeStandardCookie(urlProvider).value.components(separatedBy: ":").map { $0.components(separatedBy: "=") }.reduce(into: [String: String]()) { + guard $1.count == 2 else { return } + $0[$1[0]] = $1[1] + } + XCTAssertEqual("3", dictionary["t"]) + } + + func testExtractECCCNoAnalyticsConsent() { + User.shared.cookieConsentValue = "e" + extractReceivedCookies([HTTPCookie(properties: [.name: "ECCC", .domain: ".ecosia.org", .path: "/", .value: "e"])!]) + + XCTAssertEqual(User.shared.cookieConsentValue, "e") + XCTAssertFalse(User.shared.hasAnalyticsCookieConsent) + } + + func testExtractECCCWithAnalyticsConsent() { + User.shared.cookieConsentValue = "e" + extractReceivedCookies([HTTPCookie(properties: [.name: "ECCC", .domain: ".ecosia.org", .path: "/", .value: "eampg"])!]) + + XCTAssertEqual(User.shared.cookieConsentValue, "eampg") + XCTAssertTrue(User.shared.hasAnalyticsCookieConsent) + } +} + +extension CookieTests { + + /// This function calls the original `Cookie.received` injecting the `urlProvider` utilized for tests + func extractReceivedCookies(_ cookies: [HTTPCookie]) { + Cookie.received(cookies, urlProvider: urlProvider) + } +} diff --git a/EcosiaTests/Core/FavouritesTests.swift b/EcosiaTests/Core/FavouritesTests.swift new file mode 100644 index 000000000000..91ca5f261f1e --- /dev/null +++ b/EcosiaTests/Core/FavouritesTests.swift @@ -0,0 +1,38 @@ +@testable import Ecosia +import XCTest + +final class FavouritesTests: XCTestCase { + + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.pages) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.pages) + } + + func testSave() { + let expect = expectation(description: "") + let favourites = Favourites() + favourites.items.append(.init(url: URL(string: "https://avocado.com")!, title: "")) + PageStore.queue.async { + XCTAssertTrue(FileManager.default.fileExists(atPath: FileManager.favourites.path)) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLoad() { + let expect = expectation(description: "") + var favourites = Favourites() + favourites.items.append(.init(url: URL(string: "https://www.avocado.com")!, title: "hello world")) + favourites.items.append(.init(url: URL(string: "https://www.guacamole.com")!, title: "lorem ipsum")) + PageStore.queue.async { + favourites = .init() + XCTAssertEqual("hello world", favourites.items.first?.title) + XCTAssertEqual("lorem ipsum", favourites.items.last?.title) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } +} diff --git a/EcosiaTests/Core/FeatureFlaggingSessionInitializerTests.swift b/EcosiaTests/Core/FeatureFlaggingSessionInitializerTests.swift new file mode 100644 index 000000000000..65d7cc94e39b --- /dev/null +++ b/EcosiaTests/Core/FeatureFlaggingSessionInitializerTests.swift @@ -0,0 +1,41 @@ +import XCTest +import Foundation +@testable import Ecosia + +class FeatureManagementSessiontInitializerTests: XCTestCase { + + func testInitializeSession() async throws { + + // given + let initializer = FeatureManagementSessiontInitializerSPY() + + // when + let expect = expectation(description: "The session initialization should be sent asynchronously") + do { + let _: DummyResponse = try await initializer.startSession()! + XCTAssertTrue(initializer.initializeSessionRequestSent) + expect.fulfill() + } catch { + XCTFail("Initializing session failed: \(error.localizedDescription)") + } + + wait(for: [expect], timeout: 1.0) + } +} + +extension FeatureManagementSessiontInitializerTests { + + class FeatureManagementSessiontInitializerSPY: FeatureManagementSessionInitializer { + + var initializeSessionRequestSent: Bool! + + func startSession() async throws -> T? where T: Decodable { + initializeSessionRequestSent = true + return DummyResponse(value: 0) as? T + } + } + + struct DummyResponse: Decodable { + let value: Int + } +} diff --git a/EcosiaTests/Core/FinancialReportsTests.swift b/EcosiaTests/Core/FinancialReportsTests.swift new file mode 100644 index 000000000000..613a90ff1e90 --- /dev/null +++ b/EcosiaTests/Core/FinancialReportsTests.swift @@ -0,0 +1,40 @@ +@testable import Ecosia +import XCTest + +final class FinancialReportsTests: XCTestCase { + private var financialReports: FinancialReports! + private var mockURLSession: MockURLSessionProtocol! + + override func setUp() { + financialReports = FinancialReports.shared + mockURLSession = MockURLSessionProtocol() + } + + func testFetchAndUpdate() async throws { + mockURLSession.data = Data(""" + { + "2023-8": { "totalIncome": 456, "numberOfTreesFinanced": 11 }, + "2023-7": { "totalIncome": 123, "numberOfTreesFinanced": 10 } + } + """.utf8) + + try await financialReports.fetchAndUpdate(urlSession: mockURLSession) + + XCTAssertEqual(financialReports.latestMonth, Date(timeIntervalSince1970: 1690848000)) + XCTAssertEqual(financialReports.latestReport, + FinancialReports.Report(totalIncome: 456, + numberOfTreesFinanced: 11)) + } + + func testLocalizedMonthAndYear() async throws { + mockURLSession.data = Data(""" + { + "2021-5": { "totalIncome": 1, "numberOfTreesFinanced": 1 } + } + """.utf8) + + try await financialReports.fetchAndUpdate(urlSession: mockURLSession) + + XCTAssertEqual(financialReports.localizedMonthAndYear, "May 2021") + } +} diff --git a/EcosiaTests/Core/HistoryTests.swift b/EcosiaTests/Core/HistoryTests.swift new file mode 100644 index 000000000000..73104919a48e --- /dev/null +++ b/EcosiaTests/Core/HistoryTests.swift @@ -0,0 +1,72 @@ +@testable import Ecosia +import XCTest + +final class HistoryTests: XCTestCase { + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.pages) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.pages) + } + + func testSave() { + let expect = expectation(description: "") + let history = History() + history.add(.init(url: URL(string: "https://avocado.com")!, title: "hello world")) + PageStore.queue.async { + XCTAssertTrue(FileManager.default.fileExists(atPath: FileManager.history.path)) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLoad() { + let expect = expectation(description: "") + var history = History() + history.add(.init(url: URL(string: "https://avocado.com")!, title: "hello world")) + history.add(.init(url: URL(string: "https://guacamole.com")!, title: "lorem ipsum")) + PageStore.queue.async { + history = .init() + PageStore.queue.async { + XCTAssertGreaterThan(history.items.first!.0, Date(timeIntervalSince1970: 1)) + XCTAssertGreaterThan(history.items.last!.0, Date(timeIntervalSince1970: 1)) + XCTAssertEqual("hello world", history.items.first?.1.title) + XCTAssertEqual("lorem ipsum", history.items.last?.1.title) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } + + func testDelete() { + let expect = expectation(description: "") + var history = History() + history.add(.init(url: URL(string: "https://avocado.com")!, title: "hello world")) + PageStore.queue.async { + history = .init() + history.delete(history.items.first!.0) + PageStore.queue.async { + XCTAssertTrue(History().items.isEmpty) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } + + func testDeleteAll() { + let expect = expectation(description: "") + var history = History() + history.add(.init(url: URL(string: "https://avocado.com")!, title: "hello world")) + history.add(.init(url: URL(string: "https://guacamlo.com")!, title: "lorem ipsum")) + PageStore.queue.async { + history = .init() + history.deleteAll() + PageStore.queue.async { + XCTAssertTrue(History().items.isEmpty) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } +} diff --git a/EcosiaTests/Core/ImagesTests.swift b/EcosiaTests/Core/ImagesTests.swift new file mode 100644 index 000000000000..742bbdd61d57 --- /dev/null +++ b/EcosiaTests/Core/ImagesTests.swift @@ -0,0 +1,38 @@ +import XCTest +@testable import Ecosia + +final class ImagesTests: XCTestCase { + private var session: MockURLSession! + private var images: Images! + + override func setUp() { + session = .init() + images = .init(session) + } + + func testDownload() { + let expect = expectation(description: "") + let url = URL(string: "avocado.com")! + session.data = [.init("hello".utf8)] + images.load(self, url: url) { + XCTAssertEqual(url, $0.url) + XCTAssertFalse($0.data.isEmpty) + XCTAssertEqual(.main, Thread.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCache() { + let expect = expectation(description: "") + let url = URL(string: "avocado.com")! + session.request = { + XCTFail() + } + images.items.insert(.init(url, .init("hello".utf8))) + images.load(self, url: url) { _ in + expect.fulfill() + } + waitForExpectations(timeout: 1) + } +} diff --git a/EcosiaTests/Core/InvestmentsProjectionTests.swift b/EcosiaTests/Core/InvestmentsProjectionTests.swift new file mode 100644 index 000000000000..6be83bcee67c --- /dev/null +++ b/EcosiaTests/Core/InvestmentsProjectionTests.swift @@ -0,0 +1,34 @@ +@testable import Ecosia +import XCTest + +final class InvestmentsProjectionTests: XCTestCase { + private var investmentsProjection: InvestmentsProjection! + + override func setUp() { + investmentsProjection = InvestmentsProjection.shared + } + + func testTotalInvestedAt() { + let date = Date() + Statistics.shared.totalInvestments = 123456789 + Statistics.shared.totalInvestmentsLastUpdated = date.addingTimeInterval(-100) + Statistics.shared.investmentPerSecond = 0.5 + XCTAssertEqual(Int(100*0.5 + 123456789), investmentsProjection.totalInvestedAt(date)) + } + + func testTimerIsActive() { + let investmentPerSecond = 1.0 + Statistics.shared.investmentPerSecond = investmentPerSecond + + let exp = XCTestExpectation(description: "Wait for timer") + let projection = InvestmentsProjection() + var receivedAmount: Int? + projection.subscribe(self) { amount in + receivedAmount = amount + exp.fulfill() + } + wait(for: [exp], timeout: 1) + + XCTAssertNotNil(receivedAmount) + } +} diff --git a/EcosiaTests/Core/LanguageTests.swift b/EcosiaTests/Core/LanguageTests.swift new file mode 100644 index 000000000000..80670a0827f4 --- /dev/null +++ b/EcosiaTests/Core/LanguageTests.swift @@ -0,0 +1,19 @@ +@testable import Ecosia +import XCTest + +final class LanguageTests: XCTestCase { + func testSettingLanguageCurrentToUnexpectedLanguageReturnsDefault() { + Language.current = Language.make(for: Locale(identifier: "nz")) + XCTAssertEqual(.en, Language.current) + } + + func testMake() { + XCTAssertEqual(.en, Language.make(for: .init(identifier: "en-DE"))) + XCTAssertEqual(.de, Language.make(for: .init(identifier: "de-MX"))) + XCTAssertEqual(.es, Language.make(for: .init(identifier: "es-ES"))) + XCTAssertEqual(.es, Language.make(for: .init(identifier: "es-MX"))) + XCTAssertEqual(.en, Language.make(for: .init(identifier: "en-US"))) + XCTAssertEqual(.es, Language.make(for: .init(identifier: "es-US"))) + XCTAssertEqual(.en, Language.make(for: .init(identifier: "Invalid"))) + } +} diff --git a/EcosiaTests/Core/ListTests.swift b/EcosiaTests/Core/ListTests.swift new file mode 100644 index 000000000000..fb76173e4fef --- /dev/null +++ b/EcosiaTests/Core/ListTests.swift @@ -0,0 +1,23 @@ +@testable import Ecosia +import XCTest + +final class ListTests: XCTestCase { + func testIncomplete() { + let json = """ +[{ + "name": "test", + "id": 1 +}, +{ + "id": 2 +}] +""" + XCTAssertNil(try? JSONDecoder().decode([Model].self, from: .init(json.utf8))) + XCTAssertEqual("test", (try? JSONDecoder().decode(List.self, from: .init(json.utf8)).items.first?.name)) + } +} + +private struct Model: Decodable { + let name: String + let id: Int +} diff --git a/EcosiaTests/Core/LocalTests.swift b/EcosiaTests/Core/LocalTests.swift new file mode 100644 index 000000000000..2d7cf79526a9 --- /dev/null +++ b/EcosiaTests/Core/LocalTests.swift @@ -0,0 +1,30 @@ +@testable import Ecosia +import XCTest + +final class LocalTests: XCTestCase { + func testCurrent() { + Language.current = .en + XCTAssertEqual("en-us", Language.current.locale.rawValue) + } + + func testCountryCode() { + let locale = NSLocale(localeIdentifier: "it-DE") + XCTAssertEqual("DE", locale.countryCode) + XCTAssertEqual(.de_de, Local.make(for: locale as Locale)) + } + + func testRegion() { + XCTAssertEqual(.de_de, Local.make(for: .init(identifier: "en-DE"))) + XCTAssertEqual(.es_mx, Local.make(for: .init(identifier: "de-MX"))) + XCTAssertEqual(.es_es, Local.make(for: .init(identifier: "es-ES"))) + XCTAssertEqual(.es_mx, Local.make(for: .init(identifier: "es-MX"))) + XCTAssertEqual(.en_us, Local.make(for: .init(identifier: "en-US"))) + XCTAssertEqual(.es_us, Local.make(for: .init(identifier: "es-US"))) + XCTAssertEqual(.en_us, Local.make(for: .init(identifier: "Invalid"))) + } + + func testIdentifier() { + XCTAssertEqual("en-us", Local.en_us.rawValue) + XCTAssertEqual("de-de", Local.de_de.rawValue) + } +} diff --git a/EcosiaTests/Core/NewsTests.swift b/EcosiaTests/Core/NewsTests.swift new file mode 100644 index 000000000000..efad6344d2f0 --- /dev/null +++ b/EcosiaTests/Core/NewsTests.swift @@ -0,0 +1,158 @@ +@testable import Ecosia +import XCTest + +final class NewsTests: XCTestCase { + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.news) + try? FileManager.default.removeItem(at: FileManager.user) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.news) + try? FileManager.default.removeItem(at: FileManager.user) + } + + private func mockSavedItems() { + let items = [ + NewsModel(id: 1, text: "", language: .en, publishDate: .distantPast, + imageUrl: URL(string: "https://avocade.com")!, targetUrl: URL(string: "https://avocadoe.com")!, trackingName: ""), + NewsModel(id: 2, text: "hello", language: .en, publishDate: .distantFuture, + imageUrl: URL(string: "https://guacamole.com")!, targetUrl: URL(string: "https://guaca.com")!, trackingName: ""), + NewsModel(id: 3, text: "hello", language: .de, publishDate: .distantFuture, + imageUrl: URL(string: "https://guacamole.com")!, targetUrl: URL(string: "https://guaca.com")!, trackingName: "") + ] + try? JSONEncoder().encode(items).write(to: FileManager.news, options: .atomic) + } + + func testPublishOnMainThread() { + let expect = expectation(description: "") + + mockSavedItems() + let notifications = News() + notifications.subscribe(self) { _ in + XCTAssertEqual(.main, Thread.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLoadFromDisk() { + let expect = expectation(description: "") + mockSavedItems() + + let notifications = News() + notifications.subscribe(self) { + XCTAssertEqual(2, $0.count) + $0.forEach { + XCTAssertEqual(.en, $0.language) + } + XCTAssertGreaterThan($0.first!.publishDate, $0.last!.publishDate) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testAvoidDuplication() { + var set = Set([NewsModel(id: 1, text: "Great headline ", language: .en, publishDate: .distantPast, + imageUrl: URL(string: "https://avocade.com")!, targetUrl: URL(string: "https://avocadoe.com")!, trackingName: "")]) + set.insert(NewsModel(id: 1, text: "hello", language: .de, publishDate: .distantFuture, + imageUrl: URL(string: "https://guacamole.com")!, targetUrl: URL(string: "https://guaca.com")!, trackingName: "")) + XCTAssertEqual(1, set.count) + } + + func testLoadNewForced() { + User.shared.news = Date() + + let expect = expectation(description: "") + let session = MockURLSession() + session.data = [try! .init(contentsOf: Bundle.ecosiaTests.url(forResource: "notifications", withExtension: "json")!)] + + let notifications = News() + notifications.subscribe(self) { + XCTAssertEqual(10, $0.count) + XCTAssertGreaterThan($0.first!.publishDate, $0.last!.publishDate) + + expect.fulfill() + } + notifications.load(session: session) + waitForExpectations(timeout: 1) + } + + func testNeedsUpdateOnEmptyNews() { + let news = News() + XCTAssertTrue(news.needsUpdate) + } + + func testNeedsUpdateAfterLoading() { + let expect = expectation(description: "") + mockSavedItems() + User.shared.news = .distantPast + let news = News() + + news.subscribe(self) { _ in + XCTAssertTrue(news.needsUpdate) + + User.shared.news = Date() + XCTAssertFalse(news.needsUpdate) + + User.shared.news = Date().advanced(by: -25 * 60 * 60) + XCTAssertTrue(news.needsUpdate) + + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testSubscribeAndReceive() { + let expect = expectation(description: "") + let news = News() + + news.subscribeAndReceive(self) { items in + XCTAssert(news.state?.count == items.count) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCallOnFailed() { + let expect = expectation(description: "") + expect.isInverted = true // we expect no callback + let session = MockURLSession() + let notifications = News() + notifications.subscribe(self) { _ in + expect.fulfill() + } + notifications.load(session: session) + waitForExpectations(timeout: 1) + } + + func testCleanTextFromBundle() { + let expect = expectation(description: "") + mockSavedItems() + let notifications = News() + notifications.subscribe(self) { + $0.forEach { + XCTAssertFalse($0.text.contains("'")) + XCTAssertFalse($0.text.contains("")) + } + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCleanTextFromNetwork() { + let expect = expectation(description: "") + let session = MockURLSession() + session.data = [try! .init(contentsOf: Bundle.ecosiaTests.url(forResource: "notifications", withExtension: "json")!)] + let notifications = News() + notifications.subscribe(self) { + $0.forEach { + XCTAssertFalse($0.text.contains("'")) + XCTAssertFalse($0.text.contains("")) + } + expect.fulfill() + } + notifications.load(session: session) + waitForExpectations(timeout: 1) + } +} diff --git a/EcosiaTests/Core/PublishersTests.swift b/EcosiaTests/Core/PublishersTests.swift new file mode 100644 index 000000000000..594d904c7959 --- /dev/null +++ b/EcosiaTests/Core/PublishersTests.swift @@ -0,0 +1,59 @@ +@testable import Ecosia +import XCTest + +final class PublishersTests: XCTestCase { + func testNotifySubscriber() { + let publisher = ExamplePublisher() + let subscriber = ExampleSubscriber(expectation(description: ""), publisher: publisher) + subscriber.shouldReceive = ["hello", "world"] + publisher.eventHappened(["hello", "world"]) + waitForExpectations(timeout: 1) + } + + func testNotRetainingSubscriber() { + let publisher = ExamplePublisher() + var subscriber: ExampleSubscriber? = ExampleSubscriber(expectation(description: ""), publisher: publisher) + subscriber!.shouldReceive = ["hello", "world"] + publisher.eventHappened(["hello", "world"]) + subscriber = nil + publisher.eventHappened(["hello", "world"]) + waitForExpectations(timeout: 1) { _ in + XCTAssertTrue(publisher.subscriptions.isEmpty) + } + } + + func testUnsubscribe() { + let publisher = ExamplePublisher() + publisher.subscribe(self) { _ in } + publisher.unsubscribe(self) + XCTAssertTrue(publisher.subscriptions.isEmpty) + } + + func testSubscribeMultipleTimes() { + let publisher = ExamplePublisher() + publisher.subscribe(self) { _ in } + publisher.subscribe(self) { _ in } + XCTAssertEqual(1, publisher.subscriptions.count) + } +} + +private final class ExamplePublisher: Publisher { + var subscriptions = [Subscription<[String]>]() + + func eventHappened(_ messages: [String]) { + send(messages) + } +} + +private final class ExampleSubscriber { + var shouldReceive = [String]() + private let expect: XCTestExpectation + + init(_ expect: XCTestExpectation, publisher: ExamplePublisher) { + self.expect = expect + publisher.subscribe(self) { [weak self] in + XCTAssertEqual(self?.shouldReceive, $0) + self?.expect.fulfill() + } + } +} diff --git a/EcosiaTests/Core/ReferralsModelTests.swift b/EcosiaTests/Core/ReferralsModelTests.swift new file mode 100644 index 000000000000..ff6027f99f5e --- /dev/null +++ b/EcosiaTests/Core/ReferralsModelTests.swift @@ -0,0 +1,56 @@ +import XCTest +@testable import Ecosia + +final class ReferralsModelTests: XCTestCase { + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + func testInitWithCode() { + let expect = expectation(description: "") + User.shared.referrals = .init(code: "avocado1234") + User.queue.async { + let user = User() + XCTAssertEqual(0, user.referrals.claims) + XCTAssertEqual("avocado1234", user.referrals.code) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testUpdateReferred() { + let expect = expectation(description: "") + User.shared.referrals = .init(code: "avocado12") + User.queue.async { + User.shared.referrals.claims += 1 + User.queue.async { + let user = User() + XCTAssertEqual(1, user.referrals.claims) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } + + func testCount() { + var referrals = Referrals.Model() + XCTAssertEqual(0, referrals.count) + referrals.claims = 1 + XCTAssertEqual(1, referrals.count) + referrals.isClaimed = true + XCTAssertEqual(2, referrals.count) + } + + func testClaims() { + var referrals = Referrals.Model() + XCTAssertEqual(0, referrals.newClaims) + referrals.claims = 2 + XCTAssertEqual(2, referrals.newClaims) + referrals.accept() + XCTAssertEqual(0, referrals.newClaims) + } +} diff --git a/EcosiaTests/Core/ReferralsTests.swift b/EcosiaTests/Core/ReferralsTests.swift new file mode 100644 index 000000000000..47032b1e464c --- /dev/null +++ b/EcosiaTests/Core/ReferralsTests.swift @@ -0,0 +1,176 @@ +import XCTest +@testable import Ecosia + +final class ReferralsTests: XCTestCase { + + var httpClientMock: HTTPClientMock! + var referrals: Referrals! + let mockURL = URL(string: "https://www.example.com")! + let okResponse = HTTPURLResponse(url: URL(string: "https://example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil) + let createdResponse = HTTPURLResponse(url: URL(string: "https://www.example.com")!, statusCode: 201, httpVersion: nil, headerFields: nil) + let failureResponse = HTTPURLResponse(url: URL(string: "https://www.example.com")!, statusCode: 500, httpVersion: nil, headerFields: nil) + let notFoundResponse = HTTPURLResponse(url: URL(string: "https://www.example.com")!, statusCode: 404, httpVersion: nil, headerFields: nil) + + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.user) + + httpClientMock = HTTPClientMock() + httpClientMock.data = try! Data(contentsOf: Bundle.ecosiaTests.url(forResource: "referrals", withExtension: "json")!) + httpClientMock.response = failureResponse + + // Force clean state + var user = User() + user.referrals = .init() + User.shared = user + + referrals = Referrals(client: httpClientMock) + } + + func testFetchCodeNotCreated() async throws { + XCTAssertNil(User.shared.referrals.code) + httpClientMock.response = okResponse + + try await referrals.refresh(createCode: false) + + XCTAssertNil(User.shared.referrals.code) + } + + func testFetchCodeCreate() async throws { + XCTAssertNil(User.shared.referrals.code) + httpClientMock.response = okResponse + + let expect = expectation(description: "") + referrals.subscribe(self) { model in + self.referrals.unsubscribe(self) + XCTAssertEqual(model.code, "MANGO-2UGicG") + XCTAssertEqual(User.shared.referrals.code, "MANGO-2UGicG") + XCTAssertEqual(model.claims, 1) + expect.fulfill() + } + try await referrals.refresh(createCode: true) + await fulfillment(of: [expect], timeout: 1) + } + + func testFetchCodeWithCodeInPlace() async throws { + User.shared.referrals.code = "Avocado" + + let codeInfo = try await referrals.fetchCode() + + XCTAssertEqual(codeInfo.code, "Avocado") + } + + func testRefreshHandlesNotFound() async throws { + User.shared.referrals.code = "MANGO-2UGicG" + XCTAssertEqual(User.shared.referrals.claims, 0) + httpClientMock.response = notFoundResponse + httpClientMock.executeBeforeResponse = { + XCTAssertEqual(self.httpClientMock.response, self.notFoundResponse) + } + try await referrals.refresh() + } + + func testRefreshHandlesNotFound_CreatesNewCode() async throws { + User.shared.referrals.code = "MANGO-2UGicG" + XCTAssertEqual(User.shared.referrals.claims, 0) + httpClientMock.response = notFoundResponse + httpClientMock.data = """ + { + "code": "NEW-CODE", + "claims": 0 + } + """.data(using: .utf8)! + try await referrals.refresh() + XCTAssertEqual(User.shared.referrals.code, "NEW-CODE") + XCTAssertEqual(User.shared.referrals.claims, 0) + } + + func testRefresh() async throws { + User.shared.referrals.code = "MANGO-2UGicG" + XCTAssertEqual(User.shared.referrals.claims, 0) + httpClientMock.response = okResponse + + let expect = expectation(description: "") + referrals.subscribe(self) { model in + self.referrals.unsubscribe(self) + XCTAssertEqual(model.code, User.shared.referrals.code) + XCTAssertEqual(User.shared.referrals.claims, 1) + expect.fulfill() + } + try await referrals.refresh() + await fulfillment(of: [expect], timeout: 1) + } + + func testClaim() async throws { + User.shared.referrals.code = "MANGO-2UGicG" + XCTAssertFalse(User.shared.referrals.isClaimed) + XCTAssertFalse(User.shared.referrals.isNewClaim) + httpClientMock.response = createdResponse + + try await referrals.claim(referrer: "MANGO-1XrUBl") + + XCTAssertTrue(User.shared.referrals.isClaimed) + XCTAssertTrue(User.shared.referrals.isNewClaim) + } + + func testFetchCodeBeforeClaim() async throws { + XCTAssertNil(User.shared.referrals.code) + httpClientMock.response = createdResponse + + try await referrals.claim(referrer: "MANGO-1XrUBl") + + XCTAssertEqual(User.shared.referrals.code, "MANGO-2UGicG") + XCTAssertTrue(User.shared.referrals.isClaimed) + XCTAssertTrue(User.shared.referrals.isNewClaim) + } + + func testCooldown() async throws { + let lastUpdate = User.shared.referrals.updated + httpClientMock.response = okResponse + + try await referrals.refresh(createCode: true) + let recentUpdate = User.shared.referrals.updated + XCTAssert(recentUpdate > lastUpdate) + + // run into cooldown + try await self.referrals.refresh(force: false) + XCTAssert(User.shared.referrals.updated == recentUpdate) + + // update with force + try await self.referrals.refresh(force: true) + XCTAssert(User.shared.referrals.updated > recentUpdate) + } + + func testRefreshCooldownOnError() async throws { + User.shared.referrals.code = "MANGO-2UGicG" + httpClientMock.response = failureResponse + let lastUpdate = User.shared.referrals.updated + + try? await referrals.refresh(force: false) + + XCTAssert(User.shared.referrals.updated > lastUpdate) + } + + func testIsRefreshingTrueWhileRefreshing() async throws { + User.shared.referrals.code = "MANGO-2UGicG" + XCTAssertEqual(User.shared.referrals.claims, 0) + httpClientMock.response = okResponse + referrals.isRefreshing = false + + httpClientMock.executeBeforeResponse = { + XCTAssertTrue(self.referrals.isRefreshing) + } + try await referrals.refresh() + + XCTAssertFalse(referrals.isRefreshing) + } + + func testRefreshCodeIsNotCalledWhenRefreshing() async throws { + User.shared.referrals.code = "MANGO-2UGicG" + XCTAssertEqual(User.shared.referrals.claims, 0) + // Set failure so that it throws if unwanted request is made + httpClientMock.response = failureResponse + + referrals.isRefreshing = true + try await referrals.refresh() + } +} diff --git a/EcosiaTests/Core/Resources/Bookmarks/export_bookmark_ecosia.html b/EcosiaTests/Core/Resources/Bookmarks/export_bookmark_ecosia.html new file mode 100644 index 000000000000..d75531e77f53 --- /dev/null +++ b/EcosiaTests/Core/Resources/Bookmarks/export_bookmark_ecosia.html @@ -0,0 +1,24 @@ + + + Bookmarks +

Bookmarks

+

+

Favorites

+

+

One +
Two +

My Folder #1

+

+

Three +

+

My Folder #2

+

+

Four +

My Subfolder #1

+

+

Five +

+

+

+

+ diff --git a/EcosiaTests/Core/Resources/Bookmarks/import_input_bookmark_chrome.html b/EcosiaTests/Core/Resources/Bookmarks/import_input_bookmark_chrome.html new file mode 100644 index 000000000000..cc5083890890 --- /dev/null +++ b/EcosiaTests/Core/Resources/Bookmarks/import_input_bookmark_chrome.html @@ -0,0 +1,21 @@ + + + +Bookmarks +

Bookmarks

+

+

Lesezeichenleiste

+

+

ecosia/ios-browser: The iOS Browser that plants trees 🌱 +
Bookmark import/export – Figma +

Ecosia

+

+

Test1

+

+

test +

+

+

+

diff --git a/EcosiaTests/Core/Resources/Bookmarks/import_input_bookmark_firefox.html b/EcosiaTests/Core/Resources/Bookmarks/import_input_bookmark_firefox.html new file mode 100644 index 000000000000..19b069fdf708 --- /dev/null +++ b/EcosiaTests/Core/Resources/Bookmarks/import_input_bookmark_firefox.html @@ -0,0 +1,20 @@ + + + + +Bookmarks +

Lesezeichen-Menü

+ +

+

Mozilla Firefox

+

+

Hilfe erhalten +
Firefox anpassen +
Machen Sie mit +
Ãœber uns +
Hilfe und Anleitungen +

+

diff --git a/EcosiaTests/Core/Resources/Bookmarks/import_input_bookmark_safari.html b/EcosiaTests/Core/Resources/Bookmarks/import_input_bookmark_safari.html new file mode 100644 index 000000000000..4d9cc72f0efd --- /dev/null +++ b/EcosiaTests/Core/Resources/Bookmarks/import_input_bookmark_safari.html @@ -0,0 +1,38 @@ + + + + Lesezeichen +

Lesezeichen

+

Favoriten

+

+

Home

+

+

Pi-hole - pihole +
Free dynamic DNS for IPv6 +
Munzinger Online – Startseite +
GENIOS +

+

Apple

+

+

M1

+

+

[Fix] Issue while installing ruby with rbenv in M1 Mac - Prabin Poudel - Rails & Web App Developer | Freelancer +
Is Apple silicon ready? +
Ãœbertragen von Dateien zwischen einem Mac mit Apple Chips und einem anderen Mac - Apple Support +

+

Home: Apple Support Communities +

+

Development

+

+

Kubernetes

+

+

Kubernetes Ingress using NGINX | Nic Williams +
timescale/tobs: tobs - The Observability Stack for Kubernetes. Easy install of a full observability stack into a k8s cluster with a CLI tool or Helm charts. +
Upgrading kubeadm clusters | Kubernetes +
How to Set Up an Nginx Ingress with Cert-Manager on DigitalOcean Kubernetes | DigitalOcean +
Kubernetes on Hetzner with Kubermatic KubeOne in 2021 +
Kubernetes and RBAC: Restrict user access to one namespace · Jeremie Vallee +

+

+

restic · Backups done right! + diff --git a/EcosiaTests/Core/Resources/Bookmarks/import_output_bookmark_chrome.txt b/EcosiaTests/Core/Resources/Bookmarks/import_output_bookmark_chrome.txt new file mode 100644 index 000000000000..fbd7f30292c1 --- /dev/null +++ b/EcosiaTests/Core/Resources/Bookmarks/import_output_bookmark_chrome.txt @@ -0,0 +1 @@ +[Ecosia.BookmarkItem.folder("Lesezeichenleiste", [Ecosia.BookmarkItem.bookmark("ecosia/ios-browser: The iOS Browser that plants trees 🌱", "https://github.com/ecosia/ios-browser", Ecosia.BookmarkMetadata(addedAt: Optional(2023-03-15 08:44:44 +0000), modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("Bookmark import/export – Figma", "https://www.figma.com/file/uy3Bb5k5b3JVGvKi49P7RT/Bookmark-import%2Fexport?node-id=2%3A115787&t=zansqVg8w5nbf5vD-0", Ecosia.BookmarkMetadata(addedAt: Optional(2023-03-15 12:28:09 +0000), modifiedAt: nil)), Ecosia.BookmarkItem.folder("Ecosia", [Ecosia.BookmarkItem.folder("Test1", [Ecosia.BookmarkItem.bookmark("test", "https://test.tst/", Ecosia.BookmarkMetadata(addedAt: Optional(2023-03-15 12:34:43 +0000), modifiedAt: nil))], Ecosia.BookmarkMetadata(addedAt: Optional(2023-03-15 12:34:32 +0000), modifiedAt: Optional(2023-03-15 12:34:43 +0000)))], Ecosia.BookmarkMetadata(addedAt: Optional(2023-03-15 12:33:18 +0000), modifiedAt: Optional(2023-03-15 12:34:32 +0000)))], Ecosia.BookmarkMetadata(addedAt: Optional(2023-03-09 14:18:52 +0000), modifiedAt: Optional(2023-03-15 12:33:18 +0000)))] diff --git a/EcosiaTests/Core/Resources/Bookmarks/import_output_bookmark_firefox.txt b/EcosiaTests/Core/Resources/Bookmarks/import_output_bookmark_firefox.txt new file mode 100644 index 000000000000..8c596813d3ba --- /dev/null +++ b/EcosiaTests/Core/Resources/Bookmarks/import_output_bookmark_firefox.txt @@ -0,0 +1 @@ +[Ecosia.BookmarkItem.folder("Mozilla Firefox", [Ecosia.BookmarkItem.bookmark("Hilfe erhalten", "https://support.mozilla.org/de/products/firefox", Ecosia.BookmarkMetadata(addedAt: Optional(2021-12-02 10:41:13 +0000), modifiedAt: Optional(2021-12-02 10:41:13 +0000))), Ecosia.BookmarkItem.bookmark("Firefox anpassen", "https://support.mozilla.org/de/kb/customize-firefox-controls-buttons-and-toolbars?utm_source=firefox-browser&utm_medium=default-bookmarks&utm_campaign=customize", Ecosia.BookmarkMetadata(addedAt: Optional(2020-05-31 09:45:59 +0000), modifiedAt: Optional(2021-12-11 10:25:06 +0000))), Ecosia.BookmarkItem.bookmark("Machen Sie mit", "https://www.mozilla.org/de/contribute/", Ecosia.BookmarkMetadata(addedAt: Optional(2020-05-31 09:45:59 +0000), modifiedAt: Optional(2021-12-11 10:25:06 +0000))), Ecosia.BookmarkItem.bookmark("Über uns", "https://www.mozilla.org/de/about/", Ecosia.BookmarkMetadata(addedAt: Optional(2020-05-31 09:45:59 +0000), modifiedAt: Optional(2021-12-11 10:25:06 +0000))), Ecosia.BookmarkItem.bookmark("Hilfe und Anleitungen", "https://support.mozilla.org/de/products/firefox", Ecosia.BookmarkMetadata(addedAt: Optional(2020-05-31 09:45:59 +0000), modifiedAt: Optional(2021-12-09 17:13:46 +0000)))], Ecosia.BookmarkMetadata(addedAt: Optional(2020-05-31 09:45:59 +0000), modifiedAt: Optional(2021-12-11 10:25:06 +0000)))] diff --git a/EcosiaTests/Core/Resources/Bookmarks/import_output_bookmark_safari.txt b/EcosiaTests/Core/Resources/Bookmarks/import_output_bookmark_safari.txt new file mode 100644 index 000000000000..915b3b849f6e --- /dev/null +++ b/EcosiaTests/Core/Resources/Bookmarks/import_output_bookmark_safari.txt @@ -0,0 +1 @@ +[Ecosia.BookmarkItem.folder("Favoriten", [Ecosia.BookmarkItem.folder("Home", [Ecosia.BookmarkItem.bookmark("Pi-hole - pihole", "http://pihole.lan/admin/", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("Free dynamic DNS for IPv6", "https://dynv6.com/", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("Munzinger Online – Startseite", "https://online.munzinger.de/", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("GENIOS", "https://bib-voebb.genios.de/", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil))], Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.folder("Apple", [Ecosia.BookmarkItem.folder("M1", [Ecosia.BookmarkItem.bookmark("[Fix] Issue while installing ruby with rbenv in M1 Mac - Prabin Poudel - Rails & Web App Developer | Freelancer", "https://prabinpoudel.com.np/articles/fix-issue-while-installing-ruby-with-rbenv-in-m1-mac/", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("Is Apple silicon ready?", "https://isapplesiliconready.com/", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("Übertragen von Dateien zwischen einem Mac mit Apple Chips und einem anderen Mac - Apple Support", "https://support.apple.com/de-de/guide/mac-help/mchlb37e8ca7/12.0/mac/12.0", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil))], Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("Home: Apple Support Communities", "https://discussions.apple.com/index.jspa", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil))], Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.folder("Development", [Ecosia.BookmarkItem.folder("Kubernetes", [Ecosia.BookmarkItem.bookmark("Kubernetes Ingress using NGINX | Nic Williams", "https://www.tiredpixel.com/2021/12/06/kubernetes-ingress-using-nginx/", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("timescale/tobs: tobs - The Observability Stack for Kubernetes. Easy install of a full observability stack into a k8s cluster with a CLI tool or Helm charts.", "https://github.com/timescale/tobs", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("Upgrading kubeadm clusters | Kubernetes", "https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("How to Set Up an Nginx Ingress with Cert-Manager on DigitalOcean Kubernetes | DigitalOcean", "https://www.digitalocean.com/community/tutorials/how-to-set-up-an-nginx-ingress-with-cert-manager-on-digitalocean-kubernetes", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("Kubernetes on Hetzner with Kubermatic KubeOne in 2021", "https://www.kubermatic.com/blog/kubernetes-on-hetzner-with-kubermatic-kubeone-in-2021/", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("Kubernetes and RBAC: Restrict user access to one namespace · Jeremie Vallee", "https://jeremievallee.com/2018/05/28/kubernetes-rbac-namespace-user.html", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil))], Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil))], Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil)), Ecosia.BookmarkItem.bookmark("restic · Backups done right!", "https://restic.net/", Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil))], Ecosia.BookmarkMetadata(addedAt: nil, modifiedAt: nil))] diff --git a/EcosiaTests/Core/Resources/notifications.json b/EcosiaTests/Core/Resources/notifications.json new file mode 100644 index 000000000000..e882a339af9b --- /dev/null +++ b/EcosiaTests/Core/Resources/notifications.json @@ -0,0 +1 @@ +[{"id":357,"text":"Nouveau bilan des plantations tout droit du Brésil ! À visionner ici.","tracking_name":"TU22_FR","publish_date":"2020-02-03T12:44:18Z","target_url":"https://youtu.be/MpAWfmOkuWg","image_url":"https://s3.amazonaws.com/static.ecosia.org/images/TreeUpdate_ThumbnailBaner_x6PYaqr.jpg","language":"fr","level":3},{"id":356,"text":"Neues Baumpflanz-Update aus Brasilien! Für Video hier klicken","tracking_name":"TU22_DE","publish_date":"2020-02-03T12:42:49Z","target_url":"https://youtu.be/MpAWfmOkuWg","image_url":"https://s3.amazonaws.com/static.ecosia.org/images/TreeUpdate_ThumbnailBaner_girJyIZ.jpg","language":"de","level":3},{"id":355,"text":"New tree-planting update from Brazil! Watch here","tracking_name":"TU22_EN","publish_date":"2020-02-03T12:42:17Z","target_url":"https://youtu.be/MpAWfmOkuWg","image_url":"https://s3.amazonaws.com/static.ecosia.org/images/TreeUpdate_ThumbnailBaner.jpg","language":"en","level":3},{"id":347,"text":"Tutte le ricerche effettuate giovedì contribuiranno a piantare alberi in Australia! Per saperne di più","tracking_name":"IT_australia1","publish_date":"2020-01-20T16:03:10Z","target_url":"https://blog.ecosia.org/trees-for-australia/","image_url":"https://s3.amazonaws.com/static.ecosia.org/images/ausnotif_Cl5D6vA.jpg","language":"it","level":3},{"id":346,"text":"Alla sökningar som görs pÃ¥ torsdag planterar träd i Australien! Läs mer","tracking_name":"SW_australia1","publish_date":"2020-01-20T16:02:32Z","target_url":"https://blog.ecosia.org/trees-for-australia/","image_url":"https://s3.amazonaws.com/static.ecosia.org/images/ausnotif_YjEUtXg.jpg","language":"sv","level":3},{"id":345,"text":"Este jueves, ¡todas tus búsquedas ayudarán a plantar árboles en Australia!","tracking_name":"ES_australia1","publish_date":"2020-01-20T16:01:40Z","target_url":"https://blog.ecosia.org/trees-for-australia/","image_url":"https://s3.amazonaws.com/static.ecosia.org/images/ausnotif_smHbNe4.jpg","language":"es","level":3},{"id":344,"text":"Alle zoekopdrachten op donderdag zullen ervoor zorgen dat er bomen geplant worden in Australië! Meer informatie","tracking_name":"NL_australia1","publish_date":"2020-01-20T16:00:41Z","target_url":"https://blog.ecosia.org/trees-for-australia/","image_url":"https://s3.amazonaws.com/static.ecosia.org/images/ausnotif_avSY0Ke.jpg","language":"nl","level":3},{"id":360,"text":"L’importance des forêts et 3 manières de les protéger : découvrez notre vidéo.","tracking_name":"FR_IntlForestDay","publish_date":"2020-03-20T16:31:58Z","target_url":"https://youtu.be/_dWJVHIE9S8","image_url":"https://s3.amazonaws.com/static.ecosia.org/images/Notification_jsl5H96.jpg","language":"fr","level":3},{"id":359,"text":"Die Bedeutung von Wäldern und drei Wege, sie zu schützen: Video anschauen","tracking_name":"DE_IntlForestDay","publish_date":"2020-03-20T14:12:24Z","target_url":"https://youtu.be/_dWJVHIE9S8","image_url":"https://s3.amazonaws.com/static.ecosia.org/images/Notification_QVLTM7l.jpg","language":"de","level":3},{"id":358,"text":"The ' importance of forests and three ways you can protect them: watch video","tracking_name":"EN_IntlForestDay","publish_date":"2020-03-20T14:12:15Z","target_url":"https://youtu.be/_dWJVHIE9S8","image_url":"https://s3.amazonaws.com/static.ecosia.org/images/Notification_Rt0jeAh.jpg","language":"en","level":3}] diff --git a/EcosiaTests/Core/Resources/referrals.json b/EcosiaTests/Core/Resources/referrals.json new file mode 100644 index 000000000000..075c534b6aa7 --- /dev/null +++ b/EcosiaTests/Core/Resources/referrals.json @@ -0,0 +1,4 @@ +{ + "code": "MANGO-2UGicG", + "claims_count": 1 +} diff --git a/EcosiaTests/Core/SearchesCounterTests.swift b/EcosiaTests/Core/SearchesCounterTests.swift new file mode 100644 index 000000000000..dac06be42a10 --- /dev/null +++ b/EcosiaTests/Core/SearchesCounterTests.swift @@ -0,0 +1,37 @@ +@testable import Ecosia +import XCTest + +final class SearchesCounterTests: XCTestCase { + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + func testSubscribeAndReceive() { + let expect = expectation(description: "") + let counter = SearchesCounter() + + counter.subscribeAndReceive(self) { items in + XCTAssertEqual(counter.state, User.shared.searchCount) + counter.unsubscribe(self) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testSubscribe() { + let expect = expectation(description: "") + let counter = SearchesCounter() + + counter.subscribe(self) { items in + XCTAssertEqual(counter.state, 2) + counter.unsubscribe(self) + expect.fulfill() + } + User.shared.searchCount = 2 + waitForExpectations(timeout: 1) + } +} diff --git a/EcosiaTests/Core/SingularAdNetworkHelperTests.swift b/EcosiaTests/Core/SingularAdNetworkHelperTests.swift new file mode 100644 index 000000000000..5051e50f6d09 --- /dev/null +++ b/EcosiaTests/Core/SingularAdNetworkHelperTests.swift @@ -0,0 +1,325 @@ +import XCTest +@testable import Ecosia + +#if os(iOS) + +class SingularAdNetworkHelperTests: XCTestCase { + let mockAppDeviceInfo = AppDeviceInfo(platform: "test", + bundleId: "com.test", + osVersion: "16.1", + deviceManufacturer: "d", + deviceModel: "e", + locale: "f", + appVersion: "0.0", + installReceipt: "h") + let mockAppDeviceInfoParameters = [ + "p": "test", + "i": "com.test", + "ve": "16.1", + "app_v": "0.0", + ] + var objectPersister: MockObjectPersister! + var timestampProvider: MockTimestampProvider! + private var singularService: MockSingularService! + var helper: SingularAdNetworkHelper! + + override func setUpWithError() throws { + objectPersister = MockObjectPersister() + timestampProvider = MockTimestampProvider(currentTimestamp: 1234567890) + singularService = MockSingularService() + helper = SingularAdNetworkHelper(skan: MockSkan.self, + objectPersister: objectPersister, + timestampProvider: timestampProvider, + singularService: singularService) + MockSkan.shouldThrowError = false + } + + override func tearDownWithError() throws { + objectPersister = nil + timestampProvider = nil + singularService = nil + helper = nil + } + + // MARK: Register for ad network + func testRegisterForAdNetwork() async throws { + try await helper.registerAppForAdNetworkAttribution() + + XCTAssertEqual(MockSkan.conversionValue, 0) + XCTAssertEqual(objectPersister.getValueFor(.firstSkanCallTimestamp) as? Int, 1234567890) + XCTAssertEqual(objectPersister.getValueFor(.lastSkanCallTimestamp) as? Int, 1234567890) + XCTAssertEqual(objectPersister.getValueFor(.conversionValue) as? Int, 0) + + XCTAssertNil(objectPersister.getValueFor(.previousFineValue)) + XCTAssertNil(objectPersister.getValueFor(.coarseValue(window: .first))) + XCTAssertNil(objectPersister.getValueFor(.previousCoarseValue(window: .first))) + XCTAssertNil(objectPersister.getValueFor(.windowLockTimestamp(window: .first))) + } + + func testDoesNotRegisterForAdNetworkGivenAleradyDonePreviously() async throws { + objectPersister.setValueFor(.firstSkanCallTimestamp, value: 1) + XCTAssertNil(MockSkan.conversionValue) + XCTAssertNil(objectPersister.getValueFor(.lastSkanCallTimestamp)) + XCTAssertNil(objectPersister.getValueFor(.conversionValue)) + + try await helper.registerAppForAdNetworkAttribution() + + XCTAssertNil(MockSkan.conversionValue) + XCTAssertNil(objectPersister.getValueFor(.lastSkanCallTimestamp)) + XCTAssertNil(objectPersister.getValueFor(.conversionValue)) + XCTAssertNil(objectPersister.getValueFor(.previousFineValue)) + XCTAssertNil(objectPersister.getValueFor(.coarseValue(window: .first))) + XCTAssertNil(objectPersister.getValueFor(.previousCoarseValue(window: .first))) + XCTAssertNil(objectPersister.getValueFor(.windowLockTimestamp(window: .first))) + } + + func testRegisterForAdNetworkHandlesError() async throws { + MockSkan.shouldThrowError = true + + try await helper.registerAppForAdNetworkAttribution() + + XCTAssertEqual(objectPersister.getValueFor(.errorCode) as? Int, 1234) + } + + // MARK: Get persisted values + func testGetPersistedValuesDictionary() { + objectPersister.setValueFor(.firstSkanCallTimestamp, value: 123) + objectPersister.setValueFor(.lastSkanCallTimestamp, value: 456) + objectPersister.setValueFor(.conversionValue, value: 2) + objectPersister.setValueFor(.coarseValue(window: .first), value: 3) + objectPersister.setValueFor(.coarseValue(window: .second), value: 4) + objectPersister.setValueFor(.coarseValue(window: .third), value: 5) + objectPersister.setValueFor(.previousFineValue, value: 6) + objectPersister.setValueFor(.previousCoarseValue(window: .first), value: 7) + objectPersister.setValueFor(.previousCoarseValue(window: .second), value: 8) + objectPersister.setValueFor(.previousCoarseValue(window: .third), value: 9) + objectPersister.setValueFor(.windowLockTimestamp(window: .first), value: 10) + objectPersister.setValueFor(.windowLockTimestamp(window: .second), value: 11) + objectPersister.setValueFor(.windowLockTimestamp(window: .third), value: 12) + objectPersister.setValueFor(.errorCode, value: 1234) + + let dictionary = helper.persistedValuesDictionary + + XCTAssertEqual(dictionary["skan_first_call_to_skadnetwork_timestamp"], "123") + XCTAssertEqual(dictionary["skan_last_call_to_skadnetwork_timestamp"], "456") + XCTAssertEqual(dictionary["skan_current_conversion_value"], "2") + XCTAssertEqual(dictionary["p0_coarse"], "3") + XCTAssertEqual(dictionary["p1_coarse"], "4") + XCTAssertEqual(dictionary["p2_coarse"], "5") + XCTAssertEqual(dictionary["prev_fine_value"], "6") + XCTAssertEqual(dictionary["p0_prev_coarse_value"], "7") + XCTAssertEqual(dictionary["p1_prev_coarse_value"], "8") + XCTAssertEqual(dictionary["p2_prev_coarse_value"], "9") + XCTAssertEqual(dictionary["p0_window_lock"], "10") + XCTAssertEqual(dictionary["p1_window_lock"], "11") + XCTAssertEqual(dictionary["p2_window_lock"], "12") + XCTAssertEqual(dictionary["_skerror"], "1234") + } + + // MARK: Fetch and update from server + func testUpdateFetchedConversionValuesFromServerOnFirstWindow() async throws { + objectPersister.setValueFor(.firstSkanCallTimestamp, value: 1234567880) + objectPersister.setValueFor(.lastSkanCallTimestamp, value: 123) + objectPersister.setValueFor(.conversionValue, value: 2) + objectPersister.setValueFor(.coarseValue(window: .first), value: 3) + objectPersister.setValueFor(.previousFineValue, value: 6) + objectPersister.setValueFor(.previousCoarseValue(window: .first), value: 7) + objectPersister.setValueFor(.windowLockTimestamp(window: .first), value: 10) + singularService.responseStub = SingularConversionValueResponse(conversionValue: 50, + coarseValue: nil, + lockWindow: true) + + try await helper.fetchFromSingularServerAndUpdate(sessionIdentifier: "123", appDeviceInfo: mockAppDeviceInfo) + + XCTAssertEqual(singularService.receivedParameters, [ + "n": "__SESSION__", + "sing": "123", + "p0_coarse": "3", + "p0_prev_coarse_value": "7", + "p0_window_lock": "10", + "prev_fine_value": "6", + "skan_current_conversion_value": "2", + "skan_first_call_to_skadnetwork_timestamp": "1234567880", + "skan_last_call_to_skadnetwork_timestamp": "123", + ].merging(mockAppDeviceInfoParameters) { (current, _) in current }) + XCTAssertEqual(MockSkan.conversionValue, 50) + XCTAssertNil(MockSkan.coarseValue) + XCTAssertEqual(MockSkan.lockWindow, true) + XCTAssertEqual(objectPersister.getValueFor(.conversionValue) as? Int, 50) + XCTAssertEqual(objectPersister.getValueFor(.previousFineValue) as? Int, 2) + XCTAssertNil(objectPersister.getValueFor(.coarseValue(window: .first))) + XCTAssertEqual(objectPersister.getValueFor(.previousCoarseValue(window: .first)) as? Int, 3) + XCTAssertEqual(objectPersister.getValueFor(.windowLockTimestamp(window: .first)) as? Int, 1234567890) + } + + func testUpdateFetchedConversionValuesFromServerOnSecondWindow() async throws { + objectPersister.setValueFor(.firstSkanCallTimestamp, value: 1234387890) + objectPersister.setValueFor(.lastSkanCallTimestamp, value: 456) + objectPersister.setValueFor(.conversionValue, value: 7) + objectPersister.setValueFor(.coarseValue(window: .second), value: 5) + objectPersister.setValueFor(.previousCoarseValue(window: .second), value: 8) + objectPersister.setValueFor(.windowLockTimestamp(window: .second), value: 15) + singularService.responseStub = SingularConversionValueResponse(conversionValue: 30, + coarseValue: 2, + lockWindow: nil) + + try await helper.fetchFromSingularServerAndUpdate(sessionIdentifier: "123", appDeviceInfo: mockAppDeviceInfo) + + XCTAssertEqual(singularService.receivedParameters, [ + "n": "__SESSION__", + "sing": "123", + "p1_coarse": "5", + "p1_prev_coarse_value": "8", + "p1_window_lock": "15", + "skan_current_conversion_value": "7", + "skan_first_call_to_skadnetwork_timestamp": "1234387890", + "skan_last_call_to_skadnetwork_timestamp": "456", + ].merging(mockAppDeviceInfoParameters) { (current, _) in current }) + XCTAssertEqual(MockSkan.conversionValue, 30) + XCTAssertEqual(MockSkan.coarseValue, 2) + XCTAssertEqual(MockSkan.lockWindow, false) + XCTAssertEqual(objectPersister.getValueFor(.coarseValue(window: .second)) as? Int, 2) + XCTAssertEqual(objectPersister.getValueFor(.previousCoarseValue(window: .second)) as? Int, 5) + XCTAssertEqual(objectPersister.getValueFor(.windowLockTimestamp(window: .second)) as? Int, 15) + } + + func testUpdateFetchedConversionValuesFromServerOnThirdWindow() async throws { + objectPersister.setValueFor(.firstSkanCallTimestamp, value: 1233567890) + objectPersister.setValueFor(.lastSkanCallTimestamp, value: 789) + objectPersister.setValueFor(.conversionValue, value: 11) + objectPersister.setValueFor(.coarseValue(window: .third), value: 20) + objectPersister.setValueFor(.previousCoarseValue(window: .third), value: 23) + objectPersister.setValueFor(.windowLockTimestamp(window: .third), value: 25) + singularService.responseStub = SingularConversionValueResponse(conversionValue: 44, + coarseValue: 0, + lockWindow: false) + + try await helper.fetchFromSingularServerAndUpdate(sessionIdentifier: "123", appDeviceInfo: mockAppDeviceInfo) + + XCTAssertEqual(singularService.receivedParameters, [ + "n": "__SESSION__", + "sing": "123", + "p2_coarse": "20", + "p2_prev_coarse_value": "23", + "p2_window_lock": "25", + "skan_current_conversion_value": "11", + "skan_first_call_to_skadnetwork_timestamp": "1233567890", + "skan_last_call_to_skadnetwork_timestamp": "789", + ].merging(mockAppDeviceInfoParameters) { (current, _) in current }) + XCTAssertEqual(MockSkan.conversionValue, 44) + XCTAssertEqual(MockSkan.coarseValue, 0) + XCTAssertEqual(MockSkan.lockWindow, false) + XCTAssertEqual(objectPersister.getValueFor(.coarseValue(window: .third)) as? Int, 0) + XCTAssertEqual(objectPersister.getValueFor(.previousCoarseValue(window: .third)) as? Int, 20) + XCTAssertEqual(objectPersister.getValueFor(.windowLockTimestamp(window: .third)) as? Int, 25) + } + + func testDoesNotFetchConversionValuesFromServerOnOverWindow() async throws { + objectPersister.setValueFor(.firstSkanCallTimestamp, value: 1231542890) + + try await helper.fetchFromSingularServerAndUpdate(sessionIdentifier: "123", appDeviceInfo: mockAppDeviceInfo) + + XCTAssertNil(singularService.receivedParameters) + XCTAssertNil(MockSkan.conversionValue) + XCTAssertNil(MockSkan.coarseValue) + } + + func testErrorWhenInvalidFetchedConversionValue() async throws { + objectPersister.setValueFor(.firstSkanCallTimestamp, value: 1234567880) + singularService.responseStub = SingularConversionValueResponse(conversionValue: 64, + coarseValue: 1, + lockWindow: false) + + do { + try await helper.fetchFromSingularServerAndUpdate(sessionIdentifier: "123", appDeviceInfo: mockAppDeviceInfo) + XCTFail("Did not throw error when it should") + } catch SingularAdNetworkHelper.Error.invalidConversionValues { + // expected + } catch { + XCTFail("Received unexpected error \(error)") + } + } + + func testErrorWhenInvalidFetchedCoarseValue() async throws { + objectPersister.setValueFor(.firstSkanCallTimestamp, value: 1234567880) + singularService.responseStub = SingularConversionValueResponse(conversionValue: 1, + coarseValue: 3, + lockWindow: false) + + do { + try await helper.fetchFromSingularServerAndUpdate(sessionIdentifier: "123", appDeviceInfo: mockAppDeviceInfo) + XCTFail("Did not throw error when it should") + } catch SingularAdNetworkHelper.Error.invalidConversionValues { + // expected + } catch { + XCTFail("Received unexpected error \(error)") + } + } +} + +// MARK: SKAdNetwork Mock +class MockSkan: SKAdNetworkProtocol { + + static var conversionValue: Int? + static var coarseValue: Int? + static var lockWindow: Bool? + static var shouldThrowError = false + + static func updatePostbackConversionValue(_ conversionValue: Int) async throws { + guard !shouldThrowError else { + throw NSError(domain: "MockSkanError", code: 1234, userInfo: nil) + } + self.conversionValue = conversionValue + } + + static func updatePostbackConversionValue(_ fineValue: Int, coarseValue: Int?, lockWindow: Bool) async throws { + self.conversionValue = fineValue + self.coarseValue = coarseValue + self.lockWindow = lockWindow + } + + // Should never call this method on the latest iOS version + static func registerAppForAdNetworkAttribution() {} + + // Should never call this method on the latest iOS version + static func updateConversionValue(_ conversionValue: Int) {} +} + +// MARK: ObjectPersister Mock +class MockObjectPersister: ObjectPersister { + + var values = [String: Any]() + func setValueFor(_ object: SingularAdNetworkHelper.PersistedObject, value: Any) { + values[object.key] = value + } + func getValueFor(_ object: SingularAdNetworkHelper.PersistedObject) -> Any? { + return values[object.key] + } + + func set(_ value: Any?, forKey key: String) { + values[key] = value + } + + func object(forKey key: String) -> Any? { + return values[key] + } +} + +// MARK: SingularService Mock +private class MockSingularService: SingularServiceProtocol { + var responseStub: SingularConversionValueResponse? + + var receivedParameters: [String: String]? + func getConversionValue(request: SingularConversionValueRequest) async throws -> SingularConversionValueResponse { + receivedParameters = request.queryParameters + return responseStub! + } + + func sendNotification(request: SingularNotificationRequest) async throws { + throw NSError(domain: "SingularAdNetworkHelperTests", + code: 0, + userInfo: [NSLocalizedDescriptionKey: "Should never get called in this context"]) + } +} + +#endif diff --git a/EcosiaTests/Core/SingularServiceTests.swift b/EcosiaTests/Core/SingularServiceTests.swift new file mode 100644 index 000000000000..e09366425c0c --- /dev/null +++ b/EcosiaTests/Core/SingularServiceTests.swift @@ -0,0 +1,200 @@ +@testable import Ecosia +import XCTest + +#if os(iOS) + +class SingularServiceTests: XCTestCase { + + var httpClientMock: HTTPClientMock! + let failureResponseMock = HTTPURLResponse(url: URL(string: "https://www.example.com")!, + statusCode: 404, httpVersion: nil, headerFields: nil) + let successResponseMock = HTTPURLResponse(url: URL(string: "https://www.example.com")!, + statusCode: 200, httpVersion: nil, headerFields: nil) + var sessionInfoRequestMock: SingularSessionInfoSendRequest! + var eventRequestMock: SingularEventRequest! + var conversionValueRequestMock: SingularConversionValueRequest! + var service: SingularService! + + override func setUpWithError() throws { + httpClientMock = HTTPClientMock() + let deviceInfo = AppDeviceInfo(platform: "a", bundleId: "b", osVersion: "c", deviceManufacturer: "d", deviceModel: "e", locale: "f", country: "g", appVersion: "h") + sessionInfoRequestMock = SingularSessionInfoSendRequest(identifier: "123", info: deviceInfo, skanParameters: ["something": "test"]) + eventRequestMock = SingularEventRequest(identifier: "123", name: "some-event", info: deviceInfo) + conversionValueRequestMock = SingularConversionValueRequest(.init(identifier: "123", eventName: "c", appDeviceInfo: deviceInfo), skanParameters: ["something": "test"]) + service = SingularService(client: httpClientMock) + } + + // Since this is a generic method that supports multiple requests, we re-use the tests + // MARK: Send Notification Info + func testSessionInfoFailsOnReceivingErrorResult() async throws { + try await genericFailsOnReceivingErrorResult(sessionInfoRequestMock) + } + func testEventFailsOnReceivingErrorResult() async throws { + try await genericFailsOnReceivingErrorResult(eventRequestMock) + } + func genericFailsOnReceivingErrorResult(_ mockRequest: SingularNotificationRequest) async throws { + // given + httpClientMock.response = successResponseMock + let singularResult = SingularResponse(status: "error", errorReason: "a reason") + httpClientMock.data = try JSONEncoder().encode(singularResult) + + // when + do { + try await service.sendNotification(request: mockRequest) + } catch SingularService.Error.dataReturnedError(let reason) { + XCTAssertEqual(reason, "a reason") + } catch { + XCTFail("Received unexpected error \(error)") + } + + // then + XCTAssertEqual(httpClientMock.requests.count, 1) + XCTAssertEqual(try? httpClientMock.requests[0].makeURLRequest(), try? mockRequest.makeURLRequest()) + } + + func testSessionInfoFailsOnReceivingNoDataInResponse() async throws { + try await genericFailsOnReceivingNoDataInResponse(sessionInfoRequestMock) + } + func testEventFailsOnReceivingNoDataInResponse() async throws { + try await genericFailsOnReceivingNoDataInResponse(eventRequestMock) + } + func genericFailsOnReceivingNoDataInResponse(_ mockRequest: SingularNotificationRequest) async throws { + // given + httpClientMock.response = successResponseMock + + // when + do { + try await service.sendNotification(request: mockRequest) + } catch DecodingError.dataCorrupted { + // expected + } catch { + XCTFail("Received unexpected error \(error)") + } + + // then + XCTAssertEqual(httpClientMock.requests.count, 1) + XCTAssertEqual(try? httpClientMock.requests[0].makeURLRequest(), try? mockRequest.makeURLRequest()) + } + + func testSessionInfoReturnsDataResultOK() async throws { + try await genericReturnsDataResultOK(sessionInfoRequestMock) + } + func testEventReturnsDataResultOK() async throws { + try await genericReturnsDataResultOK(eventRequestMock) + } + func genericReturnsDataResultOK(_ mockRequest: SingularNotificationRequest) async throws { + // given + httpClientMock.response = successResponseMock + httpClientMock.data = try JSONEncoder().encode(SingularResponse(status: "ok", errorReason: nil)) + + // when + try await service.sendNotification(request: mockRequest) + + // then + XCTAssertEqual(httpClientMock.requests.count, 1) + XCTAssertEqual(try? httpClientMock.requests[0].makeURLRequest(), try? mockRequest.makeURLRequest()) + } + + func testSessionInfoNetworkError() async throws { + try await genericNetworkError(sessionInfoRequestMock) + } + func testEventNetworkError() async throws { + try await genericNetworkError(eventRequestMock) + } + func genericNetworkError(_ mockRequest: SingularNotificationRequest) async throws { + // given + httpClientMock.response = failureResponseMock + + // when + do { + // when + try await service.sendNotification(request: mockRequest) + + // then + XCTFail("Did not throw error when it should") + } catch SingularService.Error.network { + // expected + } catch { + XCTFail("Received unexpected error \(error)") + } + + // Then + XCTAssertEqual(httpClientMock.requests.count, 1) + XCTAssertEqual(try? httpClientMock.requests[0].makeURLRequest(), try? mockRequest.makeURLRequest()) + } + + // MARK: Get Conversion Value + func testGetConversionValueFailsOnFailureStatus() async throws { + // given + httpClientMock.response = failureResponseMock + + do { + // when + _ = try await service.getConversionValue(request: conversionValueRequestMock) + + // then + XCTFail("Did not throw error when it should") + } catch SingularService.Error.network { + // expected + } catch { + XCTFail("Received unexpected error \(error)") + } + } + + func testGetConversionValueReturnsResponse() async throws { + // given + httpClientMock.response = successResponseMock + let expectedResponse = SingularConversionValueResponse(conversionValue: 12, coarseValue: 1, lockWindow: true) + httpClientMock.data = try JSONEncoder().encode(expectedResponse) + + // when + let response = try await service.getConversionValue(request: conversionValueRequestMock) + + // then + XCTAssertEqual(httpClientMock.requests.count, 1) + XCTAssertEqual(try? httpClientMock.requests[0].makeURLRequest(), try? conversionValueRequestMock.makeURLRequest()) + XCTAssertEqual(response, expectedResponse) + } + + func testGetConversionDecodesFallbackResponseAndReturnsError() async throws { + // given + httpClientMock.response = successResponseMock + let expectedResponse = SingularResponse(status: "ok", errorReason: nil) + httpClientMock.data = try JSONEncoder().encode(expectedResponse) + + // when + do { + // when + _ = try await service.getConversionValue(request: conversionValueRequestMock) + + // then + XCTFail("Did not throw error when it should") + } catch SingularService.Error.noConversionValueReturned { + // expected + } catch { + XCTFail("Received unexpected error \(error)") + } + } + + func testGetConversionReturnsFallbackResponseError() async throws { + // given + httpClientMock.response = successResponseMock + let expectedResponse = SingularResponse(status: "error", errorReason: "any reason") + httpClientMock.data = try JSONEncoder().encode(expectedResponse) + + // when + do { + // when + _ = try await service.getConversionValue(request: conversionValueRequestMock) + + // then + XCTFail("Did not throw error when it should") + } catch SingularService.Error.dataReturnedError(let reason) { + XCTAssertEqual(reason, "any reason") + } catch { + XCTFail("Received unexpected error \(error)") + } + } +} + +#endif diff --git a/EcosiaTests/Core/SingularTests.swift b/EcosiaTests/Core/SingularTests.swift new file mode 100644 index 000000000000..af31172e469c --- /dev/null +++ b/EcosiaTests/Core/SingularTests.swift @@ -0,0 +1,295 @@ +import XCTest +@testable import Ecosia + +#if os(iOS) + +class SingularTests: XCTestCase { + + private var singularService: MockSingularService! + private var skanHelper: MockSingularAdNetworkHelper! + var singular: Singular! + + override func setUpWithError() throws { + singularService = MockSingularService() + skanHelper = MockSingularAdNetworkHelper() + singular = Singular(singularService: singularService, skanHelper: skanHelper) + } + + override func tearDownWithError() throws { + singularService = nil + skanHelper = nil + singular = nil + } + + // MARK: Public init + func testPublicInitIncludingSKAN() { + // when + singular = Singular(includeSKAN: true) + + // then + XCTAssertNotNil(singular.skanHelper) + } + + func testPublicInitWithoutSKAN() { + // when + singular = Singular(includeSKAN: false) + + // then + XCTAssertNil(singular.skanHelper) + } + + // MARK: Send session info + func testShouldSendSessionInfoWithSKAN() async throws { + // given + skanHelper.persistedValuesDictionary = MockSessionParameters.mockPersistedSkanValues + + // when + try await singular.sendSessionInfo(appDeviceInfo: MockSessionParameters.mockAppDeviceInfo) + + // then + XCTAssertFalse(skanHelper.registerAppForAdNetworkAttributionCalled) + XCTAssertEqual(skanHelper.fetchFromSingularServerAndUpdateEvent, .session) + XCTAssertNotNil(skanHelper.fetchFromSingularServerAndUpdateIdentifier) + XCTAssertEqual(skanHelper.fetchFromSingularServerAndUpdateAppDeviceInfo, MockSessionParameters.mockAppDeviceInfo) + XCTAssert(singularService.sendNotificationReceivedRequest is SingularSessionInfoSendRequest) + MockSessionParameters.assertReceivedParametersEqualToExpected(singularService.sendNotificationReceivedParameters) + } + + func testFirstSessionInfoWithSKAN() async throws { + // given + skanHelper.isRegistered = false + skanHelper.persistedValuesDictionary = MockSessionParameters.mockPersistedSkanValues + + // when + try await singular.sendSessionInfo(appDeviceInfo: MockSessionParameters.mockAppDeviceInfo) + + // then + XCTAssertTrue(skanHelper.registerAppForAdNetworkAttributionCalled) + XCTAssertNil(skanHelper.fetchFromSingularServerAndUpdateEvent) + XCTAssertNil(skanHelper.fetchFromSingularServerAndUpdateIdentifier) + XCTAssertNil(skanHelper.fetchFromSingularServerAndUpdateAppDeviceInfo) + XCTAssert(singularService.sendNotificationReceivedRequest is SingularSessionInfoSendRequest) + MockSessionParameters.assertReceivedParametersEqualToExpected(singularService.sendNotificationReceivedParameters) + } + + func testShouldSendSessionInfoWithoutSKAN() async throws { + // given + singular = Singular(singularService: singularService, skanHelper: nil) + + // when + try await singular.sendSessionInfo(appDeviceInfo: MockSessionParameters.mockAppDeviceInfo) + + // then + XCTAssertFalse(skanHelper.registerAppForAdNetworkAttributionCalled) + XCTAssertNil(skanHelper.fetchFromSingularServerAndUpdateEvent) + XCTAssertNil(skanHelper.fetchFromSingularServerAndUpdateIdentifier) + XCTAssertNil(skanHelper.fetchFromSingularServerAndUpdateAppDeviceInfo) + XCTAssert(singularService.sendNotificationReceivedRequest is SingularSessionInfoSendRequest) + MockSessionParameters.assertReceivedParametersEqualToExpected(singularService.sendNotificationReceivedParameters, includeSkan: false) + } + + func testShouldThrowSendSessionInfoError() async throws { + // given + let expectedError = NSError(domain: "test", code: 0) + singularService.sendNotificationError = expectedError + + // when + do { + try await singular.sendSessionInfo(appDeviceInfo: MockSessionParameters.mockAppDeviceInfo) + XCTFail("Did not throw expected error") + } catch { + // then + XCTAssertEqual(error as NSError, expectedError) + } + } + + // MARK: Handle SKAN errors + func testShouldNotThrowSKANRegisterError() async throws { + // given + skanHelper.isRegistered = false + skanHelper.persistedValuesDictionary = MockSessionParameters.mockPersistedSkanValues + skanHelper.registerAppForAdNetworkAttributionError = NSError(domain: "test", code: 0) + + // when + do { + try await singular.sendSessionInfo(appDeviceInfo: MockSessionParameters.mockAppDeviceInfo) + } catch { + XCTFail("No error expected, but received \(error)") + } + + // then + MockSessionParameters.assertReceivedParametersEqualToExpected(singularService.sendNotificationReceivedParameters) + } + + func testShouldNotThrowSKANServerError() async throws { + // given + skanHelper.persistedValuesDictionary = MockSessionParameters.mockPersistedSkanValues + skanHelper.fetchFromSingularServerAndUpdateError = NSError(domain: "test", code: 0) + + // when + do { + try await singular.sendSessionInfo(appDeviceInfo: MockSessionParameters.mockAppDeviceInfo) + } catch { + XCTFail("No error expected, but received \(error)") + } + + // then + MockSessionParameters.assertReceivedParametersEqualToExpected(singularService.sendNotificationReceivedParameters) + } + + // MARK: Send event + func testShouldSendEvent() async throws { + // given + let expectedEvent = MMPEvent.firstSearch + + // when + try await singular.sendEvent(expectedEvent, appDeviceInfo: MockSessionParameters.mockAppDeviceInfo) + + // then + XCTAssert(singularService.sendNotificationReceivedRequest is SingularEventRequest) + MockSessionParameters.assertReceivedParametersEqualToExpected(singularService.sendNotificationReceivedParameters, includeSkan: false, event: expectedEvent.rawValue) + } + + func testShouldThrowSendEventError() async throws { + // given + let expectedError = NSError(domain: "test", code: 0) + singularService.sendNotificationError = expectedError + + // when + do { + try await singular.sendEvent(MMPEvent.firstSearch, appDeviceInfo: MockSessionParameters.mockAppDeviceInfo) + XCTFail("Did not throw expected error") + } catch { + // then + XCTAssertEqual(error as NSError, expectedError) + } + } +} + +// MARK: SingularService Mock +private class MockSingularService: SingularServiceProtocol { + var responseStub: SingularConversionValueResponse? + + func getConversionValue(request: SingularConversionValueRequest) async throws -> SingularConversionValueResponse { + throw NSError(domain: "SingularTests", + code: 0, + userInfo: [NSLocalizedDescriptionKey: "Should never call this method directly in this context"]) + } + + var sendNotificationError: Error? + var sendNotificationReceivedRequest: SingularNotificationRequest? + var sendNotificationReceivedParameters: [String: String]? + func sendNotification(request: SingularNotificationRequest) async throws { + if let error = sendNotificationError { + throw error + } + sendNotificationReceivedRequest = request + sendNotificationReceivedParameters = request.queryParameters + } +} + +// MARK: SingularAdNetworkHelper Mock +class MockSingularAdNetworkHelper: SingularAdNetworkHelperProtocol { + var persistedValuesDictionary: [String: String] = [:] + var isRegistered: Bool = true + + var registerAppForAdNetworkAttributionError: Error? + var registerAppForAdNetworkAttributionCalled = false + func registerAppForAdNetworkAttribution() async throws { + if let error = registerAppForAdNetworkAttributionError { + throw error + } + registerAppForAdNetworkAttributionCalled = true + } + + var fetchFromSingularServerAndUpdateError: Error? + var fetchFromSingularServerAndUpdateEvent: SingularEvent? + var fetchFromSingularServerAndUpdateIdentifier: String? + var fetchFromSingularServerAndUpdateAppDeviceInfo: AppDeviceInfo? + func fetchFromSingularServerAndUpdate(forEvent event: SingularEvent, sessionIdentifier: String, appDeviceInfo: AppDeviceInfo) async throws { + if let error = fetchFromSingularServerAndUpdateError { + throw error + } + fetchFromSingularServerAndUpdateEvent = event + fetchFromSingularServerAndUpdateIdentifier = sessionIdentifier + fetchFromSingularServerAndUpdateAppDeviceInfo = appDeviceInfo + } +} + +// MARK: Parameters Mock +enum MockSessionParameters { + static let mockAppDeviceInfo = AppDeviceInfo(platform: "a", + bundleId: "b", + osVersion: "c", + deviceManufacturer: "d", + deviceModel: "e", + locale: "f", + country: "g", + appVersion: "h", + installReceipt: "i", + adServicesAttributionToken: "j") + private static let mockExpectedDeviceInfoParameters = [ + "p": "a", + "i": "b", + "ve": "c", + "ma": "d", + "mo": "e", + "lc": "f", + "country": "g", + "app_v": "h", + "install_receipt": "i", + "attribution_token": "j" + ] + private static let mockExpectedReducedDeviceInfoParameters = [ + "p": "a", + "i": "b", + "ve": "c" + ] + static let mockPersistedSkanValues = [ + SingularAdNetworkHelper.PersistedObject.conversionValue.queryKey: "test", + SingularAdNetworkHelper.PersistedObject.firstSkanCallTimestamp.queryKey: "abc", + SingularAdNetworkHelper.PersistedObject.lastSkanCallTimestamp.queryKey: "def", + SingularAdNetworkHelper.PersistedObject.coarseValue(window: .first).queryKey: "ghi", + SingularAdNetworkHelper.PersistedObject.coarseValue(window: .second).queryKey: "jkl", + SingularAdNetworkHelper.PersistedObject.coarseValue(window: .third).queryKey: "mno", + SingularAdNetworkHelper.PersistedObject.previousFineValue.queryKey: "pqr", + SingularAdNetworkHelper.PersistedObject.previousCoarseValue(window: .first).queryKey: "stu", + SingularAdNetworkHelper.PersistedObject.previousCoarseValue(window: .second).queryKey: "vxw", + SingularAdNetworkHelper.PersistedObject.previousCoarseValue(window: .third).queryKey: "yza", + SingularAdNetworkHelper.PersistedObject.windowLockTimestamp(window: .first).queryKey: "bcd", + SingularAdNetworkHelper.PersistedObject.windowLockTimestamp(window: .second).queryKey: "efg", + SingularAdNetworkHelper.PersistedObject.windowLockTimestamp(window: .third).queryKey: "hij" + ] + private static let mockExpectedSkanParameters = [ + "skan_current_conversion_value": "test", + "skan_first_call_to_skadnetwork_timestamp": "abc", + "skan_last_call_to_skadnetwork_timestamp": "def", + "p0_coarse": "ghi", + "p1_coarse": "jkl", + "p2_coarse": "mno", + "prev_fine_value": "pqr", + "p0_prev_coarse_value": "stu", + "p1_prev_coarse_value": "vxw", + "p2_prev_coarse_value": "yza", + "p0_window_lock": "bcd", + "p1_window_lock": "efg", + "p2_window_lock": "hij" + ] + + static func assertReceivedParametersEqualToExpected(_ receivedParameters: [String: String]?, includeSkan: Bool = true, event: String? = nil) { + var parameters = receivedParameters + + // Assert and remove id since it is randomly generated + XCTAssertNotNil(parameters?.removeValue(forKey: "sing")) + + let isEvent = (event != nil) + let deviceInfo = isEvent ? mockExpectedReducedDeviceInfoParameters : mockExpectedDeviceInfoParameters + let expectedParameters = deviceInfo + .merging(includeSkan ? mockExpectedSkanParameters : [:]) { (current, _) in current } + .merging(isEvent ? ["n": event!] : [:]) { (current, _) in current } + + XCTAssertEqual(parameters, expectedParameters) + } +} + +#endif diff --git a/EcosiaTests/Core/SnapshotsTests.swift b/EcosiaTests/Core/SnapshotsTests.swift new file mode 100644 index 000000000000..6a015712afce --- /dev/null +++ b/EcosiaTests/Core/SnapshotsTests.swift @@ -0,0 +1,94 @@ +@testable import Ecosia +import XCTest + +final class SnapshotsTests: XCTestCase { + private var tabs: Tabs! + + override func setUp() { + tabs = .init() + try? FileManager.default.removeItem(at: FileManager.snapshots) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.snapshots) + } + + func testNoImage() { + let expect = expectation(description: "") + tabs.image(UUID()) { + XCTAssertNil($0) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testGetImage() { + let expect = expectation(description: "") + let id = UUID() + tabs.save(.init("hello world".utf8), with: id) + tabs.queue.async { + self.tabs.image(UUID()) { + XCTAssertNil($0) + } + self.tabs.image(id) { + XCTAssertEqual("hello world", String(decoding: $0 ?? Data(), as: UTF8.self)) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } + + func testReplaceImage() { + let expect = expectation(description: "") + let id = UUID() + tabs.save(.init("hello world".utf8), with: id) + tabs.save(.init("lorem ipsum".utf8), with: id) + tabs.queue.async { + self.tabs.image(id) { + XCTAssertEqual("lorem ipsum", String(decoding: $0 ?? Data(), as: UTF8.self)) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } + + func testClear() { + let expect = expectation(description: "") + let idA = UUID() + let idB = UUID() + let idC = UUID() + tabs.save(.init("hello world".utf8), with: idA) + tabs.save(.init("lorem ipsum".utf8), with: idB) + tabs.save(.init("avocado".utf8), with: idC) + tabs.queue.async { + self.tabs.image(idA) { + XCTAssertNotNil($0) + self.tabs.clear() + self.tabs.image(idA) { + XCTAssertNil($0) + expect.fulfill() + } + } + } + waitForExpectations(timeout: 1) + } + + func testDelete() { + let expect = expectation(description: "") + let idA = UUID() + let idB = UUID() + tabs.save(.init("hello world".utf8), with: idA) + tabs.save(.init("lorem ipsum".utf8), with: idB) + tabs.deleteSnapshot(idA) + tabs.queue.async { + self.tabs.image(idA) { + XCTAssertNil($0) + } + self.tabs.image(idB) { + XCTAssertNotNil($0) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } +} diff --git a/EcosiaTests/Core/StatisticsTests.swift b/EcosiaTests/Core/StatisticsTests.swift new file mode 100644 index 000000000000..e567514c35c3 --- /dev/null +++ b/EcosiaTests/Core/StatisticsTests.swift @@ -0,0 +1,41 @@ +@testable import Ecosia +import XCTest + +final class StatisticsTests: XCTestCase { + private var statistics: Statistics! + private var mockURLSession: MockURLSessionProtocol! + + override func setUp() { + statistics = Statistics.shared + mockURLSession = MockURLSessionProtocol() + } + + func testFetchAndUpdate() async throws { + mockURLSession.data = Data(""" + { + "results": [ + {"name": "Total Trees Planted", "value": "123456789", "last_updated": "2023-08-01T11:40:00.000000Z"}, + {"name": "Time per tree (seconds)", "value": "0.8"}, + {"name": "Searches per tree", "value": "20"}, + {"name": "Active Users", "value": "80000000"}, + {"name": "EUR=>USD", "value": "1.5"}, + {"name": "Some other name", "value": "123"}, + {"name": "Investments amount per second", "value": "0.423", "last_updated": null}, + {"name": "Total investments amount", "value": "2345678", "last_updated": "2023-07-30T00:00:00.000000Z"} + ] + } + """.utf8) + + try await statistics.fetchAndUpdate(urlSession: mockURLSession) + + XCTAssertEqual(statistics.treesPlanted, 123456789) + XCTAssertEqual(statistics.treesPlantedLastUpdated, Date(timeIntervalSince1970: 1690890000)) + XCTAssertEqual(statistics.timePerTree, 0.8) + XCTAssertEqual(statistics.searchesPerTree, 20) + XCTAssertEqual(statistics.activeUsers, 80000000) + XCTAssertEqual(statistics.eurToUsdMultiplier, 1.5) + XCTAssertEqual(statistics.investmentPerSecond, 0.423) + XCTAssertEqual(statistics.totalInvestments, 2345678) + XCTAssertEqual(statistics.totalInvestmentsLastUpdated, Date(timeIntervalSince1970: 1690675200)) + } +} diff --git a/EcosiaTests/Core/TabsTests.swift b/EcosiaTests/Core/TabsTests.swift new file mode 100644 index 000000000000..2b63168c9b96 --- /dev/null +++ b/EcosiaTests/Core/TabsTests.swift @@ -0,0 +1,221 @@ +@testable import Ecosia +import XCTest + +final class TabsTests: XCTestCase { + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.pages) + try? FileManager.default.removeItem(at: FileManager.snapshots) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.pages) + try? FileManager.default.removeItem(at: FileManager.snapshots) + } + + func testNew() { + let tabs = Tabs() + XCTAssertTrue(tabs.items.isEmpty) + XCTAssertNil(tabs.current) + } + + func testAdd() { + let expect = expectation(description: "") + let url = URL(string: "https://www.avocado.com")! + let tabs = Tabs() + tabs.new(url) + XCTAssertEqual(url, tabs.items.first?.page?.url) + XCTAssertEqual(0, tabs.current) + PageStore.queue.async { + XCTAssertFalse(((try? Data(contentsOf: FileManager.tabs)) ?? Data()).isEmpty) + XCTAssertFalse(((try? Data(contentsOf: FileManager.currentTab)) ?? Data()).isEmpty) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testLoad() { + let expect = expectation(description: "") + let urlFirst = URL(string: "https://www.avocado.com")! + let urlSecond = URL(string: "https://www.trees.com")! + var tabs = Tabs() + tabs.new(urlFirst) + tabs.new(urlSecond) + PageStore.queue.async { + tabs = .init() + XCTAssertEqual(urlFirst, tabs.items.first?.page?.url) + XCTAssertEqual(urlSecond, tabs.items.last?.page?.url) + XCTAssertEqual(1, tabs.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCurrentOutOfBounds() { + let expect = expectation(description: "") + PageStore.save(currentTab: 0) + PageStore.queue.async { + let tabs = Tabs() + XCTAssertNil(tabs.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testClose() { + let expect = expectation(description: "") + var tabs = Tabs() + tabs.new(URL(string: "https://www.avocado.com")!) + tabs.close(tabs.items.first!.id) + PageStore.queue.async { + tabs = .init() + XCTAssertTrue(tabs.items.isEmpty) + XCTAssertNil(tabs.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCloseNotCurrent() { + let expect = expectation(description: "") + var tabs = Tabs() + let url = URL(string: "https://www.avocado.com")! + tabs.new(URL(string: "https://www.trees.com")!) + tabs.new(url) + tabs.close(tabs.items.first!.id) + PageStore.queue.async { + tabs = .init() + XCTAssertEqual(url, tabs.items.first?.page?.url) + XCTAssertEqual(1, tabs.items.count) + XCTAssertEqual(0, tabs.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCloseOffsetIndex() { + let expect = expectation(description: "") + var tabs = Tabs() + tabs.new(URL(string: "https://www.something.com")!) + tabs.new(URL(string: "https://www.else.com")!) + tabs.new(URL(string: "https://www.trees.com")!) + tabs.new(URL(string: "https://www.avocado.com")!) + tabs.close(tabs.items.first!.id) + PageStore.queue.async { + tabs = .init() + XCTAssertEqual(3, tabs.items.count) + XCTAssertEqual(2, tabs.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testClearIndexOnClose() { + let expect = expectation(description: "") + var tabs = Tabs() + tabs.new(URL(string: "https://www.something.com")!) + tabs.new(URL(string: "https://www.else.com")!) + tabs.new(URL(string: "https://www.trees.com")!) + tabs.new(URL(string: "https://www.avocado.com")!) + tabs.close(tabs.items[3].id) + PageStore.queue.async { + tabs = .init() + XCTAssertEqual(3, tabs.items.count) + XCTAssertNil(tabs.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCloseNoIndex() { + let expect = expectation(description: "") + var tabs = Tabs() + tabs.new(URL(string: "https://www.avocado.com")!) + tabs.new(URL(string: "https://www.trees.com")!) + tabs.current = nil + tabs.close(tabs.items.first!.id) + PageStore.queue.async { + tabs = .init() + XCTAssertEqual(1, tabs.items.count) + XCTAssertNil(tabs.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testKeepIndexOnClose() { + let expect = expectation(description: "") + var tabs = Tabs() + tabs.new(URL(string: "https://www.something.com")!) + tabs.new(URL(string: "https://www.else.com")!) + tabs.new(URL(string: "https://www.trees.com")!) + tabs.new(URL(string: "https://www.avocado.com")!) + tabs.current = 1 + tabs.close(tabs.items[3].id) + PageStore.queue.async { + tabs = .init() + XCTAssertEqual(3, tabs.items.count) + XCTAssertEqual(1, tabs.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testClear() { + let expect = expectation(description: "") + var tabs = Tabs() + tabs.new(URL(string: "https://www.something.com")!) + tabs.new(URL(string: "https://www.else.com")!) + tabs.clear() + PageStore.queue.async { + tabs = .init() + XCTAssertTrue(tabs.items.isEmpty) + XCTAssertNil(tabs.current) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testMaxOneWithoutPage() { + let expect = expectation(description: "") + let tabs = Tabs() + tabs.new(nil) + tabs.new(URL(string: "https://www.avocado.com")!) + tabs.new(nil) + tabs.new(nil) + PageStore.queue.async { + XCTAssertEqual(2, tabs.items.count) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testUpdatePage() { + let expect = expectation(description: "") + let url = URL(string: "https://www.avocado.com")! + let title = "hello world" + let tabs = Tabs() + tabs.new(URL(string: "https://www.some.com")!) + tabs.new(nil) + tabs.update(tabs.items.last!.id, page: .init(url: url, title: title)) + PageStore.queue.async { + XCTAssertEqual(url, tabs.items.last?.page?.url) + XCTAssertEqual(title, tabs.items.last?.page?.title) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testCloseRemoveSnapshot() { + let expect = expectation(description: "") + let tabs = Tabs() + tabs.new(URL(string: "https://www.avocado.com")!) + let id = tabs.items.first!.id + tabs.save(.init("hello world".utf8), with: id) + tabs.close(tabs.items.first!.id) + tabs.image(id) { + XCTAssertNil($0) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } +} diff --git a/EcosiaTests/Core/Tools/BookmarkFixtures.swift b/EcosiaTests/Core/Tools/BookmarkFixtures.swift new file mode 100644 index 000000000000..e44754d03ae0 --- /dev/null +++ b/EcosiaTests/Core/Tools/BookmarkFixtures.swift @@ -0,0 +1,32 @@ +import Foundation +@testable import Ecosia + +enum BookmarkFixtures { + enum Browser: String { + case chrome, firefox, safari + } + + case html(Browser), debugString(Browser) + + var value: String { + switch self { + case let .html(browser): + return String( + data: try! Data(contentsOf: Bundle.ecosiaTests.url(forResource: "import_input_bookmark_\(browser.rawValue)", withExtension: "html")!), + encoding: .utf8 + )!.trimmingCharacters(in: .newlines) + case let .debugString(browser): + return String( + data: try! Data(contentsOf: Bundle.ecosiaTests.url(forResource: "import_output_bookmark_\(browser.rawValue)", withExtension: "txt")!), + encoding: .utf8 + )!.trimmingCharacters(in: .newlines) + } + } + + static var ecosiaExportedHtml: String { + String( + data: try! Data(contentsOf: Bundle.ecosiaTests.url(forResource: "export_bookmark_ecosia", withExtension: "html")!), + encoding: .utf8 + )!.trimmingCharacters(in: .newlines) + } +} diff --git a/EcosiaTests/Core/Tools/HTTPClientMock.swift b/EcosiaTests/Core/Tools/HTTPClientMock.swift new file mode 100644 index 000000000000..8c41fec5a2eb --- /dev/null +++ b/EcosiaTests/Core/Tools/HTTPClientMock.swift @@ -0,0 +1,16 @@ +@testable import Ecosia +import Foundation + +class HTTPClientMock: HTTPClient { + + var requests: [BaseRequest] = [] + var response: HTTPURLResponse? + var data = Data() + var executeBeforeResponse: (() -> Void)? + + func perform(_ request: BaseRequest) async throws -> (Data, HTTPURLResponse?) { + requests.append(request) + executeBeforeResponse?() + return (data, response) + } +} diff --git a/EcosiaTests/Core/Tools/MockTimestampProvider.swift b/EcosiaTests/Core/Tools/MockTimestampProvider.swift new file mode 100644 index 000000000000..472a8da9b941 --- /dev/null +++ b/EcosiaTests/Core/Tools/MockTimestampProvider.swift @@ -0,0 +1,11 @@ +import Foundation +@testable import Ecosia + +final class MockTimestampProvider: TimestampProvider { + + var currentTimestamp: TimeInterval + + init(currentTimestamp: TimeInterval) { + self.currentTimestamp = currentTimestamp + } +} diff --git a/EcosiaTests/Core/Tools/MockURLSession.swift b/EcosiaTests/Core/Tools/MockURLSession.swift new file mode 100644 index 000000000000..45ab157b3512 --- /dev/null +++ b/EcosiaTests/Core/Tools/MockURLSession.swift @@ -0,0 +1,32 @@ +import Foundation + +final class MockURLSession: URLSession { + var data = [Data]() + var request: (() -> Void)? + var response: HTTPURLResponse? + + override init() { + super.init() + } + + override func dataTask(with: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + request?() + completionHandler(data.popLast(), response, nil) + return MockDataTask() + } + + override func dataTask(with: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + request?() + completionHandler(data.popLast(), response, nil) + return MockDataTask() + } +} + +private class MockDataTask: URLSessionDataTask { + override init() { + super.init() + } + + override func resume() { + } +} diff --git a/EcosiaTests/Core/Tools/MockURLSessionProtocol.swift b/EcosiaTests/Core/Tools/MockURLSessionProtocol.swift new file mode 100644 index 000000000000..75005a327291 --- /dev/null +++ b/EcosiaTests/Core/Tools/MockURLSessionProtocol.swift @@ -0,0 +1,13 @@ +import Foundation +@testable import Ecosia + +// Needed in spite of the already existing MockURLSession +// since URLSession's async methods are not open +class MockURLSessionProtocol: URLSessionProtocol { + var data: Data? + + func data(from url: URL) async throws -> (Data, URLResponse) { + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + return (data!, response) + } +} diff --git a/EcosiaTests/Core/TreesProjectionTests.swift b/EcosiaTests/Core/TreesProjectionTests.swift new file mode 100644 index 000000000000..5670898f795a --- /dev/null +++ b/EcosiaTests/Core/TreesProjectionTests.swift @@ -0,0 +1,34 @@ +@testable import Ecosia +import XCTest + +final class TreesProjectionTests: XCTestCase { + private var treesProjection: TreesProjection! + + override func setUp() { + treesProjection = TreesProjection.shared + } + + func testTreesAt() { + let date = Date() + Statistics.shared.treesPlanted = 10 + Statistics.shared.treesPlantedLastUpdated = date.addingTimeInterval(-100) + Statistics.shared.timePerTree = 2 + XCTAssertEqual(Int(100/2 + 10-1), treesProjection.treesAt(date)) + } + + func testTimerIsActive() { + let timePerTree = 0.1 + Statistics.shared.timePerTree = timePerTree + + let exp = XCTestExpectation(description: "Wait for timer") + let projection = TreesProjection() + var receivedCount: Int? + projection.subscribe(self) { count in + receivedCount = count + exp.fulfill() + } + wait(for: [exp], timeout: timePerTree) + + XCTAssertNotNil(receivedCount) + } +} diff --git a/EcosiaTests/Core/URLProviderDependantTests/ProductionURLProviderTests.swift b/EcosiaTests/Core/URLProviderDependantTests/ProductionURLProviderTests.swift new file mode 100644 index 000000000000..87804d69dc2c --- /dev/null +++ b/EcosiaTests/Core/URLProviderDependantTests/ProductionURLProviderTests.swift @@ -0,0 +1,22 @@ +@testable import Ecosia +import XCTest + +final class ProductionURLProviderTests: XCTestCase { + + var urlProvider: URLProvider = .production + + func testProduction() { + XCTAssertEqual("https://www.ecosia.org", urlProvider.root.absoluteString) + } + + func testProductionURLsAreValid() { + XCTAssertNotNil(urlProvider.root) + XCTAssertNotNil(urlProvider.statistics) + XCTAssertNotNil(urlProvider.privacy) + XCTAssertNotNil(urlProvider.faq) + XCTAssertNotNil(urlProvider.terms) + XCTAssertNotNil(urlProvider.aboutCounter) + XCTAssertNotNil(urlProvider.snowplow) + XCTAssertNotNil(urlProvider.notifications) + } +} diff --git a/EcosiaTests/Core/URLProviderDependantTests/StagingURLProviderTests.swift b/EcosiaTests/Core/URLProviderDependantTests/StagingURLProviderTests.swift new file mode 100644 index 000000000000..a8ba879734bb --- /dev/null +++ b/EcosiaTests/Core/URLProviderDependantTests/StagingURLProviderTests.swift @@ -0,0 +1,22 @@ +@testable import Ecosia +import XCTest + +final class StagingURLProviderTests: XCTestCase { + + var urlProvider: URLProvider = .staging + + func testStaging() { + XCTAssertEqual("https://www.ecosia-staging.xyz", urlProvider.root.absoluteString) + } + + func testStagingURLsAreValid() { + XCTAssertNotNil(urlProvider.root) + XCTAssertNotNil(urlProvider.statistics) + XCTAssertNotNil(urlProvider.privacy) + XCTAssertNotNil(urlProvider.faq) + XCTAssertNotNil(urlProvider.terms) + XCTAssertNotNil(urlProvider.aboutCounter) + XCTAssertNotNil(urlProvider.snowplow) + XCTAssertNotNil(urlProvider.notifications) + } +} diff --git a/EcosiaTests/Core/URLProviderDependantTests/URLProviderTests.swift b/EcosiaTests/Core/URLProviderDependantTests/URLProviderTests.swift new file mode 100644 index 000000000000..121511d53453 --- /dev/null +++ b/EcosiaTests/Core/URLProviderDependantTests/URLProviderTests.swift @@ -0,0 +1,92 @@ +@testable import Ecosia +import XCTest + +final class URLProviderTests: XCTestCase { + + var urlProvider: URLProvider = .staging + + func testFinancialReports() { + let def = Language.current + Language.current = .en + XCTAssertNotNil(urlProvider.financialReports) + XCTAssertEqual(urlProvider.financialReports.pathComponents.last, "ecosia-financial-reports-tree-planting-receipts") + + Language.current = .fr + XCTAssertNotNil(urlProvider.financialReports) + XCTAssertEqual(urlProvider.financialReports.pathComponents.last, "rapports-financiers-recus-de-plantations-arbres") + + Language.current = .de + XCTAssertNotNil(urlProvider.financialReports) + XCTAssertEqual(urlProvider.financialReports.pathComponents.last, "ecosia-finanzberichte-baumplanzbelege") + + Language.current = def + } + + func testBlog() { + let def = Language.current + Language.current = .en + XCTAssertNotNil(urlProvider.blog) + XCTAssertEqual(urlProvider.blog.absoluteString, "https://blog.ecosia.org/") + + Language.current = .fr + XCTAssertNotNil(urlProvider.blog) + XCTAssertEqual(urlProvider.blog.absoluteString, "https://fr.blog.ecosia.org/") + + Language.current = .de + XCTAssertNotNil(urlProvider.blog) + XCTAssertEqual(urlProvider.blog.absoluteString, "https://de.blog.ecosia.org/") + + Language.current = def + } + + func testTrees() { + let def = Language.current + Language.current = .en + XCTAssertNotNil(urlProvider.trees) + XCTAssertTrue(urlProvider.trees.absoluteString.hasSuffix("tag/where-does-ecosia-plant-trees/")) + + Language.current = .fr + XCTAssertNotNil(urlProvider.trees) + XCTAssertTrue(urlProvider.trees.absoluteString.hasSuffix("tag/projets/")) + + Language.current = .de + XCTAssertNotNil(urlProvider.trees) + XCTAssertTrue(urlProvider.trees.absoluteString.hasSuffix("tag/projekte/")) + + Language.current = def + } + + func testBetaProgram() { + let def = Language.current + Language.current = .en + XCTAssertNotNil(urlProvider.betaProgram) + XCTAssertEqual(urlProvider.betaProgram.absoluteString, "https://ecosia.typeform.com/to/EeMLqL3X") + + Language.current = .fr + XCTAssertNotNil(urlProvider.betaProgram) + XCTAssertEqual(urlProvider.betaProgram.absoluteString, "https://ecosia.typeform.com/to/oaFZzT0F") + + Language.current = .de + XCTAssertNotNil(urlProvider.betaProgram) + XCTAssertEqual(urlProvider.betaProgram.absoluteString, "https://ecosia.typeform.com/to/catmFLuA") + + Language.current = def + } + + func testBetaFeedback() { + let def = Language.current + Language.current = .en + XCTAssertNotNil(urlProvider.betaFeedback) + XCTAssertEqual(urlProvider.betaFeedback.absoluteString, "https://ecosia.typeform.com/to/LlUGlFT9") + + Language.current = .fr + XCTAssertNotNil(urlProvider.betaFeedback) + XCTAssertEqual(urlProvider.betaFeedback.absoluteString, "https://ecosia.typeform.com/to/PRw7550n") + + Language.current = .de + XCTAssertNotNil(urlProvider.betaFeedback) + XCTAssertEqual(urlProvider.betaFeedback.absoluteString, "https://ecosia.typeform.com/to/pIQ3uwp9") + + Language.current = def + } +} diff --git a/EcosiaTests/Core/URLRequestTests.swift b/EcosiaTests/Core/URLRequestTests.swift new file mode 100644 index 000000000000..9cdb059516ce --- /dev/null +++ b/EcosiaTests/Core/URLRequestTests.swift @@ -0,0 +1,13 @@ +import XCTest +@testable import Ecosia + +final class URLRequestTests: XCTestCase { + + func testAddLanguageRegionHeader() { + var request = URLRequest(url: URL(string: "https://www.ecosia.org/search")!) + request.addLanguageRegionHeader() + + let dashedLanguageAndRegion = Locale.current.identifier.replacingOccurrences(of: "_", with: "-").lowercased() + XCTAssertEqual(request.value(forHTTPHeaderField: "x-ecosia-app-language-region"), dashedLanguageAndRegion) + } +} diff --git a/EcosiaTests/Core/URLTests.swift b/EcosiaTests/Core/URLTests.swift new file mode 100644 index 000000000000..12b416323659 --- /dev/null +++ b/EcosiaTests/Core/URLTests.swift @@ -0,0 +1,179 @@ +@testable import Ecosia +import XCTest + +final class URLTests: XCTestCase { + + private var root: String! + var urlProvider: URLProvider = .production + + override func setUp() { + root = "\(urlProvider.root.scheme!)" + "://" + urlProvider.root.host! + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + func testSearchUrl() { + let expect = expectation(description: "") + let suffix = "&tt=iosapp" + User.queue.async { + XCTAssertEqual(self.root + "/search?q=somefakesitecom" + suffix, URL.ecosiaSearchWithQuery("somefakesitecom", urlProvider: self.urlProvider).absoluteString) + XCTAssertEqual(self.root + "/search?q=some%20fakes%20ite.com" + suffix, URL.ecosiaSearchWithQuery("some fakes ite.com", urlProvider: self.urlProvider).absoluteString) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testEncodedQuery() { + let expect = expectation(description: "") + User.queue.async { + XCTAssertEqual(self.root + "/search?q=Paul+Coffee%3DGood,%20right?&tt=iosapp", URL.ecosiaSearchWithQuery("Paul+Coffee=Good, right?", urlProvider: self.urlProvider).absoluteString) + XCTAssertEqual(self.root + "/search?q=Hello%20THEre!&tt=iosapp", URL.ecosiaSearchWithQuery("Hello THEre!", urlProvider: self.urlProvider).absoluteString) + XCTAssertEqual(self.root + "/search?q=quinney%20for%20%20president&tt=iosapp", URL.ecosiaSearchWithQuery("quinney for president", urlProvider: self.urlProvider).absoluteString) + XCTAssertEqual(self.root + "/search?q=+?%25&tt=iosapp", URL.ecosiaSearchWithQuery("+?%", urlProvider: self.urlProvider).absoluteString) + XCTAssertEqual(self.root + "/search?q=Hello%2520THEre%2521&tt=iosapp", URL.ecosiaSearchWithQuery("Hello%20THEre%21", urlProvider: self.urlProvider).absoluteString) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testAvoidEcosifyWrongScheme() { + let ecosified = URL(string: "gmsg://ecosia.org")!.ecosified(isIncognitoEnabled: false) + XCTAssertEqual(ecosified, URL(string: "gmsg://ecosia.org")) + } + + func testDontEcosifyIfNotEcosia() { + let ecosified = URL(string: "https://guacamole.org")!.ecosified(isIncognitoEnabled: false) + XCTAssertEqual(ecosified, URL(string: "https://guacamole.org")) + } + + func testPolicyAllow() { + XCTAssertEqual(.allow, URL(string: "ecosia.org")!.policy) + XCTAssertEqual(.allow, URL(string: "http://ecosia.org")!.policy) + XCTAssertEqual(.allow, URL(string: "http://www.ecosia.org")!.policy) + XCTAssertEqual(.allow, URL(string: "https://www.ecosia.org")!.policy) + XCTAssertEqual(.allow, URL(string: "ecosia://ecosia.org")!.policy) + XCTAssertEqual(.allow, URL(string: "maps://ecosia.org")!.policy) + } + + func testPolicyCancel() { + XCTAssertEqual(.cancel, URL(string: "gmsg://mobileads.google.com/appEvent?name=sitedefault&info=true&google.afma.Notify_dt=1625136586354")!.policy) + XCTAssertEqual(.cancel, URL(string: "gmsg://mobileads.google.com")!.policy) + XCTAssertEqual(.cancel, URL(string: "gmsg://something")!.policy) + XCTAssertEqual(.cancel, URL(string: "gmsg://")!.policy) + } + + func testPatchWithAnalyticsId() { + let id = UUID() + User.shared.sendAnonymousUsageData = true + User.shared.cookieConsentValue = "a" + User.shared.analyticsId = id + + let domain = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(domain, URL(string: "https://ecosia.org?_sp=\(id.uuidString)")) + + let search = URL(string: "https://www.www.ecosia.org/search?q=foo")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(search, URL(string: "https://www.www.ecosia.org/search?q=foo&_sp=\(id.uuidString)")) + + let alreadyPatched = URL(string: "https://www.www.ecosia.org/search?q=foo&_sp=12345")!.ecosified(isIncognitoEnabled: false) + XCTAssertEqual(alreadyPatched, URL(string: "https://www.www.ecosia.org/search?q=foo&_sp=12345")) + + let multiPatch = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(multiPatch, URL(string: "https://ecosia.org?_sp=\(id.uuidString)")) + } + + func testPatchWithNULLAnalyticsId() { + let id = UUID() + User.shared.sendAnonymousUsageData = false + User.shared.analyticsId = id + + let domain = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(domain, URL(string: "https://ecosia.org?_sp=\(UUID(uuid: UUID_NULL).uuidString)")) + + let search = URL(string: "https://www.www.ecosia.org/search?q=foo")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(search, URL(string: "https://www.www.ecosia.org/search?q=foo&_sp=\(UUID(uuid: UUID_NULL).uuidString)")) + + let multiPatch = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(multiPatch, URL(string: "https://ecosia.org?_sp=\(UUID(uuid: UUID_NULL).uuidString)")) + } + + func testURLEcosifiedInIncognitoMode() { + let domain = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: true, urlProvider: self.urlProvider) + XCTAssertEqual(domain, URL(string: "https://ecosia.org?_sp=\(UUID(uuid: UUID_NULL).uuidString)")) + } + + func testURLEcosifiedWithAnonymousUsageDataToggleOFF() { + User.shared.sendAnonymousUsageData = false + let domain = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(domain, URL(string: "https://ecosia.org?_sp=\(UUID(uuid: UUID_NULL).uuidString)")) + } + + func testURLEcosifiedWithRejectedAnalyticsCookies() { + User.shared.cookieConsentValue = "e" + let domain = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(domain, URL(string: "https://ecosia.org?_sp=\(UUID(uuid: UUID_NULL).uuidString)")) + } + + func testURLEcosifiedWithAnonymousUsageDataToggleOFFButRejectedAnalyticsCookies() { + User.shared.sendAnonymousUsageData = false + User.shared.cookieConsentValue = "e" + let domain = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(domain, URL(string: "https://ecosia.org?_sp=\(UUID(uuid: UUID_NULL).uuidString)")) + } + + func testURLEcosifiedWithAnonymousUsageDataToggleONAndRejectedAnalyticsCookies() { + User.shared.sendAnonymousUsageData = true + User.shared.cookieConsentValue = "e" + let domain = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(domain, URL(string: "https://ecosia.org?_sp=\(UUID(uuid: UUID_NULL).uuidString)")) + } + + func testEcosify() { + User.shared.sendAnonymousUsageData = true + User.shared.cookieConsentValue = "a" + let ecosified = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(ecosified, URL(string: "https://ecosia.org?_sp=\(User.shared.analyticsId.uuidString)")) + } + + func testEcosifyWithNULLAnalyticsID() { + User.shared.sendAnonymousUsageData = false + let ecosified = URL(string: "https://ecosia.org")!.ecosified(isIncognitoEnabled: false, urlProvider: self.urlProvider) + XCTAssertEqual(ecosified, URL(string: "https://ecosia.org?_sp=\(UUID(uuid: UUID_NULL).uuidString)")) + } + + func testAssertIsNotEcosiaSearchURLOnNonEcosiaURL() { + let nonEcosiaURL = URL(string: "https://www.non-ecosia.com/search")! + XCTAssertFalse(nonEcosiaURL.isEcosiaSearchQuery(urlProvider)) + } + + func testAssertIsNotEcosiaSearchURLOnNonEcosiaSearchQueryURL() { + let nonSearchEcosiaURL = URL(string: "https://www.ecosia.org/example")! + XCTAssertFalse(nonSearchEcosiaURL.isEcosiaSearchQuery(urlProvider)) + } + + func testAssertIsEcosiaSearchURLOnEcosiaSearchQueryURL() { + let searchEcosiaURL = URL(string: "https://www.ecosia.org/search")! + XCTAssertTrue(searchEcosiaURL.isEcosiaSearchQuery(urlProvider)) + } + + func testAssertShouldEcosifyOnNonEcosiaURL() { + let nonSearchEcosiaURL = URL(string: "https://www.google.com")! + XCTAssertFalse(nonSearchEcosiaURL.shouldEcosify(urlProvider)) + } + + func testAssertShouldEcosifyOnAllEcosiaURLs() { + let ecosiaURLs = [ + "https://ecosia.org", + "https://www.ecosia.org/search?q=foo", + "https://ecosia.org/image?q=test", + "https://ecosia.org/chat?q=test", + "https://ecosia.org/news?q=test", + "https://blog.ecosia.org/", + "https://www.ecosia.org/settings" + ] + ecosiaURLs.forEach { urlString in + XCTAssertTrue(URL(string: urlString)!.shouldEcosify(urlProvider)) + } + } +} diff --git a/EcosiaTests/Core/UnleashFeatureManagementSessionInitializerTests.swift b/EcosiaTests/Core/UnleashFeatureManagementSessionInitializerTests.swift new file mode 100644 index 000000000000..d140475647d6 --- /dev/null +++ b/EcosiaTests/Core/UnleashFeatureManagementSessionInitializerTests.swift @@ -0,0 +1,84 @@ +import XCTest +import Foundation +@testable import Ecosia + +class UnleashFeatureManagementSessionInitializerTests: XCTestCase { + + // MARK: - Test Mocks + + class MockHTTPClient: HTTPClient { + + var performCalled = false + var performRequest: BaseRequest? + var performResult: HTTPClient.Result? + var performError: Error? + + func perform(_ request: BaseRequest) async throws -> HTTPClient.Result { + performCalled = true + performRequest = request + + if let error = performError { + throw error + } + + return performResult ?? (Data(), HTTPURLResponse()) + } + } + + // MARK: - Test Cases + + func testInitializeSession_WithValidData_ReturnsDecodedObject() async throws { + // Arrange + let client = MockHTTPClient() + let request = UnleashTests.stagingUnleashRequest + let expectedData = "{\"etag\": \"a-etag\"}".data(using: .utf8)! + let expectedResponse = HTTPURLResponse(url: URL(string: "https://ecosia.org")!, statusCode: 200, httpVersion: nil, headerFields: ["etag": "a-etag"])! + let model = try await UnleashTests.makeAvailableUnleashModel() + let initializer = UnleashFeatureManagementSessionInitializer(client: client, request: request, model: model) + + client.performResult = (expectedData, expectedResponse) + + // Act + let latestAvailableModel: Unleash.Model = try await initializer.startSession()! + + // Assert + XCTAssertTrue(client.performCalled, "The `perform` method should be called.") + XCTAssertEqual(try? client.performRequest?.makeURLRequest(), try? request.makeURLRequest(), "The request passed to the `perform` method should match the initialized request.") + XCTAssertEqual(latestAvailableModel.etag, "a-etag", "The decoded value should match the expected value.") + } + + func testInitializeSession_WithNoData_ThrowsNoDataError() async throws { + // Arrange + let client = MockHTTPClient() + let request = UnleashTests.stagingUnleashRequest + let model = try await UnleashTests.makeAvailableUnleashModel() + let initializer = UnleashFeatureManagementSessionInitializer(client: client, request: request, model: model) + + let expectedResponse = HTTPURLResponse(url: URL(string: "https://ecosia.org")!, statusCode: 200, httpVersion: nil, headerFields: nil)! + client.performResult = (Data(), expectedResponse) + + // Act & Assert + do { + let _: Unleash.Model = try await initializer.startSession()! + } catch { + XCTAssertEqual(error as? UnleashFeatureManagementSessionInitializer.Error, .noData, "The error should be `.noData`.") + } + } + + func testInitializeSession_WithNetworkError_ThrowsNetworkError() async throws { + // Arrange + let client = MockHTTPClient() + let model = try await UnleashTests.makeAvailableUnleashModel() + let initializer = UnleashFeatureManagementSessionInitializer(client: client, request: UnleashTests.stagingUnleashRequest, model: model) + + let expectedError = UnleashFeatureManagementSessionInitializer.Error.network + client.performError = expectedError + + // Act & Assert + do { + let _: Unleash.Model = try await initializer.startSession()! + } catch { + XCTAssertEqual(error as? UnleashFeatureManagementSessionInitializer.Error, expectedError, "The error should be `.network`.") + } + } +} diff --git a/EcosiaTests/Core/UnleashRefreshConfiguratorTests.swift b/EcosiaTests/Core/UnleashRefreshConfiguratorTests.swift new file mode 100644 index 000000000000..38d8f7cc65a3 --- /dev/null +++ b/EcosiaTests/Core/UnleashRefreshConfiguratorTests.swift @@ -0,0 +1,99 @@ +import Foundation + +@testable import Ecosia +import XCTest + +final class UnleashRefreshConfiguratorTests: XCTestCase { + + override func setUp() { + Unleash.rules = [] + } + + override func tearDown() { + Unleash.rules = [] + } + + func testAppUpdateRuleCheck() async { + // Given + let configurator = UnleashRefreshConfigurator() + configurator.withAppUpdateCheckRule(appVersion: "1.0.0") + + // Simulate App Version update + Unleash.model.appVersion = "1.1.0" + + // Then + XCTAssertTrue(Unleash.shouldRefresh, "Unleash should refresh after an app update.") + } + + func testAppUpdateRuleCheckNoVersionChange() async { + // Given + let configurator = UnleashRefreshConfigurator() + configurator.withAppUpdateCheckRule(appVersion: "1.0.0") + + // Simulate no App Version update (version stays the same) + Unleash.model.appVersion = "1.0.0" + + // Then + XCTAssertFalse(Unleash.shouldRefresh, "Unleash should not refresh when app version has not changed.") + } + + func testTwentyFourHourCacheExpirationRuleMoreThan24h() async { + // Given + let mockTimestampProvider = MockTimestampProvider(currentTimestamp: Date().addingTimeInterval(TimeInterval.twentyFourHoursTimeInterval + 1).timeIntervalSince1970) + let timeRule = TimeBasedRefreshingRule(interval: TimeInterval.twentyFourHoursTimeInterval, timestampProvider: mockTimestampProvider) + Unleash.addRule(timeRule) + + // When + Unleash.model.updated = Date() + + // Then + XCTAssertTrue(Unleash.shouldRefresh, "Unleash should refresh after 24 hours.") + } + + func testTwentyFourHourCacheExpirationRuleLessThan24h() async { + // Given + let configurator = UnleashRefreshConfigurator() + configurator.withTwentyFourHoursCacheExpirationRule() + + // When + Unleash.model.updated = Date().addingTimeInterval(TimeInterval.twentyFourHoursTimeInterval - 1) + + // Then + XCTAssertFalse(Unleash.shouldRefresh, "Unleash should not refresh if less than 24 hours have passed.") + } + + func testDeviceRegionUpdateCheckWithNoDeviceRegionChange() async { + // Given + let configurator = UnleashRefreshConfigurator() + configurator.withDeviceRegionUpdateCheckRule(localeProvider: MockLocale("us")) + + // When + Unleash.model.deviceRegion = "us" + + // Then + XCTAssertFalse(Unleash.shouldRefresh, "Unleash should not refresh when the device region has not changed.") + } + + func testDeviceRegionUpdateCheckWithDeviceRegionChange() async { + // Given + let configurator = UnleashRefreshConfigurator() + configurator.withDeviceRegionUpdateCheckRule(localeProvider: MockLocale("us")) + + // When + Unleash.model.deviceRegion = "uk" + + // Then + XCTAssertTrue(Unleash.shouldRefresh, "Unleash should refresh after a device region change.") + } +} + +extension UnleashRefreshConfiguratorTests { + + struct MockLocale: RegionLocatable { + var regionIdentifierLowercasedWithFallbackValue: String + + init(_ identifier: String) { + self.regionIdentifierLowercasedWithFallbackValue = identifier + } + } +} diff --git a/EcosiaTests/Core/UnleashTests.swift b/EcosiaTests/Core/UnleashTests.swift new file mode 100644 index 000000000000..e5b3fece91a8 --- /dev/null +++ b/EcosiaTests/Core/UnleashTests.swift @@ -0,0 +1,257 @@ +@testable import Ecosia +import XCTest + +final class UnleashTests: XCTestCase { + + static let appVersion = "0.0.0" + + override func setUp() { + Unleash.rules = [] + try? FileManager.default.removeItem(at: FileManager.unleash) + } + + override func tearDown() { + Unleash.rules = [] + try? FileManager.default.removeItem(at: FileManager.unleash) + } + + func testSaveAndLoadFromCache() async throws { + var model = Unleash.Model() + let toggle = Unleash.Toggle(name: Unleash.Toggle.Name.configTest.rawValue, + enabled: true, + variant: .init(name: "control", enabled: true, payload: nil)) + model.toggles.insert(toggle) + try await Unleash.save(model) + + let loaded = await Unleash.load() + Unleash.model = loaded! + + XCTAssertTrue(Unleash.isEnabled(.configTest)) + XCTAssertTrue(Unleash.getVariant(.configTest).name == "control") + XCTAssertTrue(Unleash.getVariant(.configTest).enabled) + } + + func testReset() async throws { + let model = try await Unleash.start(env: .staging, appVersion: Self.appVersion) + let resetModel = try await Unleash.reset(env: .staging, appVersion: Self.appVersion) + + XCTAssertNotEqual(model.id, resetModel.id) + } + + func testMakeURL() { + let base = URL(string: "https://ecosia.org")! + let context = ["foo": "bar"] + var request = UnleashTests.stagingUnleashRequest + request.queryParameters = context + let url = request.baseURL + + XCTAssertTrue(url.absoluteString.hasPrefix(base.absoluteString)) + XCTAssertEqual(URLComponents(string: try request.makeURLRequest().url!.absoluteString)?.queryItems?.count, 1) + } + + func testMakeRequest() { + let stagingRequest = try! UnleashTests.stagingUnleashRequest.makeURLRequest() + XCTAssertNotNil(stagingRequest.value(forHTTPHeaderField: CloudflareKeyProvider.clientId)) + XCTAssertNotNil(stagingRequest.value(forHTTPHeaderField: CloudflareKeyProvider.clientSecret)) + XCTAssertNotNil(stagingRequest.value(forHTTPHeaderField: "If-None-Match")) + + let prodRequest = try! UnleashTests.prodUnleashRequest.makeURLRequest() + XCTAssertNil(prodRequest.value(forHTTPHeaderField: CloudflareKeyProvider.clientId)) + XCTAssertNil(prodRequest.value(forHTTPHeaderField: CloudflareKeyProvider.clientSecret)) + XCTAssertNotNil(stagingRequest.value(forHTTPHeaderField: "If-None-Match")) + } + + func testConfigTestEnabled() { + + let expectedEnabledStatus = true + let toggleName: Unleash.Toggle.Name = .configTest + let exampleToggle = Unleash.Toggle(name: toggleName.rawValue, + enabled: expectedEnabledStatus, + variant: Unleash.Variant(name: "", enabled: false, payload: nil)) + + let mockModel = Unleash.Model(toggles: Set([exampleToggle])) + + Unleash.model = mockModel + + let isEnabled = Unleash.isEnabled(toggleName) + + XCTAssertEqual(isEnabled, expectedEnabledStatus) + } +} + +extension UnleashTests { + + static func makeAvailableUnleashModel() async throws -> Unleash.Model { + try await Unleash.start(client: MockOKHTTPClient(), request: UnleashTests.stagingUnleashRequest, env: .staging, appVersion: Self.appVersion) + } +} + +extension UnleashTests { + + struct MockOKHTTPClient: HTTPClient { + + func perform(_ request: BaseRequest) async throws -> HTTPClient.Result { + let url = URL(string: "https://ecosia.org")! + let okResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) + let model = Unleash.Model(id: UUID(), toggles: [], updated: Date(), etag: "a-etag") + let modelData = try JSONEncoder().encode(model) + return (modelData, okResponse) + } + } +} + +extension UnleashTests { + + static var stagingUnleashRequest = MockStagingUnleashRequest(etag: "a-tag") + static var prodUnleashRequest = MockProdUnleashRequest(etag: "a-tag") + + static func mockMakeURLRequest(for url: URL, + path: String?, + queryParameters: [String: String]?, + etag: String, + method: HTTPMethod, + body: Data?, + environment: Environment) -> URLRequest { + + var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) + + if let queryParameters { + urlComponents?.queryItems = queryParameters.map({ .init(name: $0.key, value: $0.value ) }) + } + + if let path { + urlComponents?.path = path + } + + var request = URLRequest(url: urlComponents?.url ?? url) + request.httpMethod = method.rawValue + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(etag, forHTTPHeaderField: "If-None-Match") + request.cachePolicy = .reloadIgnoringLocalCacheData + request.httpBody = body + + if let auth = environment.auth { + request.setValue(auth.id, forHTTPHeaderField: CloudflareKeyProvider.clientId) + request.setValue(auth.secret, forHTTPHeaderField: CloudflareKeyProvider.clientSecret) + } + + return request + } + + struct MockStagingUnleashRequest: BaseRequest { + + var environment: Environment { + .staging + } + + var method: Ecosia.HTTPMethod { + .get + } + + var baseURL: URL { + URL(string: "https://ecosia.org")! + } + + var path: String { + "" + } + + var etag: String + + var queryParameters: [String: String]? + + var additionalHeaders: [String: String]? { + ["If-None-Match": etag] + } + + var body: Data? + + func makeURLRequest() throws -> URLRequest { + UnleashTests.mockMakeURLRequest(for: baseURL, + path: path, + queryParameters: queryParameters, + etag: etag, + method: method, + body: body, + environment: environment) + } + } + + struct MockProdUnleashRequest: BaseRequest { + + var environment: Environment { + .production + } + + var method: Ecosia.HTTPMethod { + .get + } + + var baseURL: URL { + URL(string: "https://ecosia.org")! + } + + var path: String { + "" + } + + var etag: String + + var queryParameters: [String: String]? + + var additionalHeaders: [String: String]? { + ["If-None-Match": etag] + } + + var body: Data? + + func makeURLRequest() throws -> URLRequest { + UnleashTests.mockMakeURLRequest(for: baseURL, + path: path, + queryParameters: queryParameters, + etag: etag, + method: method, + body: body, + environment: environment) + } + } +} + +extension UnleashTests { + + // Need two types of mocks as the `Unleash.addRule` performs a type-safe addition + + struct MockAppUpdateRule: RefreshingRule { + let shouldRefresh: Bool + } + + struct MockDeviceRegionChangeRule: RefreshingRule { + let shouldRefresh: Bool + } + + func testShouldRefreshIfAnyRuleIsTrue() async { + // Given + let rule1 = MockAppUpdateRule(shouldRefresh: false) + let rule2 = MockDeviceRegionChangeRule(shouldRefresh: true) + + // When + Unleash.addRule(rule1) + Unleash.addRule(rule2) + + // Then + XCTAssertTrue(Unleash.shouldRefresh, "Unleash should refresh if any rule returns true.") + } + + func testShouldNotRefreshIfAllRulesAreFalse() async { + // Given + let rule1 = MockAppUpdateRule(shouldRefresh: false) + let rule2 = MockDeviceRegionChangeRule(shouldRefresh: false) + + // When + Unleash.addRule(rule1) + Unleash.addRule(rule2) + + // Then + XCTAssertFalse(Unleash.shouldRefresh, "Unleash should not refresh if all rules return false.") + } +} diff --git a/EcosiaTests/Core/UpgradeTests.swift b/EcosiaTests/Core/UpgradeTests.swift new file mode 100644 index 000000000000..edc46d5e4580 --- /dev/null +++ b/EcosiaTests/Core/UpgradeTests.swift @@ -0,0 +1,45 @@ +import XCTest +@testable import Ecosia + +final class UpgradeTests: XCTestCase { + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + func testFrom5_3To6() { + var old = User5_3() + old.install = .init(timeIntervalSince1970: 123) + old.news = .init(timeIntervalSince1970: 456) + old.analyticsId = .init() + old.marketCode = .bg_bg + old.adultFilter = .strict + old.autoComplete = false + old.firstTime = false + old.personalized = true + old.migrated = true + old.id = "hello world" + old.treeCount = 909 + old.state[User5_3.Key.welcomeScreen.rawValue] = "\(false)" + + try! JSONEncoder().encode(old).write(to: FileManager.user, options: .atomic) + + let upgraded = User() + XCTAssertEqual(old.install, upgraded.install) + XCTAssertEqual(old.news, upgraded.news) + XCTAssertEqual(old.analyticsId, upgraded.analyticsId) + XCTAssertEqual(old.marketCode, upgraded.marketCode) + XCTAssertEqual(old.adultFilter, upgraded.adultFilter) + XCTAssertEqual(old.autoComplete, upgraded.autoComplete) + XCTAssertEqual(old.firstTime, upgraded.firstTime) + XCTAssertEqual(old.personalized, upgraded.personalized) + XCTAssertEqual(old.migrated, upgraded.migrated) + XCTAssertEqual(old.id, upgraded.id) + XCTAssertEqual(old.treeCount, upgraded.searchCount) + XCTAssertEqual(old.state, upgraded.state) + XCTAssertEqual(Referrals.Model(), upgraded.referrals) + } +} diff --git a/EcosiaTests/Core/UserStateTests.swift b/EcosiaTests/Core/UserStateTests.swift new file mode 100644 index 000000000000..c60d5a21a543 --- /dev/null +++ b/EcosiaTests/Core/UserStateTests.swift @@ -0,0 +1,51 @@ +@testable import Ecosia +import XCTest + +final class UserStateTests: XCTestCase { + private var user: User! + + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.user) + user = .init() + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + func testStoredState() { + let storedState = ["test": "something_stored"] + let expect = expectation(description: "") + User.shared.state = storedState + User.queue.async { + let user = User() + XCTAssertEqual(user.state, storedState) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testShouldShowImpactIntro() { + XCTAssertTrue(user.shouldShowImpactIntro) + + user.state[User.Key.impactIntro.rawValue] = "\(false)" + + XCTAssertFalse(self.user.shouldShowImpactIntro) + } + + func testHideImpactIntro() { + user.state[User.Key.impactIntro.rawValue] = "\(true)" + + user.hideImpactIntro() + + XCTAssertEqual(user.state[User.Key.impactIntro.rawValue], "\(false)") + } + + func testShowImpactIntro() { + user.state[User.Key.impactIntro.rawValue] = "\(false)" + + user.showImpactIntro() + + XCTAssertEqual(user.state[User.Key.impactIntro.rawValue], "\(true)") + } +} diff --git a/EcosiaTests/Core/UserTests.swift b/EcosiaTests/Core/UserTests.swift new file mode 100644 index 000000000000..16bcd052f113 --- /dev/null +++ b/EcosiaTests/Core/UserTests.swift @@ -0,0 +1,370 @@ +@testable import Ecosia +import XCTest + +final class UserTests: XCTestCase { + override func setUp() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + override func tearDown() { + try? FileManager.default.removeItem(at: FileManager.user) + } + + func testFirstTime() { + let expect = expectation(description: "") + XCTAssertTrue(User.shared.firstTime) + let analyticsId = User.shared.analyticsId + User.shared.firstTime = false + User.queue.async { + let user = User() + XCTAssertEqual(analyticsId, user.analyticsId) + XCTAssertFalse(user.firstTime) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testInstall() { + let date = Date() + try? FileManager.default.removeItem(at: FileManager.user) + XCTAssertGreaterThanOrEqual(Int(User().install.timeIntervalSince1970), Int(date.timeIntervalSince1970)) + } + + func testInstallSavesAfterFirst() { + let expect = expectation(description: "") + let user = User() + User.queue.async { + XCTAssertEqual(Int(user.install.timeIntervalSince1970), Int(User().install.timeIntervalSince1970)) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testNotSavingOnLoad() { + let expect = expectation(description: "") + var user = User() + user.firstTime = false + User.shared = user + User.queue.async { + user = User() + try! FileManager.default.removeItem(at: FileManager.user) + XCTAssertFalse(user.firstTime) + User.queue.async { + XCTAssertNotNil(user) + DispatchQueue.main.async { + XCTAssertFalse(FileManager.default.fileExists(atPath: FileManager.user.path)) + expect.fulfill() + } + } + } + waitForExpectations(timeout: 1) + } + + func testAnalyticsId() { + let expect = expectation(description: "") + let id = UUID() + XCTAssertNotEqual(id, User.shared.analyticsId) + User.shared.analyticsId = id + User.queue.async { + let user = User() + XCTAssertEqual(id, user.analyticsId) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testTreeCount() { + let expect = expectation(description: "") + User.shared.searchCount = 123 + User.queue.async { + let user = User() + XCTAssertEqual(123, user.searchCount) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testAdultFilter() { + let expect = expectation(description: "") + User.shared.adultFilter = .off + User.queue.async { + let user = User() + XCTAssertEqual(.off, user.adultFilter) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testMarketCode() { + let expect = expectation(description: "") + User.shared.marketCode = .ar_sa + User.queue.async { + let user = User() + XCTAssertEqual(.ar_sa, user.marketCode) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testAutoComplete() { + let expect = expectation(description: "") + User.shared.autoComplete = false + User.queue.async { + let user = User() + XCTAssertFalse(user.autoComplete) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testState() { + let expect = expectation(description: "") + User.shared.state["lorem"] = "hello" + User.queue.async { + let user = User() + XCTAssertEqual("hello", user.state["lorem"]) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testId() { + let expect = expectation(description: "") + User.shared.id = "hello world" + User.queue.async { + let user = User() + XCTAssertEqual("hello world", user.id) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testPersonalized() { + let expect = expectation(description: "") + User.shared.personalized = true + User.queue.async { + let user = User() + XCTAssertEqual(true, user.personalized) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testSendAnonymousUsageData() { + let expect = expectation(description: "") + User.shared.sendAnonymousUsageData = false + User.queue.async { + let user = User() + XCTAssertEqual(false, user.personalized) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testSendAnonymousUsageDataDefaultsToTrue() { + let user = User() + XCTAssertEqual(true, user.sendAnonymousUsageData) + } + + func testMigrated() { + let expect = expectation(description: "") + XCTAssert(User.shared.migrated == false) + User.shared.migrated = true + User.queue.async { + let user = User() + XCTAssertEqual(true, user.migrated) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testNews() { + let expect = expectation(description: "") + XCTAssertEqual(.distantPast, User.shared.news) + User.shared.news = Date() + User.queue.async { + let user = User() + XCTAssertNotNil(user.news) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testShowTopSites() { + let expect = expectation(description: "") + XCTAssertEqual(true, User.shared.showTopSites) + User.shared.showTopSites = false + User.queue.async { + let user = User() + XCTAssertEqual(false, user.showTopSites) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testTopSitesRows() { + let expect = expectation(description: "") + XCTAssertEqual(4, User.shared.topSitesRows) + User.shared.topSitesRows = 2 + User.queue.async { + let user = User() + XCTAssertEqual(2, user.topSitesRows) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testShowClimateImpact() { + let expect = expectation(description: "") + XCTAssertEqual(true, User.shared.showClimateImpact) + User.shared.showClimateImpact = false + User.queue.async { + let user = User() + XCTAssertEqual(false, user.showClimateImpact) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testShowEcosiaNews() { + let expect = expectation(description: "") + XCTAssertEqual(true, User.shared.showEcosiaNews) + User.shared.showEcosiaNews = false + User.queue.async { + let user = User() + XCTAssertEqual(false, user.showEcosiaNews) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testShowAboutEcosia() { + let expect = expectation(description: "") + XCTAssertEqual(true, User.shared.showAboutEcosia) + User.shared.showAboutEcosia = false + User.queue.async { + let user = User() + XCTAssertEqual(false, user.showAboutEcosia) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testWhatsNewItemsVersions() { + let expect = expectation(description: "") + XCTAssertTrue(User.shared.whatsNewItemsVersionsShown.isEmpty) + User.shared.whatsNewItemsVersionsShown.formUnion(["test", "test1", "test"]) + User.queue.async { + let user = User() + XCTAssertEqual(user.whatsNewItemsVersionsShown, ["test", "test1"]) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testShowsReferralSpotlight() { + let expect = expectation(description: "") + XCTAssertFalse(User.shared.showsReferralSpotlight) + + // set install to 4 days ago + User.shared.install = Calendar.current.date(byAdding: .day, value: -4, to: .init())! + + User.queue.async { + let user = User() + XCTAssertTrue(user.showsReferralSpotlight) + + User.shared.hideReferralSpotlight() + User.queue.async { + let user = User() + XCTAssertFalse(user.showsReferralSpotlight) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } + + func testShowsInactiveTabsTooltip() { + let expect = expectation(description: "") + XCTAssertTrue(User.shared.showsInactiveTabsTooltip) + + User.queue.async { + let user = User() + XCTAssertTrue(user.showsInactiveTabsTooltip) + + User.shared.hideInactiveTabsTooltip() + User.queue.async { + let user = User() + XCTAssertFalse(user.showsInactiveTabsTooltip) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } + + func testShowsBookmarksImportExportTooltip() { + let expect = expectation(description: "") + User.shared.state = [:] + + XCTAssertTrue(User.shared.showsBookmarksImportExportTooltip) + + User.queue.async { + let user = User() + XCTAssertTrue(user.showsBookmarksImportExportTooltip) + + User.shared.hideBookmarksImportExportTooltip() + User.queue.async { + let user = User() + XCTAssertFalse(user.showsBookmarksImportExportTooltip) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } + + func testCookieCounterConsent() { + let expect = expectation(description: "") + User.queue.async { + let user = User() + XCTAssertNil(user.cookieConsentValue) + User.shared.cookieConsentValue = "eamp" + User.queue.async { + let user = User() + XCTAssertEqual("eamp", user.cookieConsentValue) + expect.fulfill() + } + } + waitForExpectations(timeout: 1) + } + + func testSearchSettingChangeNotifiaction() { + let expect = expectation(description: "") + var count = 0 + + NotificationCenter.default.addObserver(forName: .searchSettingsChanged, object: nil, queue: .main) { _ in + + count += 1 + + if count == 4 { + expect.fulfill() + } + } + + User.shared.personalized = !User.shared.personalized + User.shared.marketCode = .en_ww + User.shared.autoComplete = !User.shared.autoComplete + User.shared.adultFilter = .off + + wait(for: [expect], timeout: 1) + } + + func testAnalyticsUserState() { + let expect = expectation(description: "") + User.shared.analyticsUserState = User.AnalyticsStateContext() + User.queue.async { + let user = User() + XCTAssertEqual(User.PushNotificationState.notDetermined, user.analyticsUserState.pushNotificationState) + expect.fulfill() + } + waitForExpectations(timeout: 1) + } +} diff --git a/EcosiaTests/Core/Versions/User5_3.swift b/EcosiaTests/Core/Versions/User5_3.swift new file mode 100644 index 000000000000..118b18ca651e --- /dev/null +++ b/EcosiaTests/Core/Versions/User5_3.swift @@ -0,0 +1,27 @@ +import Foundation +@testable import Ecosia + +struct User5_3: Codable { + var install: Date? + var news: Date? + var analyticsId = UUID() + var marketCode = Local.make(for: .current) + var adultFilter = AdultFilter.moderate + var autoComplete = true + var firstTime = true + var personalized: Bool? = false + var topSites: Bool? = true + var migrated: Bool? = false + var id: String? + var treeCount = 0 + var state = [String: String]() + + init() { + install = .init() + } + + enum Key: String { + case + welcomeScreen + } +} diff --git a/EcosiaTests/EcosiaHomeViewModelTests.swift b/EcosiaTests/EcosiaHomeViewModelTests.swift index 0c372c5048af..9af8758a7bdb 100644 --- a/EcosiaTests/EcosiaHomeViewModelTests.swift +++ b/EcosiaTests/EcosiaHomeViewModelTests.swift @@ -5,7 +5,7 @@ import XCTest @testable import Client -@testable import Core +@testable import Ecosia class EcosiaHomeViewModelTests: XCTestCase { diff --git a/EcosiaTests/EcosiaInstallTypeTests.swift b/EcosiaTests/EcosiaInstallTypeTests.swift index 08d55e9fc26d..91e14a271c7c 100644 --- a/EcosiaTests/EcosiaInstallTypeTests.swift +++ b/EcosiaTests/EcosiaInstallTypeTests.swift @@ -4,7 +4,7 @@ import XCTest @testable import Client -@testable import Core +@testable import Ecosia final class EcosiaInstallTypeTests: XCTestCase { diff --git a/EcosiaTests/EcosiaNTPTooltipHighlightTests.swift b/EcosiaTests/EcosiaNTPTooltipHighlightTests.swift index b7746ee5cb6b..4eb148a1742f 100644 --- a/EcosiaTests/EcosiaNTPTooltipHighlightTests.swift +++ b/EcosiaTests/EcosiaNTPTooltipHighlightTests.swift @@ -3,12 +3,12 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import XCTest -@testable import Core +@testable import Ecosia @testable import Client class EcosiaNTPTooltipHighlightTests: XCTestCase { - var user: Core.User! + var user: Ecosia.User! override func setUpWithError() throws { try? FileManager().removeItem(at: FileManager.user) diff --git a/EcosiaTests/EcosiaOverlayModeManagerTests.swift b/EcosiaTests/EcosiaOverlayModeManagerTests.swift index 4135b33f7f84..82d2c47f46e6 100644 --- a/EcosiaTests/EcosiaOverlayModeManagerTests.swift +++ b/EcosiaTests/EcosiaOverlayModeManagerTests.swift @@ -3,7 +3,6 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import XCTest - @testable import Client final class EcosiaOverlayModeManagerTests: XCTestCase { diff --git a/EcosiaTests/IntegrationTests/AppDelegateFeatureManagementIntegrationTests.swift b/EcosiaTests/IntegrationTests/AppDelegateFeatureManagementIntegrationTests.swift index 367793555a87..d10f86e8e95e 100644 --- a/EcosiaTests/IntegrationTests/AppDelegateFeatureManagementIntegrationTests.swift +++ b/EcosiaTests/IntegrationTests/AppDelegateFeatureManagementIntegrationTests.swift @@ -4,7 +4,7 @@ import XCTest @testable import Client -@testable import Core +@testable import Ecosia final class AppDelegateFeatureManagementIntegrationTests: XCTestCase { var appDelegate: AppDelegate! diff --git a/EcosiaTests/Mocks/MockAppVersionInfoProvider.swift b/EcosiaTests/Mocks/MockAppVersionInfoProvider.swift index 6ad81ed7d533..4743c9f90e10 100644 --- a/EcosiaTests/Mocks/MockAppVersionInfoProvider.swift +++ b/EcosiaTests/Mocks/MockAppVersionInfoProvider.swift @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ @testable import Client +@testable import Ecosia struct MockAppVersionInfoProvider: AppVersionInfoProvider { diff --git a/EcosiaTests/Mocks/MockNewsModel.swift b/EcosiaTests/Mocks/MockNewsModel.swift index c1218b5c5489..24ead6d0516a 100644 --- a/EcosiaTests/Mocks/MockNewsModel.swift +++ b/EcosiaTests/Mocks/MockNewsModel.swift @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -import Core +@testable import Ecosia func createMockNewsModel() throws -> NewsModel? { let currentTimestamp = Date().timeIntervalSince1970 diff --git a/EcosiaTests/Mocks/MockUNNotificationSettings.swift b/EcosiaTests/Mocks/MockUNNotificationSettings.swift index 1a424a4f8eed..530cf3a65400 100644 --- a/EcosiaTests/Mocks/MockUNNotificationSettings.swift +++ b/EcosiaTests/Mocks/MockUNNotificationSettings.swift @@ -4,6 +4,7 @@ import Foundation @testable import Client +@testable import Ecosia struct MockUNNotificationSettings: AnalyticsUNNotificationSettingsProtocol { var authorizationStatus: UNAuthorizationStatus diff --git a/EcosiaTests/SnapshotTests/LocalizationOverrideTestingBundle.swift b/EcosiaTests/SnapshotTests/LocalizationOverrideTestingBundle.swift index 3d0482ccfbcf..4aa21ed42101 100644 --- a/EcosiaTests/SnapshotTests/LocalizationOverrideTestingBundle.swift +++ b/EcosiaTests/SnapshotTests/LocalizationOverrideTestingBundle.swift @@ -4,6 +4,7 @@ import Foundation import ObjectiveC.runtime +import Ecosia var overriddenLocaleIdentifier: String = "" @@ -11,7 +12,7 @@ final class LocalizationOverrideTestingBundle: Bundle { override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String { // You can dynamically choose the table based on a stored locale or use a predetermined table - guard let path = Bundle.main.path(forResource: overriddenLocaleIdentifier, ofType: "lproj"), + guard let path = Bundle.ecosia.path(forResource: overriddenLocaleIdentifier, ofType: "lproj"), let bundle = Bundle(path: path) else { return super.localizedString(forKey: key, value: value, table: tableName) } diff --git a/EcosiaTests/SnapshotTests/NTP/NTPComponentTests.swift b/EcosiaTests/SnapshotTests/NTP/NTPComponentTests.swift index e2bc87fc40aa..83d7f57fedd2 100644 --- a/EcosiaTests/SnapshotTests/NTP/NTPComponentTests.swift +++ b/EcosiaTests/SnapshotTests/NTP/NTPComponentTests.swift @@ -6,7 +6,7 @@ import SnapshotTesting import XCTest import Common import Shared -import Core +import Ecosia import MozillaAppServices @testable import Client diff --git a/EcosiaTests/SnapshotTests/NTP/NTPTests.swift b/EcosiaTests/SnapshotTests/NTP/NTPTests.swift index c50ce90d95a1..ad58e42c087a 100644 --- a/EcosiaTests/SnapshotTests/NTP/NTPTests.swift +++ b/EcosiaTests/SnapshotTests/NTP/NTPTests.swift @@ -4,7 +4,7 @@ import SnapshotTesting import XCTest -import Core +import Ecosia import Common @testable import Client diff --git a/EcosiaTests/SnapshotTests/EcosiaSnapshotTests.xctestplan b/EcosiaTests/SnapshotTests/SnapshotTests.xctestplan similarity index 100% rename from EcosiaTests/SnapshotTests/EcosiaSnapshotTests.xctestplan rename to EcosiaTests/SnapshotTests/SnapshotTests.xctestplan diff --git a/EcosiaTests/UnitTest.xctestplan b/EcosiaTests/UnitTest.xctestplan new file mode 100644 index 000000000000..1b63099402b6 --- /dev/null +++ b/EcosiaTests/UnitTest.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "9989C302-1D94-43E1-BC80-01AAA064C73B", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:Client.xcodeproj", + "identifier" : "122935722CE78D0A00EC1297", + "name" : "EcosiaTests" + } + } + ], + "version" : 1 +} diff --git a/EcosiaTests/VersionTests.swift b/EcosiaTests/VersionTests.swift index e1b997b66024..3afcf5794a62 100644 --- a/EcosiaTests/VersionTests.swift +++ b/EcosiaTests/VersionTests.swift @@ -4,6 +4,7 @@ import XCTest @testable import Client +@testable import Ecosia final class VersionTests: XCTestCase { @@ -100,7 +101,7 @@ extension VersionTests { // Setup let version = Version("8.3.0")! let appVersionInfoProvider = MockAppVersionInfoProvider(mockedAppVersion: version.description) - let dataProvider = WhatsNewLocalDataProvider() + let dataProvider = WhatsNewLocalDataProvider(versionProvider: appVersionInfoProvider) // Given: An initial version of 8.3.0 and a "toVersion" of 8.3.0 Version.updateFromCurrent(forKey: Self.appVersionUpdateTestKey, @@ -119,7 +120,7 @@ extension VersionTests { let fromVersion = Version("8.3.0")! let toVersion = Version("8.3.1")! let appVersionInfoProvider = MockAppVersionInfoProvider(mockedAppVersion: toVersion.description) - let dataProvider = WhatsNewLocalDataProvider() + let dataProvider = WhatsNewLocalDataProvider(versionProvider: appVersionInfoProvider) // Given: An initial version of 8.3.0 and a "toVersion" of 8.3.1 Version.updateFromCurrent(forKey: Self.appVersionUpdateTestKey, provider: MockAppVersionInfoProvider(mockedAppVersion: fromVersion.description)) diff --git a/EcosiaTests/WhatsNewLocalDataProviderTests.swift b/EcosiaTests/WhatsNewLocalDataProviderTests.swift index 434d48f0c8aa..27ac7ccf1740 100644 --- a/EcosiaTests/WhatsNewLocalDataProviderTests.swift +++ b/EcosiaTests/WhatsNewLocalDataProviderTests.swift @@ -4,7 +4,7 @@ import XCTest @testable import Client -@testable import Core +@testable import Ecosia // This tests are dependant on WhatsNewLocalDataProvider.whatsNewItems hardcoded implementation final class WhatsNewLocalDataProviderTests: XCTestCase { diff --git a/README.md b/README.md index e88af1b97216..2d9a904aa97a 100644 --- a/README.md +++ b/README.md @@ -246,4 +246,4 @@ We built our snapshot testing setup with `SnapshotTestHelper` to streamline UI c - **Comparison**: We capture snapshots of the UI and compare them to reference images to spot any unintended changes. -More details [here](SNAPSHOT_TESTING_WIKI.md) +More details [here](SNAPSHOT_TESTING_WIKI.md) \ No newline at end of file diff --git a/Shared/UserAgent.swift b/Shared/UserAgent.swift index 269e2f692ae5..1b700ae71801 100644 --- a/Shared/UserAgent.swift +++ b/Shared/UserAgent.swift @@ -5,8 +5,7 @@ import Common import WebKit import UIKit -// Ecosia: importing Core -import Core +import Ecosia open class UserAgent { public static let uaBitSafari = "Safari/605.1.15" diff --git a/Storage/DefaultSuggestedSites.swift b/Storage/DefaultSuggestedSites.swift index 0b256ae8230e..d2dd96abf479 100644 --- a/Storage/DefaultSuggestedSites.swift +++ b/Storage/DefaultSuggestedSites.swift @@ -3,8 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/ import Foundation -// Ecosia: Core import -import Core +import Ecosia open class DefaultSuggestedSites { /* Ecosia: Replace Default Suggested Sites diff --git a/perform_snapshot_tests.sh b/perform_snapshot_tests.sh index 180f2b9eb96d..f3bc995f9155 100755 --- a/perform_snapshot_tests.sh +++ b/perform_snapshot_tests.sh @@ -409,9 +409,15 @@ for device_set_key in "${!device_set_tests[@]}"; do echo " - Running xcodebuild Command: $xcodebuild_cmd" + # Disable 'set -e' temporarily to ensure the script continues even if the test fails + set +e + # Run the xcodebuild command eval $xcodebuild_cmd + # Re-enable 'set -e' + set -e + # Increment xcodebuild execution counter xcodebuild_count=$((xcodebuild_count + 1)) done @@ -446,14 +452,7 @@ if [ "${#xcresult_files[@]}" -eq 0 ]; then exit 1 fi -# Merge the xcresult files +# Merge the xcresult files into one $xcresulttool_path merge "${xcresult_files[@]}" --output-path "$combined_result_path" -echo "Combined xcresult created at: $combined_result_path" - -# ================================ -# Final Output -# ================================ - -# Print the total number of times xcodebuild was executed -echo "Total xcodebuild commands executed: $xcodebuild_count" \ No newline at end of file +echo "Combined xcresult created at: $combined_result_path" \ No newline at end of file