diff --git a/Gemfile b/Gemfile index eeea97b170..dbf5d8b934 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" ruby "2.6.5" -gem "fastlane", "2.195.0" +gem "fastlane", "2.196.0" gem "cocoapods", "1.11.2" gem "generamba", "1.5.0" diff --git a/Gemfile.lock b/Gemfile.lock index bc67a209c5..0191e27c5c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,13 +17,13 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.507.0) + aws-partitions (1.515.0) aws-sdk-core (3.121.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.48.0) + aws-sdk-kms (1.49.0) aws-sdk-core (~> 3, >= 3.120.0) aws-sigv4 (~> 1.1) aws-sdk-s3 (1.103.0) @@ -82,11 +82,11 @@ GEM domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) - emoji_regex (3.2.2) + emoji_regex (3.2.3) escape (0.0.4) - ethon (0.14.0) + ethon (0.15.0) ffi (>= 1.15.0) - excon (0.85.0) + excon (0.87.0) faraday (1.8.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -109,10 +109,10 @@ GEM faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday_middleware (1.1.0) + faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.5) - fastlane (2.195.0) + fastlane (2.196.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -164,7 +164,7 @@ GEM xcodeproj (>= 1.5.0, < 2.0.0) gh_inspector (1.1.3) git (1.2.9.1) - google-apis-androidpublisher_v3 (0.11.0) + google-apis-androidpublisher_v3 (0.12.0) google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.1) addressable (~> 2.5, >= 2.5.1) @@ -209,12 +209,12 @@ GEM i18n (1.8.10) concurrent-ruby (~> 1.0) jmespath (1.4.0) - json (2.5.1) - jwt (2.2.3) + json (2.6.0) + jwt (2.3.0) liquid (4.0.0) memoist (0.16.2) mini_magick (4.11.0) - mini_mime (1.1.1) + mini_mime (1.1.2) minitest (5.14.4) molinillo (0.8.0) multi_json (1.15.0) @@ -283,7 +283,7 @@ PLATFORMS DEPENDENCIES cocoapods (= 1.11.2) - fastlane (= 2.195.0) + fastlane (= 2.196.0) fastlane-plugin-firebase_app_distribution generamba (= 1.5.0) diff --git a/Podfile b/Podfile index 0c68f4338d..2318256884 100644 --- a/Podfile +++ b/Podfile @@ -86,7 +86,7 @@ end def testing_pods pod 'Quick', '4.0.0' pod 'Nimble', '9.2.1' - pod 'Mockingjay', '3.0.0-alpha.1' + pod 'Mockingjay', :git => 'https://github.com/kylef/Mockingjay.git', :branch => 'master' end target 'Stepic' do diff --git a/Podfile.lock b/Podfile.lock index 05219c3b8a..46fe1e634f 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -241,7 +241,7 @@ DEPENDENCIES: - Kanna (= 5.2.7) - Koloda (= 5.0.1) - lottie-ios (= 3.2.3) - - Mockingjay (= 3.0.0-alpha.1) + - Mockingjay (from `https://github.com/kylef/Mockingjay.git`, branch `master`) - Nimble (= 9.2.1) - Nuke (= 9.5.0) - PanModal (from `https://github.com/ivan-magda/PanModal.git`, branch `remove-presenting-appearance-transitions`) @@ -298,7 +298,6 @@ SPEC REPOS: - Kanna - Koloda - lottie-ios - - Mockingjay - nanopb - Nimble - Nuke @@ -329,6 +328,9 @@ EXTERNAL SOURCES: Highlightr: :git: https://github.com/ivan-magda/Highlightr.git :tag: v2.1.3 + Mockingjay: + :branch: master + :git: https://github.com/kylef/Mockingjay.git PanModal: :branch: remove-presenting-appearance-transitions :git: https://github.com/ivan-magda/PanModal.git @@ -346,6 +348,9 @@ CHECKOUT OPTIONS: Highlightr: :git: https://github.com/ivan-magda/Highlightr.git :tag: v2.1.3 + Mockingjay: + :commit: 291c52cb6a5d4dfb3094f3851333f1ddbf350ff1 + :git: https://github.com/kylef/Mockingjay.git PanModal: :commit: 32fc8b5868b0254a2025c9c01b24c0e4b3fe537d :git: https://github.com/ivan-magda/PanModal.git @@ -393,7 +398,7 @@ SPEC CHECKSUMS: Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234 Koloda: d07b9199a383abc5898b62aa945a599f5e7c0c4b lottie-ios: c058aeafa76daa4cf64d773554bccc8385d0150e - Mockingjay: 0f7c5aa49c7f1b95621cee3c79b557141f5a225c + Mockingjay: 97656c6f59879923976a0a52ef09da45756cca82 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 Nimble: e7e615c0335ee4bf5b0d786685451e62746117d5 Nuke: 6f400a4ea957e09149ec335a3c6acdcc814d89e4 @@ -420,6 +425,6 @@ SPEC CHECKSUMS: VK-ios-sdk: 5bcf00a2014a7323f98db9328b603d4f96635caa YandexMobileMetrica: 9e713c16bb6aca0ba63b84c8d7b8b86d32f4ecc4 -PODFILE CHECKSUM: 4ca3c532fc5e939bc8772b61325b4b84a1fa0fef +PODFILE CHECKSUM: 9f45485f08220cf29dd4c05d283f66e467fe342b COCOAPODS: 1.11.2 diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index d0dcbcf44f..5b4fbb15f4 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 041631F190D4D0F3F37E101A /* CourseRevenueAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6A4070C7AACD5DDD9D7C13 /* CourseRevenueAssembly.swift */; }; 062AB834A48F37524A84D976 /* NewProfileInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DE4213EB9F9BB5C2C25E78 /* NewProfileInteractor.swift */; }; 0791DFE4B73F0C765044006C /* FillBlanksQuizInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F6D07472E32C2DC0EE070CE /* FillBlanksQuizInteractor.swift */; }; + 07D32F4EF9D7CED0373404C6 /* CourseInfoPurchaseModalInputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79BE29668E3FDAF5A383D8CD /* CourseInfoPurchaseModalInputProtocol.swift */; }; 07D41EBCF7EE589FF01C3457 /* NewProfileCertificatesDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 009A8D41353D2ECAACAB303D /* NewProfileCertificatesDataFlow.swift */; }; 07E09A9AA17EC2FDCFA5616C /* CourseSearchPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E429497D178104E4DF68B16C /* CourseSearchPresenter.swift */; }; 07F30A0440E03D9E51762E60 /* SubmissionsFilterOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1335D9DD2A130563C0EA06D6 /* SubmissionsFilterOutputProtocol.swift */; }; @@ -227,7 +228,7 @@ 089877A0214047650065DFA2 /* SplitTestGroupProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0898779D214047640065DFA2 /* SplitTestGroupProtocol.swift */; }; 089877A1214047650065DFA2 /* SplitTestProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0898779E214047640065DFA2 /* SplitTestProtocol.swift */; }; 089877A2214047650065DFA2 /* SplitTestingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0898779F214047650065DFA2 /* SplitTestingService.swift */; }; - 089877A6214047BC0065DFA2 /* AnalyticsUserPropertiesServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089877A4214047BB0065DFA2 /* AnalyticsUserPropertiesServiceProtocol.swift */; }; + 089877A6214047BC0065DFA2 /* ABAnalyticsServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089877A4214047BB0065DFA2 /* ABAnalyticsServiceProtocol.swift */; }; 089877AD214047EE0065DFA2 /* Numbers+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089877A8214047EC0065DFA2 /* Numbers+Random.swift */; }; 089877B0214047EE0065DFA2 /* UserDefaults+StorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089877AB214047ED0065DFA2 /* UserDefaults+StorageServiceProtocol.swift */; }; 089877B221404CF10065DFA2 /* StringStorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089877B121404CF00065DFA2 /* StringStorageServiceProtocol.swift */; }; @@ -429,6 +430,7 @@ 2C0B42C2234CD17C00B03EA1 /* CustomMenuBlockTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2C0B42C0234CD17C00B03EA1 /* CustomMenuBlockTableViewCell.xib */; }; 2C0B771D25480A7500BA474D /* CourseListFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0B771C25480A7500BA474D /* CourseListFilterViewModel.swift */; }; 2C0C68FC247DBA6200B950F6 /* IAPReceiptValidationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0C68FB247DBA6200B950F6 /* IAPReceiptValidationService.swift */; }; + 2C0D345E271D687C00C51EEB /* CourseInfoPurchaseModalPromoCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0D345D271D687C00C51EEB /* CourseInfoPurchaseModalPromoCodeView.swift */; }; 2C0F80BA25C9B80C006E9233 /* DiscountAppearanceSplitTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0F80B925C9B80C006E9233 /* DiscountAppearanceSplitTest.swift */; }; 2C0F80C525C9BB5A006E9233 /* PromoPriceButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0F80C425C9BB5A006E9233 /* PromoPriceButton.swift */; }; 2C0F80D925C9C861006E9233 /* TransparentPromoPriceButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0F80D825C9C861006E9233 /* TransparentPromoPriceButton.swift */; }; @@ -584,6 +586,7 @@ 2C434D3725B6FD8200854D6F /* NSDateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A3A9CD1BD5A14D0032C36E /* NSDateExtensions.swift */; }; 2C434D4525B7367500854D6F /* WidgetContentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C434D4425B7367500854D6F /* WidgetContentProvider.swift */; }; 2C434D4E25B74D1A00854D6F /* Array+Reordering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98BC02EC89CF78E0FC7A6 /* Array+Reordering.swift */; }; + 2C4391AA271994CC000770AE /* CourseInfoPurchaseModalHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4391A9271994CB000770AE /* CourseInfoPurchaseModalHeaderView.swift */; }; 2C4436B321356D960084489C /* CourseSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088FD8161FB242B3008A2953 /* CourseSubscriber.swift */; }; 2C453398204D46E90061342A /* PinsMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C453397204D46E90061342A /* PinsMap.swift */; }; 2C45339A204D5DEE0061342A /* PinsMapSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C453399204D5DEE0061342A /* PinsMapSpec.swift */; }; @@ -591,6 +594,7 @@ 2C47914626BBD17400920ED2 /* InstructionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C47914526BBD17400920ED2 /* InstructionType.swift */; }; 2C47A164206284B1003E87EC /* NotificationRequestAlertContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C47A163206284B1003E87EC /* NotificationRequestAlertContext.swift */; }; 2C47E6B72608EB8900732642 /* CatalogBlockItemModuleFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C47E6B62608EB8900732642 /* CatalogBlockItemModuleFactory.swift */; }; + 2C4841262715ED760091FE20 /* CourseInfoPurchaseModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4841252715ED760091FE20 /* CourseInfoPurchaseModalViewModel.swift */; }; 2C48BA0725727822009103B2 /* AuthorsCourseListCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48BA0625727822009103B2 /* AuthorsCourseListCollectionViewDataSource.swift */; }; 2C48BA0F25727885009103B2 /* AuthorsCourseListCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48BA0E25727885009103B2 /* AuthorsCourseListCollectionViewDelegate.swift */; }; 2C48BA1F25727CC4009103B2 /* AuthorsCourseListCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C48BA1E25727CC4009103B2 /* AuthorsCourseListCollectionViewCell.swift */; }; @@ -945,6 +949,7 @@ 2CA9D98F2012334B007AA743 /* nouns_m.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2C35C4CB1F4DA462002F3BF4 /* nouns_m.plist */; }; 2CABB842267B929C0070E5E7 /* CourseWidgetPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CABB841267B929C0070E5E7 /* CourseWidgetPriceView.swift */; }; 2CAC7D5624EF56A600942D14 /* FillBlanksQuizInputContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAC7D5524EF56A600942D14 /* FillBlanksQuizInputContainerView.swift */; }; + 2CACFB222719B916006E8FB3 /* CourseInfoPurchaseModalCourseCoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CACFB212719B916006E8FB3 /* CourseInfoPurchaseModalCourseCoverView.swift */; }; 2CAD40412600C305008EFDE5 /* StoryPartFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAD40402600C305008EFDE5 /* StoryPartFactory.swift */; }; 2CAD405C2600D10D008EFDE5 /* story-template-text.json in Resources */ = {isa = PBXBuildFile; fileRef = 2CAD405B2600D10D008EFDE5 /* story-template-text.json */; }; 2CAD40682600D1D7008EFDE5 /* StoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAD40672600D1D7008EFDE5 /* StoryTests.swift */; }; @@ -1012,7 +1017,12 @@ 2CBC5AFD2682483D0000F2D1 /* CourseRevenueTabPurchasesCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBC5AFC2682483D0000F2D1 /* CourseRevenueTabPurchasesCellView.swift */; }; 2CBCBD4A20D1AAFC000B5732 /* AchievementsListTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBCBD4820D1AAFC000B5732 /* AchievementsListTableViewCell.swift */; }; 2CBCBD4B20D1AAFC000B5732 /* AchievementsListTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CBCBD4920D1AAFC000B5732 /* AchievementsListTableViewCell.xib */; }; + 2CBD17E9271EBE41007B9CDB /* CourseInfoPurchaseModalPromoCodeRightDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBD17E8271EBE41007B9CDB /* CourseInfoPurchaseModalPromoCodeRightDetailView.swift */; }; 2CBD855C201799B700E14F83 /* AdaptiveRatingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBD855B201799B700E14F83 /* AdaptiveRatingsViewController.swift */; }; + 2CBEAACE271EFA9A00B52D2C /* CourseInfoPurchaseModalDisclaimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBEAACD271EFA9A00B52D2C /* CourseInfoPurchaseModalDisclaimerView.swift */; }; + 2CBEAAD0271F0B8B00B52D2C /* CourseInfoPurchaseModalActionButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBEAACF271F0B8B00B52D2C /* CourseInfoPurchaseModalActionButtonsView.swift */; }; + 2CBEAAD3271F0DB500B52D2C /* CourseInfoPurchaseModalActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBEAAD2271F0DB500B52D2C /* CourseInfoPurchaseModalActionButton.swift */; }; + 2CBEAAD5271F1D4D00B52D2C /* CoursePurchaseFlowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBEAAD4271F1D4D00B52D2C /* CoursePurchaseFlowType.swift */; }; 2CC0754720177A2E004A6005 /* AdaptiveStatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC0754620177A2E004A6005 /* AdaptiveStatsViewController.swift */; }; 2CC16BA923875DE30000EF36 /* DiscussionsSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC16BA823875DE30000EF36 /* DiscussionsSkeletonView.swift */; }; 2CC276FE23EB98CE00E88D6E /* UIBarButtonItem+StepikBarButtonItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC276FD23EB98CE00E88D6E /* UIBarButtonItem+StepikBarButtonItems.swift */; }; @@ -1025,6 +1035,8 @@ 2CC3519C1F6837B4004255B6 /* RegistrationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3519B1F6837B4004255B6 /* RegistrationViewController.swift */; }; 2CC3519D1F683E7C004255B6 /* AuthNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D674921D78B46900B60963 /* AuthNavigationViewController.swift */; }; 2CC38AB025750A5700BE3826 /* AuthorsCourseListWidgetRatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC38AAF25750A5700BE3826 /* AuthorsCourseListWidgetRatingView.swift */; }; + 2CC3C7EE2719631900E72F6F /* DictionaryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3C7ED2719631900E72F6F /* DictionaryExtensions.swift */; }; + 2CC3C7F0271963AF00E72F6F /* DictionaryExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC3C7EF271963AF00E72F6F /* DictionaryExtensionsTests.swift */; }; 2CC4FD5C2664CBA000A33178 /* UserCoursesReviewsPossibleReviewCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC4FD5B2664CBA000A33178 /* UserCoursesReviewsPossibleReviewCellView.swift */; }; 2CC4FD5F2664EB2D00A33178 /* UserCoursesReviewsTableSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC4FD5E2664EB2D00A33178 /* UserCoursesReviewsTableSectionView.swift */; }; 2CC5AA6C242A34C500C09F94 /* RecommendationsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC5AA6B242A34C500C09F94 /* RecommendationsAPI.swift */; }; @@ -1086,6 +1098,8 @@ 2CDBCCCF23EB777E005D2370 /* SubmissionURLProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDBCCCE23EB777E005D2370 /* SubmissionURLProvider.swift */; }; 2CDC9EFA24E4FD0D00916BAE /* CourseInfoTryForFreeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDC9EF924E4FD0D00916BAE /* CourseInfoTryForFreeButton.swift */; }; 2CDCDD19268B5CDE002A4889 /* CourseInfoTabReviewsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDCDD18268B5CDE002A4889 /* CourseInfoTabReviewsSummaryView.swift */; }; + 2CDD3FDC2715F6D30029AF40 /* CoursesRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDD3FDB2715F6D30029AF40 /* CoursesRepository.swift */; }; + 2CDD3FDE2715F86D0029AF40 /* DataFetchPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDD3FDD2715F86D0029AF40 /* DataFetchPolicy.swift */; }; 2CDE82E2221D5CCB00C41887 /* highlight.js in Resources */ = {isa = PBXBuildFile; fileRef = 2CDE82E1221D5CCB00C41887 /* highlight.js */; }; 2CE02A772176649F009C633C /* UserNotificationsCenterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE02A762176649F009C633C /* UserNotificationsCenterDelegate.swift */; }; 2CE05969268A1DCB00369E59 /* CourseRevenueTabPurchasesCellSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CE05968268A1DCB00369E59 /* CourseRevenueTabPurchasesCellSkeletonView.swift */; }; @@ -1202,6 +1216,7 @@ 2CFFC6212681EBA3005D3082 /* CourseRevenueIncomeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFFC6202681EBA3005D3082 /* CourseRevenueIncomeItemView.swift */; }; 2CFFC6232681EBB3005D3082 /* CourseRevenueIncomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFFC6222681EBB3005D3082 /* CourseRevenueIncomeView.swift */; }; 2CFFC6262681F1CE005D3082 /* ExpandContentControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CFFC6252681F1CE005D3082 /* ExpandContentControl.swift */; }; + 2FFEE41B5F3C02CDD36F0777 /* CourseInfoPurchaseModalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 758AD88FFDE3FCD437D26C47 /* CourseInfoPurchaseModalProvider.swift */; }; 314618ACBB0339BB33E8F48E /* StepQuizReviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D20EE766983EF4FCEB8DA1C /* StepQuizReviewViewController.swift */; }; 3203AD6A1594995EDE114EA0 /* Pods_StepicTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 938533EAAD61D57EF139C60C /* Pods_StepicTests.framework */; }; 3547E584AFEE09996EB90808 /* UserCoursesReviewsOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A64AF153016C7406F9244FD9 /* UserCoursesReviewsOutputProtocol.swift */; }; @@ -1233,6 +1248,7 @@ 544AC96D1208A33D1B86B67B /* DebugMenuPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABCEAAA2CD44CB8C3B268980 /* DebugMenuPresenter.swift */; }; 5469EC1AD797F38AC790B9E7 /* NewProfileCreatedCoursesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D375711B0841DF75C1D6C6D /* NewProfileCreatedCoursesInteractor.swift */; }; 54A790C581BEE7B51FFC1121 /* AuthorsCourseListDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F668BE1FBD15A64170DAE6 /* AuthorsCourseListDataFlow.swift */; }; + 552A685475C9262F4CF1B5B2 /* CourseInfoPurchaseModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DA7BE8123937B6574E8544D /* CourseInfoPurchaseModalView.swift */; }; 55A86C658087D6A12E5B9202 /* TableQuizDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3F263ADE90D66C1622121F5 /* TableQuizDataFlow.swift */; }; 565A05E2FFDFA1E33C95D221 /* CourseListFilterOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71B79C2BD72E891ECD68BD6A /* CourseListFilterOutputProtocol.swift */; }; 5BB0F4923200056C332A59FF /* LessonFinishedStepsPanModalOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B343CD0106AAF3E89DE7F23 /* LessonFinishedStepsPanModalOutputProtocol.swift */; }; @@ -1709,12 +1725,15 @@ 62E98FB9AFF029A44C3A1AAE /* CourseInfoTabSyllabusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98C1FE5E4C87C3199EC01 /* CourseInfoTabSyllabusProvider.swift */; }; 62E98FE1CE19810D6930DAE6 /* SubmissionsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E9845417AC50C4D61B42C6 /* SubmissionsProvider.swift */; }; 62E98FFD1EEDB775EE381221 /* CourseListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E98F968FBB3ED0E6309407 /* CourseListPresenter.swift */; }; + 6304CED128C9DCF1E60C8819 /* CourseInfoPurchaseModalDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27011EEBCB800A7B6ED6736 /* CourseInfoPurchaseModalDataFlow.swift */; }; 64AF865C5174A2A525C5FB8A /* NewProfileCreatedCoursesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5E1D17883366854D97EDB /* NewProfileCreatedCoursesPresenter.swift */; }; 64F1FC49DFF47555128FA13C /* NewProfileCertificatesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCAF185A5BB14E3BFB36948 /* NewProfileCertificatesPresenter.swift */; }; 6670CEAE1D677A5EADD37531 /* UserCoursesAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EC26997F83B299C9E3FCC5D /* UserCoursesAssembly.swift */; }; 69C6D82E51B19E566B93C25F /* CatalogBlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14EED2F7CA17040D19760C2C /* CatalogBlocksView.swift */; }; 6A10E1CBC8245445C41FB9F8 /* LessonFinishedStepsPanModalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0250C97AA6FB7D60234B25A /* LessonFinishedStepsPanModalProvider.swift */; }; + 6A182A43AE6D72146643363D /* CourseInfoPurchaseModalPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3647FD98066BDBD6F93B727F /* CourseInfoPurchaseModalPresenter.swift */; }; 6ADEFFDE33B06A7049CF8E2F /* NewProfileCertificatesAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC925F9685393FA44977549 /* NewProfileCertificatesAssembly.swift */; }; + 6B85847876C117651FE721A0 /* CourseInfoPurchaseModalAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44055B26E5385DE3398B1959 /* CourseInfoPurchaseModalAssembly.swift */; }; 6C92601BC04D155B948F85DD /* CourseSearchAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 410AC500A2080A2C5AC312AF /* CourseSearchAssembly.swift */; }; 6D3288F90505044F2DB76FA8 /* SubmissionsFilterDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8336942AE67DE9A75C7FC37 /* SubmissionsFilterDataFlow.swift */; }; 6E71905283A952608BEDF67D /* WishlistWidgetInputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD706EACE58852DD2B4DAAB5 /* WishlistWidgetInputProtocol.swift */; }; @@ -1736,8 +1755,9 @@ 7EBAD8337C302E2147741450 /* CourseBenefitDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8744553C2B3C12884F84BFD9 /* CourseBenefitDetailViewController.swift */; }; 7ED87AD657E488FBD3C0D88E /* StepikAcademyCourseListDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A25E416101A848548101C7DA /* StepikAcademyCourseListDataFlow.swift */; }; 81AADBF0C345BFDB7176221D /* WishlistWidgetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78AA738A584651A3F55AD9DB /* WishlistWidgetViewController.swift */; }; - 8489EE7725FAF13B004A85C5 /* StepicUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8489EE7625FAF13B004A85C5 /* StepicUITests.swift */; }; + 8489EE7725FAF13B004A85C5 /* UnregisteredUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8489EE7625FAF13B004A85C5 /* UnregisteredUITests.swift */; }; 8489EEE425FFBE8E004A85C5 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8489EEE325FFBE8E004A85C5 /* Common.swift */; }; + 84FA5057262F204400603346 /* LoginUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FA5056262F204400603346 /* LoginUITests.swift */; }; 85CFA575736E6C0570D48065 /* UserCoursesReviewsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3ED7422D4B76AAD73F35E6E /* UserCoursesReviewsInteractor.swift */; }; 861B96371FE1DF7F00773EDA /* CAGradientLayer+Init.swift in Sources */ = {isa = PBXBuildFile; fileRef = 861B96361FE1DF7F00773EDA /* CAGradientLayer+Init.swift */; }; 8622056B2055561F00F14255 /* PinsMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8622056A2055561F00F14255 /* PinsMapView.swift */; }; @@ -1807,8 +1827,10 @@ C56B3693FD36033B00C6AEB7 /* LessonFinishedStepsPanModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7B8028216D0178B5F2E2594 /* LessonFinishedStepsPanModalViewController.swift */; }; C6BC3AA52225E047D18CBB8C /* CourseRevenueTabPurchasesInputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F15109916590EB196992DAC /* CourseRevenueTabPurchasesInputProtocol.swift */; }; C6CF7E954A942E8ADC3FE8A2 /* CourseBenefitDetailAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CFC3F756271B77E39E1880B /* CourseBenefitDetailAssembly.swift */; }; + C739F25ABC1F1E42F9E1DE78 /* CourseInfoPurchaseModalOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E5B29B62F5FD9D6BCCE5C0 /* CourseInfoPurchaseModalOutputProtocol.swift */; }; C95BEF69AB5100E263ED66F6 /* DownloadARQuickLookAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = B259C3826C752171E0305872 /* DownloadARQuickLookAssembly.swift */; }; C9BDAEBEA7114EB494288E48 /* StepikAcademyCourseListOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D850EA3EA4ED70B2E50B3650 /* StepikAcademyCourseListOutputProtocol.swift */; }; + C9F3B231FE41885E9A267848 /* CourseInfoPurchaseModalInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE9652E5F5D70BCE63C65FF /* CourseInfoPurchaseModalInteractor.swift */; }; CB429BDBAF7772B836FCA675 /* NewProfileStreakNotificationsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6ED5894DADC5B17310B92 /* NewProfileStreakNotificationsPresenter.swift */; }; CC059E3EAF336C083C5FE2C9 /* CourseInfoTabNewsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002BF529815F3B811257A956 /* CourseInfoTabNewsAssembly.swift */; }; CC81627A3343CB1F28B98772 /* StepikAcademyCourseListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB7D664C83C2A5490B7145E /* StepikAcademyCourseListInteractor.swift */; }; @@ -1855,6 +1877,7 @@ F97BB185088B9D4BDC7CF147 /* CourseListFilterAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE3F54FF0EABDFC0589465 /* CourseListFilterAssembly.swift */; }; FBB6AC66F23F6493B8F5E225 /* NewProfileAchievementsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BB507BCF6F48523222D6D3 /* NewProfileAchievementsPresenter.swift */; }; FC068C7F8C2A511B54EDA4AE /* CourseRevenuePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ECDC2FDC117ECB927EFEF83 /* CourseRevenuePresenter.swift */; }; + FE177755A3F7B06F53EB999B /* CourseInfoPurchaseModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92F464AFB76FCA89A3B4CE1 /* CourseInfoPurchaseModalViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2154,7 +2177,7 @@ 0898779D214047640065DFA2 /* SplitTestGroupProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplitTestGroupProtocol.swift; sourceTree = ""; }; 0898779E214047640065DFA2 /* SplitTestProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplitTestProtocol.swift; sourceTree = ""; }; 0898779F214047650065DFA2 /* SplitTestingService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplitTestingService.swift; sourceTree = ""; }; - 089877A4214047BB0065DFA2 /* AnalyticsUserPropertiesServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsUserPropertiesServiceProtocol.swift; sourceTree = ""; }; + 089877A4214047BB0065DFA2 /* ABAnalyticsServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ABAnalyticsServiceProtocol.swift; sourceTree = ""; }; 089877A8214047EC0065DFA2 /* Numbers+Random.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Numbers+Random.swift"; sourceTree = ""; }; 089877AB214047ED0065DFA2 /* UserDefaults+StorageServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+StorageServiceProtocol.swift"; sourceTree = ""; }; 089877B121404CF00065DFA2 /* StringStorageServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringStorageServiceProtocol.swift; sourceTree = ""; }; @@ -2370,6 +2393,7 @@ 2C0B42C0234CD17C00B03EA1 /* CustomMenuBlockTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CustomMenuBlockTableViewCell.xib; sourceTree = ""; }; 2C0B771C25480A7500BA474D /* CourseListFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseListFilterViewModel.swift; sourceTree = ""; }; 2C0C68FB247DBA6200B950F6 /* IAPReceiptValidationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPReceiptValidationService.swift; sourceTree = ""; }; + 2C0D345D271D687C00C51EEB /* CourseInfoPurchaseModalPromoCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalPromoCodeView.swift; sourceTree = ""; }; 2C0F80B925C9B80C006E9233 /* DiscountAppearanceSplitTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscountAppearanceSplitTest.swift; sourceTree = ""; }; 2C0F80C425C9BB5A006E9233 /* PromoPriceButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoPriceButton.swift; sourceTree = ""; }; 2C0F80D825C9C861006E9233 /* TransparentPromoPriceButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentPromoPriceButton.swift; sourceTree = ""; }; @@ -2548,6 +2572,7 @@ 2C434D2B25B6F0C400854D6F /* StepikNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepikNetworkService.swift; sourceTree = ""; }; 2C434D3125B6FC7300854D6F /* Responses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Responses.swift; sourceTree = ""; }; 2C434D4425B7367500854D6F /* WidgetContentProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetContentProvider.swift; sourceTree = ""; }; + 2C4391A9271994CB000770AE /* CourseInfoPurchaseModalHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalHeaderView.swift; sourceTree = ""; }; 2C453397204D46E90061342A /* PinsMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinsMap.swift; sourceTree = ""; }; 2C453399204D5DEE0061342A /* PinsMapSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinsMapSpec.swift; sourceTree = ""; }; 2C45C6EE254160A100AF24BF /* HTMLExtractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLExtractorTests.swift; sourceTree = ""; }; @@ -2555,6 +2580,7 @@ 2C47914526BBD17400920ED2 /* InstructionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionType.swift; sourceTree = ""; }; 2C47A163206284B1003E87EC /* NotificationRequestAlertContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationRequestAlertContext.swift; sourceTree = ""; }; 2C47E6B62608EB8900732642 /* CatalogBlockItemModuleFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogBlockItemModuleFactory.swift; sourceTree = ""; }; + 2C4841252715ED760091FE20 /* CourseInfoPurchaseModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalViewModel.swift; sourceTree = ""; }; 2C48BA0625727822009103B2 /* AuthorsCourseListCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorsCourseListCollectionViewDataSource.swift; sourceTree = ""; }; 2C48BA0E25727885009103B2 /* AuthorsCourseListCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorsCourseListCollectionViewDelegate.swift; sourceTree = ""; }; 2C48BA1E25727CC4009103B2 /* AuthorsCourseListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorsCourseListCollectionViewCell.swift; sourceTree = ""; }; @@ -2922,6 +2948,7 @@ 2CAB3718219EF332007A524A /* Model_course_time_to_complete_v26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_course_time_to_complete_v26.xcdatamodel; sourceTree = ""; }; 2CABB841267B929C0070E5E7 /* CourseWidgetPriceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseWidgetPriceView.swift; sourceTree = ""; }; 2CAC7D5524EF56A600942D14 /* FillBlanksQuizInputContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FillBlanksQuizInputContainerView.swift; sourceTree = ""; }; + 2CACFB212719B916006E8FB3 /* CourseInfoPurchaseModalCourseCoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalCourseCoverView.swift; sourceTree = ""; }; 2CAD40402600C305008EFDE5 /* StoryPartFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryPartFactory.swift; sourceTree = ""; }; 2CAD405B2600D10D008EFDE5 /* story-template-text.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "story-template-text.json"; sourceTree = ""; }; 2CAD40672600D1D7008EFDE5 /* StoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryTests.swift; sourceTree = ""; }; @@ -2999,8 +3026,13 @@ 2CBCBD4820D1AAFC000B5732 /* AchievementsListTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AchievementsListTableViewCell.swift; sourceTree = ""; }; 2CBCBD4920D1AAFC000B5732 /* AchievementsListTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AchievementsListTableViewCell.xib; sourceTree = ""; }; 2CBCD3A7213583EB005D10FF /* Model_CourseListRename_v25.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_CourseListRename_v25.xcdatamodel; sourceTree = ""; }; + 2CBD17E8271EBE41007B9CDB /* CourseInfoPurchaseModalPromoCodeRightDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalPromoCodeRightDetailView.swift; sourceTree = ""; }; 2CBD855B201799B700E14F83 /* AdaptiveRatingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveRatingsViewController.swift; sourceTree = ""; }; 2CBE4EE22583912800C78855 /* Model_course_list_similar_v68.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_course_list_similar_v68.xcdatamodel; sourceTree = ""; }; + 2CBEAACD271EFA9A00B52D2C /* CourseInfoPurchaseModalDisclaimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalDisclaimerView.swift; sourceTree = ""; }; + 2CBEAACF271F0B8B00B52D2C /* CourseInfoPurchaseModalActionButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalActionButtonsView.swift; sourceTree = ""; }; + 2CBEAAD2271F0DB500B52D2C /* CourseInfoPurchaseModalActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalActionButton.swift; sourceTree = ""; }; + 2CBEAAD4271F1D4D00B52D2C /* CoursePurchaseFlowType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursePurchaseFlowType.swift; sourceTree = ""; }; 2CBF593423C8A61D00C366A1 /* Model_is_certificate_issued.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_is_certificate_issued.xcdatamodel; sourceTree = ""; }; 2CC0754620177A2E004A6005 /* AdaptiveStatsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveStatsViewController.swift; sourceTree = ""; }; 2CC16BA823875DE30000EF36 /* DiscussionsSkeletonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionsSkeletonView.swift; sourceTree = ""; }; @@ -3014,6 +3046,8 @@ 2CC351991F68339A004255B6 /* AuthTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthTextField.swift; sourceTree = ""; }; 2CC3519B1F6837B4004255B6 /* RegistrationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegistrationViewController.swift; sourceTree = ""; }; 2CC38AAF25750A5700BE3826 /* AuthorsCourseListWidgetRatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorsCourseListWidgetRatingView.swift; sourceTree = ""; }; + 2CC3C7ED2719631900E72F6F /* DictionaryExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryExtensions.swift; sourceTree = ""; }; + 2CC3C7EF271963AF00E72F6F /* DictionaryExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryExtensionsTests.swift; sourceTree = ""; }; 2CC4FD5B2664CBA000A33178 /* UserCoursesReviewsPossibleReviewCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCoursesReviewsPossibleReviewCellView.swift; sourceTree = ""; }; 2CC4FD5E2664EB2D00A33178 /* UserCoursesReviewsTableSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCoursesReviewsTableSectionView.swift; sourceTree = ""; }; 2CC5AA6B242A34C500C09F94 /* RecommendationsAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecommendationsAPI.swift; sourceTree = ""; }; @@ -3079,6 +3113,8 @@ 2CDC9EF924E4FD0D00916BAE /* CourseInfoTryForFreeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTryForFreeButton.swift; sourceTree = ""; }; 2CDC9EFB24E516F800916BAE /* Model_course_preview_lesson_id_v63.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_course_preview_lesson_id_v63.xcdatamodel; sourceTree = ""; }; 2CDCDD18268B5CDE002A4889 /* CourseInfoTabReviewsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabReviewsSummaryView.swift; sourceTree = ""; }; + 2CDD3FDB2715F6D30029AF40 /* CoursesRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursesRepository.swift; sourceTree = ""; }; + 2CDD3FDD2715F86D0029AF40 /* DataFetchPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataFetchPolicy.swift; sourceTree = ""; }; 2CDE82E1221D5CCB00C41887 /* highlight.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = highlight.js; sourceTree = ""; }; 2CE02A762176649F009C633C /* UserNotificationsCenterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationsCenterDelegate.swift; sourceTree = ""; }; 2CE05968268A1DCB00369E59 /* CourseRevenueTabPurchasesCellSkeletonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseRevenueTabPurchasesCellSkeletonView.swift; sourceTree = ""; }; @@ -3209,6 +3245,7 @@ 2FA5E1D17883366854D97EDB /* NewProfileCreatedCoursesPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCreatedCoursesPresenter.swift; sourceTree = ""; }; 33074471A14A38067198824D /* CourseSearchViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseSearchViewController.swift; sourceTree = ""; }; 342AEC970102A4B3CA9FD0DC /* AuthorsCourseListOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthorsCourseListOutputProtocol.swift; sourceTree = ""; }; + 3647FD98066BDBD6F93B727F /* CourseInfoPurchaseModalPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalPresenter.swift; sourceTree = ""; }; 383E389EDEA545B7F09F70EF /* Pods-StepicTests.production release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StepicTests.production release.xcconfig"; path = "Target Support Files/Pods-StepicTests/Pods-StepicTests.production release.xcconfig"; sourceTree = ""; }; 3B343CD0106AAF3E89DE7F23 /* LessonFinishedStepsPanModalOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonFinishedStepsPanModalOutputProtocol.swift; sourceTree = ""; }; 3BCD8888462BD21ECF82C602 /* DownloadARQuickLookViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DownloadARQuickLookViewController.swift; sourceTree = ""; }; @@ -3216,6 +3253,7 @@ 3C1049108DA5FB2370275330 /* Pods-StepicTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StepicTests.release.xcconfig"; path = "Target Support Files/Pods-StepicTests/Pods-StepicTests.release.xcconfig"; sourceTree = ""; }; 3CF9B962777693E708B98356 /* NewProfileSocialProfilesView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileSocialProfilesView.swift; sourceTree = ""; }; 3D61DBD05355014717A35F04 /* LessonFinishedStepsPanModalInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonFinishedStepsPanModalInteractor.swift; sourceTree = ""; }; + 3DA7BE8123937B6574E8544D /* CourseInfoPurchaseModalView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalView.swift; sourceTree = ""; }; 3DDFFE939BC2A621E0E1FAD3 /* UserCoursesReviewsWidgetView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesReviewsWidgetView.swift; sourceTree = ""; }; 3E5C2A5CDF6CD9FA686E5D06 /* TableQuizAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TableQuizAssembly.swift; sourceTree = ""; }; 3F6D07472E32C2DC0EE070CE /* FillBlanksQuizInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FillBlanksQuizInteractor.swift; sourceTree = ""; }; @@ -3224,6 +3262,7 @@ 410AC500A2080A2C5AC312AF /* CourseSearchAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseSearchAssembly.swift; sourceTree = ""; }; 424840FC832A3CF79ACD98CF /* StepikAcademyCourseListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepikAcademyCourseListPresenter.swift; sourceTree = ""; }; 4356ADE389830A4DB65CC0D7 /* CatalogBlocksViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CatalogBlocksViewController.swift; sourceTree = ""; }; + 44055B26E5385DE3398B1959 /* CourseInfoPurchaseModalAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalAssembly.swift; sourceTree = ""; }; 44CA1414D85B7D7645957E8F /* StepikAcademyCourseListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepikAcademyCourseListViewController.swift; sourceTree = ""; }; 481132AEA7BC453EB69DA98B /* LessonFinishedDemoPanModalView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonFinishedDemoPanModalView.swift; sourceTree = ""; }; 489380E1AE5474348FB1450D /* Pods-Stepic.production debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stepic.production debug.xcconfig"; path = "Target Support Files/Pods-Stepic/Pods-Stepic.production debug.xcconfig"; sourceTree = ""; }; @@ -3735,20 +3774,24 @@ 71B79C2BD72E891ECD68BD6A /* CourseListFilterOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseListFilterOutputProtocol.swift; sourceTree = ""; }; 72D1EE8518225C9F49BE44EA /* NewProfileUserActivityView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileUserActivityView.swift; sourceTree = ""; }; 73BB1098494DA2104179C786 /* CourseInfoTabNewsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseInfoTabNewsView.swift; sourceTree = ""; }; + 74E5B29B62F5FD9D6BCCE5C0 /* CourseInfoPurchaseModalOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalOutputProtocol.swift; sourceTree = ""; }; + 758AD88FFDE3FCD437D26C47 /* CourseInfoPurchaseModalProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalProvider.swift; sourceTree = ""; }; 75A30D817521E42507A608B3 /* NewProfileCertificatesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCertificatesInteractor.swift; sourceTree = ""; }; 75B4327DE945A7D6C60553E1 /* SubmissionsFilterPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SubmissionsFilterPresenter.swift; sourceTree = ""; }; 77A061513F473CC4D8745A02 /* NewProfileSocialProfilesAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileSocialProfilesAssembly.swift; sourceTree = ""; }; 77F668BE1FBD15A64170DAE6 /* AuthorsCourseListDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthorsCourseListDataFlow.swift; sourceTree = ""; }; 78AA738A584651A3F55AD9DB /* WishlistWidgetViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WishlistWidgetViewController.swift; sourceTree = ""; }; 78B0D81FECC529158A0A2819 /* StepQuizReviewDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepQuizReviewDataFlow.swift; sourceTree = ""; }; + 79BE29668E3FDAF5A383D8CD /* CourseInfoPurchaseModalInputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalInputProtocol.swift; sourceTree = ""; }; 7B9DB0A6C0B38CE13676ED50 /* NewProfileStreakNotificationsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileStreakNotificationsInteractor.swift; sourceTree = ""; }; 7F15948AB9342436A2D3423A /* Pods-Stepic.develop release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stepic.develop release.xcconfig"; path = "Target Support Files/Pods-Stepic/Pods-Stepic.develop release.xcconfig"; sourceTree = ""; }; 812902FE72DE5F64FCC48841 /* CourseBenefitDetailView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseBenefitDetailView.swift; sourceTree = ""; }; 81AA980E65D068EB9A20F675 /* LessonFinishedStepsPanModalDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonFinishedStepsPanModalDataFlow.swift; sourceTree = ""; }; 83BC8E57E212C4D980F78B79 /* CourseRevenueTabPurchasesDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseRevenueTabPurchasesDataFlow.swift; sourceTree = ""; }; 8489EE7425FAF13A004A85C5 /* StepicUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StepicUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 8489EE7625FAF13B004A85C5 /* StepicUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepicUITests.swift; sourceTree = ""; }; + 8489EE7625FAF13B004A85C5 /* UnregisteredUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnregisteredUITests.swift; sourceTree = ""; }; 8489EEE325FFBE8E004A85C5 /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; + 84FA5056262F204400603346 /* LoginUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginUITests.swift; sourceTree = ""; }; 85424A5DCFCD1768BD36AD45 /* CourseBenefitDetailInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseBenefitDetailInteractor.swift; sourceTree = ""; }; 858D7264845A798247AE1350 /* UserCoursesReviewsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesReviewsDataFlow.swift; sourceTree = ""; }; 861B96361FE1DF7F00773EDA /* CAGradientLayer+Init.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CAGradientLayer+Init.swift"; sourceTree = ""; }; @@ -3802,6 +3845,7 @@ A7380FB54898647E5E6B699C /* StepikAcademyCourseListAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepikAcademyCourseListAssembly.swift; sourceTree = ""; }; A8AB6C9621B79FA5EA8C8D70 /* CourseRevenueInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseRevenueInteractor.swift; sourceTree = ""; }; AA0C8737382C6CDE636B766E /* CourseBenefitDetailDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseBenefitDetailDataFlow.swift; sourceTree = ""; }; + AAE9652E5F5D70BCE63C65FF /* CourseInfoPurchaseModalInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalInteractor.swift; sourceTree = ""; }; ABCEAAA2CD44CB8C3B268980 /* DebugMenuPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DebugMenuPresenter.swift; sourceTree = ""; }; AC70D0220ABA60A77FC65411 /* FillBlanksQuizDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FillBlanksQuizDataFlow.swift; sourceTree = ""; }; AD0054F69D610FEA28F620F8 /* Pods-StepicTests.production debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StepicTests.production debug.xcconfig"; path = "Target Support Files/Pods-StepicTests/Pods-StepicTests.production debug.xcconfig"; sourceTree = ""; }; @@ -3850,11 +3894,13 @@ D7FDCEA2CEBE8396085B5648 /* NewProfileCreatedCoursesView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCreatedCoursesView.swift; sourceTree = ""; }; D82EBE189B9B826DA622520E /* Pods-Stepic.develop debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Stepic.develop debug.xcconfig"; path = "Target Support Files/Pods-Stepic/Pods-Stepic.develop debug.xcconfig"; sourceTree = ""; }; D850EA3EA4ED70B2E50B3650 /* StepikAcademyCourseListOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepikAcademyCourseListOutputProtocol.swift; sourceTree = ""; }; + D92F464AFB76FCA89A3B4CE1 /* CourseInfoPurchaseModalViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalViewController.swift; sourceTree = ""; }; D967D8F1F73BA94F1C5565C3 /* CourseRevenueTabMonthlyView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseRevenueTabMonthlyView.swift; sourceTree = ""; }; D9B5E3A57F5CDE395E9306E0 /* UserCoursesReviewsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesReviewsViewController.swift; sourceTree = ""; }; DB041A813271DC2DB7168CD4 /* LessonFinishedStepsPanModalAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LessonFinishedStepsPanModalAssembly.swift; sourceTree = ""; }; DB60763DF30E81A85EF73B62 /* StepQuizReviewProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StepQuizReviewProvider.swift; sourceTree = ""; }; DD4A385A37C2127557150147 /* NewProfileCreatedCoursesOutputProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCreatedCoursesOutputProtocol.swift; sourceTree = ""; }; + E27011EEBCB800A7B6ED6736 /* CourseInfoPurchaseModalDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseInfoPurchaseModalDataFlow.swift; sourceTree = ""; }; E358732932D647EFD60F96E8 /* NewProfileSocialProfilesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileSocialProfilesInteractor.swift; sourceTree = ""; }; E3ED7422D4B76AAD73F35E6E /* UserCoursesReviewsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserCoursesReviewsInteractor.swift; sourceTree = ""; }; E429497D178104E4DF68B16C /* CourseSearchPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CourseSearchPresenter.swift; sourceTree = ""; }; @@ -4133,8 +4179,8 @@ 2C0176C32188A49100DDB9D0 /* Analytics */ = { isa = PBXGroup; children = ( + 089877A4214047BB0065DFA2 /* ABAnalyticsServiceProtocol.swift */, 08E7CA0F20DAF6E0004F8563 /* AnalyticsUserProperties.swift */, - 089877A4214047BB0065DFA2 /* AnalyticsUserPropertiesServiceProtocol.swift */, 2C99D087238967FD00078D2B /* Events */, 62E98F39D7A7B4486E71DF3E /* SplitTests */, ); @@ -4272,6 +4318,7 @@ 2C3ABA9023D229C200E90439 /* Bundle+Version.swift */, 62E98E4C4AECA5B066006DE7 /* CollectionExtensions.swift */, 2C0A109C268B1412001D4023 /* Date+Region.swift */, + 2CC3C7ED2719631900E72F6F /* DictionaryExtensions.swift */, 2C7AC49825B37ACA0024D4D2 /* FileManager+SharedContainer.swift */, 2C096582236C555D005B771A /* NSAttributedString+TrimmingCharacters.swift */, 62E98F7C1643A576801B8D47 /* URL+AppendQueryParameters.swift */, @@ -4435,6 +4482,7 @@ 2C1966B026E7CBDF000D5B06 /* AnnouncementsRepository.swift */, 2C26BF09240810A3000EE23C /* AttemptsRepository.swift */, 2C1AC31D255B476A00E6ECA9 /* CatalogBlocksRepository.swift */, + 2CDD3FDB2715F6D30029AF40 /* CoursesRepository.swift */, 2C98424626DE78EA0098E36B /* SearchResultsRepository.swift */, 2C57B2A0240945B2008284F0 /* SubmissionsRepository.swift */, ); @@ -4577,6 +4625,19 @@ path = CourseRevenue; sourceTree = ""; }; + 2C4391A827198B74000770AE /* Views */ = { + isa = PBXGroup; + children = ( + 2CACFB212719B916006E8FB3 /* CourseInfoPurchaseModalCourseCoverView.swift */, + 2CBEAACD271EFA9A00B52D2C /* CourseInfoPurchaseModalDisclaimerView.swift */, + 2C4391A9271994CB000770AE /* CourseInfoPurchaseModalHeaderView.swift */, + 3DA7BE8123937B6574E8544D /* CourseInfoPurchaseModalView.swift */, + 2CBEAAD1271F0D6500B52D2C /* ActionButtons */, + 2CBD17E7271EBDF9007B9CDB /* PromoCode */, + ); + path = Views; + sourceTree = ""; + }; 2C48BA02257277DD009103B2 /* Views */ = { isa = PBXGroup; children = ( @@ -4665,6 +4726,7 @@ isa = PBXGroup; children = ( 2C7E293226B03B69008581F4 /* CGSizeExtensionsTests.swift */, + 2CC3C7EF271963AF00E72F6F /* DictionaryExtensionsTests.swift */, 2C546C1423DA05F300352F27 /* StringExtensionsTests.swift */, ); path = ExtensionsTests; @@ -4804,7 +4866,8 @@ isa = PBXGroup; children = ( 8489EEE325FFBE8E004A85C5 /* Common.swift */, - 8489EE7625FAF13B004A85C5 /* StepicUITests.swift */, + 84FA5056262F204400603346 /* LoginUITests.swift */, + 8489EE7625FAF13B004A85C5 /* UnregisteredUITests.swift */, ); path = Sources; sourceTree = ""; @@ -5163,6 +5226,7 @@ 2C20C86B22F988030052E9BF /* CodeEditorTheme.swift */, 087585BF1FB524C20047A269 /* ContentLanguage.swift */, 2CA0DE63266FC0C60008C0D6 /* CourseBenefitStatus.swift */, + 2CBEAAD4271F1D4D00B52D2C /* CoursePurchaseFlowType.swift */, 08A9F71D1FC38C9E00640F1F /* CourseTag.swift */, 2C8EE76D2604B65B003512BC /* CourseType.swift */, 2C5D341525C997DF00372C61 /* CurrencySymbolMap.swift */, @@ -6435,6 +6499,15 @@ path = Cell; sourceTree = ""; }; + 2CBD17E7271EBDF9007B9CDB /* PromoCode */ = { + isa = PBXGroup; + children = ( + 2CBD17E8271EBE41007B9CDB /* CourseInfoPurchaseModalPromoCodeRightDetailView.swift */, + 2C0D345D271D687C00C51EEB /* CourseInfoPurchaseModalPromoCodeView.swift */, + ); + path = PromoCode; + sourceTree = ""; + }; 2CBE8A9C2625C58400AF7C86 /* Notifications */ = { isa = PBXGroup; children = ( @@ -6469,6 +6542,15 @@ path = ConcreteNotifications; sourceTree = ""; }; + 2CBEAAD1271F0D6500B52D2C /* ActionButtons */ = { + isa = PBXGroup; + children = ( + 2CBEAAD2271F0DB500B52D2C /* CourseInfoPurchaseModalActionButton.swift */, + 2CBEAACF271F0B8B00B52D2C /* CourseInfoPurchaseModalActionButtonsView.swift */, + ); + path = ActionButtons; + sourceTree = ""; + }; 2CC3ADE12625E947001C6B73 /* PermissionStatus */ = { isa = PBXGroup; children = ( @@ -6535,6 +6617,7 @@ isa = PBXGroup; children = ( 08484EE4211AF42E0006266F /* CachedValue.swift */, + 2CDD3FDD2715F86D0029AF40 /* DataFetchPolicy.swift */, 62E98D0214499A3812DE45C3 /* DataSourceType.swift */, 2C9BBE422490462C00FFED49 /* Debouncer.swift */, 2CD462E9226F4279004E4725 /* FetchResult.swift */, @@ -8079,6 +8162,15 @@ path = SubmissionsFilter; sourceTree = ""; }; + 5C571739346FF8C16ABA467D /* InputOutput */ = { + isa = PBXGroup; + children = ( + 79BE29668E3FDAF5A383D8CD /* CourseInfoPurchaseModalInputProtocol.swift */, + 74E5B29B62F5FD9D6BCCE5C0 /* CourseInfoPurchaseModalOutputProtocol.swift */, + ); + path = InputOutput; + sourceTree = ""; + }; 5F48944D8F8DBE3A61FECCAA /* Achievements */ = { isa = PBXGroup; children = ( @@ -9104,6 +9196,7 @@ 62E98B60A1FDF0B21CC9D1D1 /* CourseInfoSubmodules */ = { isa = PBXGroup; children = ( + FE4A5A74E4FE1CF64B89764F /* CourseInfoPurchaseModal */, 62E9808F18F52B116DC08916 /* CourseInfoTabInfo */, BB86F080C93979BAF271FD06 /* CourseInfoTabNews */, 62E9864ACFB074F6BF8CA2F3 /* CourseInfoTabReviews */, @@ -9970,6 +10063,22 @@ path = InputOutput; sourceTree = ""; }; + FE4A5A74E4FE1CF64B89764F /* CourseInfoPurchaseModal */ = { + isa = PBXGroup; + children = ( + 44055B26E5385DE3398B1959 /* CourseInfoPurchaseModalAssembly.swift */, + E27011EEBCB800A7B6ED6736 /* CourseInfoPurchaseModalDataFlow.swift */, + AAE9652E5F5D70BCE63C65FF /* CourseInfoPurchaseModalInteractor.swift */, + 3647FD98066BDBD6F93B727F /* CourseInfoPurchaseModalPresenter.swift */, + 758AD88FFDE3FCD437D26C47 /* CourseInfoPurchaseModalProvider.swift */, + D92F464AFB76FCA89A3B4CE1 /* CourseInfoPurchaseModalViewController.swift */, + 2C4841252715ED760091FE20 /* CourseInfoPurchaseModalViewModel.swift */, + 5C571739346FF8C16ABA467D /* InputOutput */, + 2C4391A827198B74000770AE /* Views */, + ); + path = CourseInfoPurchaseModal; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -10758,6 +10867,7 @@ 2C04BA452406E04400D74D4B /* ReplyTests.swift in Sources */, 2CF8DF9A25F2754C00F577C2 /* ReviewSessionResponseSpec.swift in Sources */, 2CEDC67226089DAE00B0B018 /* PlatformTypeTests.swift in Sources */, + 2CC3C7F0271963AF00E72F6F /* DictionaryExtensionsTests.swift in Sources */, 2C5967EB23E7828800072800 /* SubmissionTests.swift in Sources */, 2CCDD20124DBB61100A48EE0 /* ProtectedTests.swift in Sources */, 62E98ABEDEB0D955D6F3A951 /* UserAgentTests.swift in Sources */, @@ -10995,7 +11105,7 @@ 2C5F77C11F90F63B00E8E175 /* Notification+FetchMethods.swift in Sources */, 2C5D51592024653B00B9D932 /* BaseCardsStepsViewController.swift in Sources */, 08BC47091CD9FE10009A1D25 /* PersistentTaskManagerProtocol.swift in Sources */, - 089877A6214047BC0065DFA2 /* AnalyticsUserPropertiesServiceProtocol.swift in Sources */, + 089877A6214047BC0065DFA2 /* ABAnalyticsServiceProtocol.swift in Sources */, 083D64AF1C19BDB2003222F0 /* ControllerHelper.swift in Sources */, 083AABE81BE8D63D005E1E96 /* Progress+CoreDataProperties.swift in Sources */, 087585B71FB51D840047A269 /* CourseList+CoreDataProperties.swift in Sources */, @@ -11038,6 +11148,7 @@ 08C9F7D01C29AE8B00E544D0 /* WebControllerManager.swift in Sources */, 080CE15B1E95804C0089A27F /* SearchResultsAPI.swift in Sources */, 0846B1071EDDED4400D64D77 /* StepOptions+CoreDataProperties.swift in Sources */, + 2C4391AA271994CC000770AE /* CourseInfoPurchaseModalHeaderView.swift in Sources */, 2C096583236C555D005B771A /* NSAttributedString+TrimmingCharacters.swift in Sources */, 2C50363E24C59374001FAE04 /* NewProfileCertificatesHorizontalFlowLayout.swift in Sources */, 2C22042C20E277F70060117A /* Skeletonable+UIView.swift in Sources */, @@ -11077,6 +11188,7 @@ 2C6FA0B02588B0EC00D50DAA /* DefaultSimpleCourseListView.swift in Sources */, 2C98423C26DE4CA80098E36B /* SearchResult+CoreDataProperties.swift in Sources */, 2C12E4722565668000DC52CB /* UIViewController+PresentPanModal.swift in Sources */, + 2CBEAACE271EFA9A00B52D2C /* CourseInfoPurchaseModalDisclaimerView.swift in Sources */, 2CA867C12588FFF40006576E /* GridSimpleCourseListCollectionHeaderView.swift in Sources */, 2C0FE8C425F81A4900626289 /* InstructionPlainObject.swift in Sources */, 08CBA3011F57459800302154 /* MenuUIManager.swift in Sources */, @@ -11289,6 +11401,7 @@ 2C80C829201A504700ABB312 /* StepCardView.swift in Sources */, 084156961BCBFFBD006B8C73 /* Step.swift in Sources */, 08D2F72D2122E4B5009BA052 /* StoryNavigationDelegate.swift in Sources */, + 2CBEAAD5271F1D4D00B52D2C /* CoursePurchaseFlowType.swift in Sources */, 08F555581C4FAF5300C877E8 /* Submission.swift in Sources */, 08263D701DE5F230002E5B7F /* NotificationTimePickerViewController.swift in Sources */, 08E43E9D214C27B200E3CB50 /* ModalRouterSourceProtocol.swift in Sources */, @@ -11413,6 +11526,7 @@ 2C313E5526AFE636004ECBD2 /* StepQuizReviewStatusCircleView.swift in Sources */, 084C658D1FDAD04C006A3E17 /* RemoteConfig.swift in Sources */, 62E987459D5C971C022BE7A5 /* StepikURLSessionConfiguration.swift in Sources */, + 2CDD3FDE2715F86D0029AF40 /* DataFetchPolicy.swift in Sources */, 62E98DD20F2FC4D7DA37DF9A /* AlamofireDefaultSessionManager.swift in Sources */, 2CB11B1526135D8C001E400D /* CourseInfoTabSyllabusSectionExamActionButton.swift in Sources */, 08484F1A211B5B750006266F /* Skeletonable+UICollectionView.swift in Sources */, @@ -11622,6 +11736,7 @@ 2C3DAB8D233D71B100453B1C /* StepFontSizeStorageManager.swift in Sources */, 62E981A7B87D4281EBBB5184 /* DiscussionProxiesNetworkService.swift in Sources */, 2C96410E257001BE0007A3A4 /* AdaptiveRatingsNetworkService.swift in Sources */, + 2C0D345E271D687C00C51EEB /* CourseInfoPurchaseModalPromoCodeView.swift in Sources */, 2C6DECE123D02DF400E542B9 /* SettingsRightDetailTableViewCell.swift in Sources */, 2C8F3AE323CCBEAB004D113A /* StreamVideoQuality.swift in Sources */, 2C1E429C2497830F009F4C69 /* CoursePurchasesNetworkService.swift in Sources */, @@ -11664,6 +11779,7 @@ 2C2144CF26CD80F1005A61CE /* QuizTitleFactory.swift in Sources */, 62E984A5B0B216EF4C06E037 /* CourseInfoTabInfoPresenter.swift in Sources */, 62E98E8B19788BBE5859D696 /* CourseInfoTabInfoProvider.swift in Sources */, + 2CBEAAD0271F0B8B00B52D2C /* CourseInfoPurchaseModalActionButtonsView.swift in Sources */, 62E988775E581205F08D9163 /* CourseInfoTabInfoViewController.swift in Sources */, 2C572760252C71BE00C4C7C0 /* DiscussionsBadgesView.swift in Sources */, 62E9841CE54BD9A985D64460 /* CourseInfoTabInfoViewModel.swift in Sources */, @@ -12021,6 +12137,7 @@ 2CEBEBF6242F8FB200DBFDF0 /* StepikRequestRetrier.swift in Sources */, 62E98D7DEA0DB85F22C353CC /* NewFreeAnswerQuizViewController.swift in Sources */, 2C223A1524A0CC91002E4F02 /* IAPServiceDelegate.swift in Sources */, + 2C4841262715ED760091FE20 /* CourseInfoPurchaseModalViewModel.swift in Sources */, 62E98377FC4206342B5DEAD9 /* NewFreeAnswerQuizViewModel.swift in Sources */, 62E9867E94A2DD00EEE119E0 /* NewMatchingQuizAssembly.swift in Sources */, 62E9894A942372F5B1786756 /* NewMatchingQuizDataFlow.swift in Sources */, @@ -12079,6 +12196,7 @@ 62E989C07C8092DDD028E286 /* SolutionDataFlow.swift in Sources */, 2CEE66B32661180E0079F03B /* UserCoursesReviewsLeavedReviewCellView.swift in Sources */, 62E9874B2367F47B416932CF /* SolutionInteractor.swift in Sources */, + 2CBD17E9271EBE41007B9CDB /* CourseInfoPurchaseModalPromoCodeRightDetailView.swift in Sources */, 2CCD26A9269CB9A600053536 /* CourseRevenueTabMonthlyCellSkeletonView.swift in Sources */, 2CB31C72256D11E500265CC5 /* CourseListsNetworkService.swift in Sources */, 62E98CB8722C1DBC5B578107 /* SolutionPresenter.swift in Sources */, @@ -12131,6 +12249,7 @@ 62E98373328574F3493D6AE0 /* WriteCommentViewModel.swift in Sources */, 62E9829D238FEEDA8DC159CA /* WriteCommentOutputProtocol.swift in Sources */, 2C98423F26DE58A80098E36B /* SearchQueryResult.swift in Sources */, + 2CDD3FDC2715F6D30029AF40 /* CoursesRepository.swift in Sources */, 62E980B8758DEF7D06A56FB5 /* WriteCommentSolutionControl.swift in Sources */, 62E980D610A476A99FF7CAF4 /* WriteCourseReviewAssembly.swift in Sources */, 62E9852E9E2178B0917B73F6 /* WriteCourseReviewDataFlow.swift in Sources */, @@ -12151,6 +12270,7 @@ 2C921A41269C839800029A1E /* CourseRevenueTabMonthlyItemView.swift in Sources */, 245107514E8F81CF9A8C4C44 /* DownloadARQuickLookView.swift in Sources */, 2CA267A526CFB638000668C7 /* SubmissionsSelectionView.swift in Sources */, + 2CC3C7EE2719631900E72F6F /* DictionaryExtensions.swift in Sources */, 2C911B9025C02E430076DC31 /* StorageRecordsNetworkService.swift in Sources */, 2C0B771D25480A7500BA474D /* CourseListFilterViewModel.swift in Sources */, 008437078C67C7149BE6DE53 /* DownloadARQuickLookViewController.swift in Sources */, @@ -12221,6 +12341,7 @@ 247608C654D7C9A7C5994A12 /* NewProfileCertificatesViewController.swift in Sources */, 224F80151254989529458041 /* NewProfileCreatedCoursesAssembly.swift in Sources */, B2FE12ABF98ED99EAF727405 /* NewProfileCreatedCoursesDataFlow.swift in Sources */, + 2CACFB222719B916006E8FB3 /* CourseInfoPurchaseModalCourseCoverView.swift in Sources */, 5469EC1AD797F38AC790B9E7 /* NewProfileCreatedCoursesInteractor.swift in Sources */, 64AF865C5174A2A525C5FB8A /* NewProfileCreatedCoursesPresenter.swift in Sources */, 2C19D9B22617586100181DB0 /* StepikAcademyCourseListCollectionViewDataSource.swift in Sources */, @@ -12304,6 +12425,7 @@ CC81627A3343CB1F28B98772 /* StepikAcademyCourseListInteractor.swift in Sources */, F40280C566FFF3A1E7E1D1A3 /* StepikAcademyCourseListPresenter.swift in Sources */, 48D5535F0B128F0A9D6B40F7 /* StepikAcademyCourseListProvider.swift in Sources */, + 2CBEAAD3271F0DB500B52D2C /* CourseInfoPurchaseModalActionButton.swift in Sources */, F4EE5E18834D9CC6399DF19F /* StepikAcademyCourseListView.swift in Sources */, 98230689D5C33F2FF95CD633 /* StepikAcademyCourseListViewController.swift in Sources */, C9BDAEBEA7114EB494288E48 /* StepikAcademyCourseListOutputProtocol.swift in Sources */, @@ -12421,6 +12543,15 @@ D064B6AE4E478249DCE7B7F8 /* CourseInfoTabNewsViewController.swift in Sources */, 2BF2D5AC65ACA8B668CF93A6 /* CourseInfoTabNewsInputProtocol.swift in Sources */, 62E988A3F6BFDF9F0A9777BF /* CourseInfoTabNewsStatisticsView.swift in Sources */, + 6B85847876C117651FE721A0 /* CourseInfoPurchaseModalAssembly.swift in Sources */, + 6304CED128C9DCF1E60C8819 /* CourseInfoPurchaseModalDataFlow.swift in Sources */, + C9F3B231FE41885E9A267848 /* CourseInfoPurchaseModalInteractor.swift in Sources */, + 6A182A43AE6D72146643363D /* CourseInfoPurchaseModalPresenter.swift in Sources */, + 2FFEE41B5F3C02CDD36F0777 /* CourseInfoPurchaseModalProvider.swift in Sources */, + 552A685475C9262F4CF1B5B2 /* CourseInfoPurchaseModalView.swift in Sources */, + FE177755A3F7B06F53EB999B /* CourseInfoPurchaseModalViewController.swift in Sources */, + 07D32F4EF9D7CED0373404C6 /* CourseInfoPurchaseModalInputProtocol.swift in Sources */, + C739F25ABC1F1E42F9E1DE78 /* CourseInfoPurchaseModalOutputProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -12460,7 +12591,8 @@ buildActionMask = 2147483647; files = ( 8489EEE425FFBE8E004A85C5 /* Common.swift in Sources */, - 8489EE7725FAF13B004A85C5 /* StepicUITests.swift in Sources */, + 8489EE7725FAF13B004A85C5 /* UnregisteredUITests.swift in Sources */, + 84FA5057262F204400603346 /* LoginUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -12681,7 +12813,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; @@ -12706,7 +12838,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -12848,7 +12980,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Production.plist"; @@ -12878,7 +13010,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -12969,7 +13101,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13021,7 +13153,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; @@ -13102,7 +13234,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13150,7 +13282,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -13671,7 +13803,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -13725,7 +13857,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; @@ -13807,7 +13939,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -13855,7 +13987,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 383; + CURRENT_PROJECT_VERSION = 385; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; diff --git a/Stepic/Info-Develop.plist b/Stepic/Info-Develop.plist index 4494c65457..47179e245b 100644 --- a/Stepic/Info-Develop.plist +++ b/Stepic/Info-Develop.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.194-develop + 1.195-develop CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 383 + 385 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Production.plist b/Stepic/Info-Production.plist index de220d9f0f..4a92b3eedc 100644 --- a/Stepic/Info-Production.plist +++ b/Stepic/Info-Production.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.194 + 1.195 CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 383 + 385 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Info-Release.plist b/Stepic/Info-Release.plist index 756d513de8..9ae88f108e 100644 --- a/Stepic/Info-Release.plist +++ b/Stepic/Info-Release.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.194-release + 1.195-release CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 383 + 385 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Legacy/Analytics/ABAnalyticsServiceProtocol.swift b/Stepic/Legacy/Analytics/ABAnalyticsServiceProtocol.swift new file mode 100755 index 0000000000..a8b93566de --- /dev/null +++ b/Stepic/Legacy/Analytics/ABAnalyticsServiceProtocol.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol ABAnalyticsServiceProtocol { + func setGroup(test: String, group: String) +} diff --git a/Stepic/Legacy/Analytics/AnalyticsUserProperties.swift b/Stepic/Legacy/Analytics/AnalyticsUserProperties.swift index 1403497156..9236218998 100644 --- a/Stepic/Legacy/Analytics/AnalyticsUserProperties.swift +++ b/Stepic/Legacy/Analytics/AnalyticsUserProperties.swift @@ -15,6 +15,8 @@ import YandexMobileMetrica final class AnalyticsUserProperties: ABAnalyticsServiceProtocol { static let shared = AnalyticsUserProperties() + // MARK: ABAnalyticsServiceProtocol + func setGroup(test: String, group: String) { self.setAmplitudeProperty(key: test, value: group) // AppMetrica @@ -24,25 +26,7 @@ final class AnalyticsUserProperties: ABAnalyticsServiceProtocol { YMMYandexMetrica.report(userProfile, onFailure: nil) } - func setAmplitudeProperty(key: String, value: Any?) { - if let value = value { - Amplitude.instance().setUserProperties([key: value]) - } else if let identify = AMPIdentify().unset(key) { - Amplitude.instance().identify(identify) - } - } - - private func setCrashlyticsProperty(key: String, value: Any?) { - if let value = value { - Crashlytics.crashlytics().setCustomValue(value, forKey: key) - } - } - - private func incrementAmplitudeProperty(key: String, value: Int = 1) { - if let identify = AMPIdentify().add(key, value: value as NSObject) { - Amplitude.instance().identify(identify) - } - } + // MARK: Public API func clearUserDependentProperties() { self.setUserID(to: nil) @@ -50,8 +34,8 @@ final class AnalyticsUserProperties: ABAnalyticsServiceProtocol { } func setUserID(to id: Int?) { - self.setAmplitudeProperty(key: "stepik_id", value: id) - self.setCrashlyticsProperty(key: "stepik_id", value: id) + self.setAmplitudeProperty(key: UserPropertyKey.stepikID.rawValue, value: id) + self.setCrashlyticsProperty(key: UserPropertyKey.stepikID.rawValue, value: id) let userProfileID: String? = id != nil ? String(id.require()) : nil // Update AppMetrica user profile id. @@ -61,44 +45,52 @@ final class AnalyticsUserProperties: ABAnalyticsServiceProtocol { } func incrementSubmissionsCount() { - self.incrementAmplitudeProperty(key: "submissions_count") + self.incrementAmplitudeProperty(key: UserPropertyKey.submissionsCount.rawValue) } func decrementCoursesCount() { - self.incrementAmplitudeProperty(key: "courses_count", value: -1) + self.incrementAmplitudeProperty(key: UserPropertyKey.coursesCount.rawValue, value: -1) } func incrementCoursesCount() { - self.incrementAmplitudeProperty(key: "courses_count") + self.incrementAmplitudeProperty(key: UserPropertyKey.coursesCount.rawValue) } func setCoursesCount(count: Int?) { - self.setAmplitudeProperty(key: "courses_count", value: count) + self.setAmplitudeProperty(key: UserPropertyKey.coursesCount.rawValue, value: count) } func setPushPermissionStatus(_ status: NotificationPermissionStatus) { - let key = "push_permission" - - switch status { - case .authorized: - self.setAmplitudeProperty(key: key, value: "granted") - case .denied: - self.setAmplitudeProperty(key: key, value: "not_granted") - case .notDetermined: - self.setAmplitudeProperty(key: key, value: "not_determined") - } + let statusStringValue: String = { + switch status { + case .authorized: + return "granted" + case .denied: + return "not_granted" + case .notDetermined: + return "not_determined" + } + }() + + self.setAmplitudeProperty(key: UserPropertyKey.pushPermission.rawValue, value: statusStringValue) } func setStreaksNotificationsEnabled(_ enabled: Bool) { - self.setAmplitudeProperty(key: "streaks_notifications_enabled", value: enabled ? "enabled" : "disabled") + self.setAmplitudeProperty( + key: UserPropertyKey.streaksNotificationsEnabled.rawValue, + value: enabled ? "enabled" : "disabled" + ) } func setScreenOrientation(isPortrait: Bool) { - self.setAmplitudeProperty(key: "screen_orientation", value: isPortrait ? "portrait" : "landscape") + self.setAmplitudeProperty( + key: UserPropertyKey.screenOrientation.rawValue, + value: isPortrait ? "portrait" : "landscape" + ) } func setApplicationID(id: String) { - self.setAmplitudeProperty(key: "application_id", value: id) + self.setAmplitudeProperty(key: UserPropertyKey.applicationID.rawValue, value: id) } func updateUserID() { @@ -115,6 +107,65 @@ final class AnalyticsUserProperties: ABAnalyticsServiceProtocol { return false }() - self.setAmplitudeProperty(key: "is_night_mode_enabled", value: "\(isEnabled)") + self.setAmplitudeProperty(key: UserPropertyKey.isNightModeEnabled.rawValue, value: "\(isEnabled)") + } + + func setRemoteConfigUserProperties(_ keysAndValues: [String: Any]) { + Amplitude.instance().setUserProperties(keysAndValues) + Crashlytics.crashlytics().setCustomKeysAndValues(keysAndValues) + self.setYandexMetricaProfileAttributes(keysAndValues) + } + + // MARK: Private API + + private func setAmplitudeProperty(key: String, value: Any?) { + if let value = value { + Amplitude.instance().setUserProperties([key: value]) + } else if let identify = AMPIdentify().unset(key) { + Amplitude.instance().identify(identify) + } + } + + private func incrementAmplitudeProperty(key: String, value: Int = 1) { + if let identify = AMPIdentify().add(key, value: value as NSObject) { + Amplitude.instance().identify(identify) + } + } + + private func setCrashlyticsProperty(key: String, value: Any?) { + if let value = value { + Crashlytics.crashlytics().setCustomValue(value, forKey: key) + } + } + + private func setYandexMetricaProfileAttributes(_ profileAttributes: [String: Any]) { + let userProfileUpdates = profileAttributes.map { key, value -> YMMUserProfileUpdate in + if let boolValue = value as? Bool { + return YMMProfileAttribute.customBool(key).withValue(boolValue) + } else if let doubleValue = value as? Double { + return YMMProfileAttribute.customNumber(key).withValue(doubleValue) + } else { + return YMMProfileAttribute.customString(key).withValue(String(describing: value)) + } + } + + let userProfile = YMMMutableUserProfile() + userProfile.apply(from: userProfileUpdates) + YMMYandexMetrica.report(userProfile) { error in + print("AnalyticsUserProperties :: YMMYandexMetrica :: failed report userProfile with error = \(error)") + } + } + + // MARK: Inner Types + + private enum UserPropertyKey: String { + case stepikID = "stepik_id" + case submissionsCount = "submissions_count" + case coursesCount = "courses_count" + case pushPermission = "push_permission" + case streaksNotificationsEnabled = "streaks_notifications_enabled" + case screenOrientation = "screen_orientation" + case applicationID = "application_id" + case isNightModeEnabled = "is_night_mode_enabled" } } diff --git a/Stepic/Legacy/Analytics/AnalyticsUserPropertiesServiceProtocol.swift b/Stepic/Legacy/Analytics/AnalyticsUserPropertiesServiceProtocol.swift deleted file mode 100755 index 6d2518d713..0000000000 --- a/Stepic/Legacy/Analytics/AnalyticsUserPropertiesServiceProtocol.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// ABAnalyticsServiceProtocol.swift -// SplitTests -// -// Created by Alex Zimin on 15/06/2018. -// Copyright © 2018 Akexander. All rights reserved. -// - -import Foundation - -protocol ABAnalyticsServiceProtocol { - func setGroup(test: String, group: String) -} diff --git a/Stepic/Legacy/Model/RemoteConfig/RemoteConfig.swift b/Stepic/Legacy/Model/RemoteConfig/RemoteConfig.swift index ebfff42001..79e8a6d7ae 100644 --- a/Stepic/Legacy/Model/RemoteConfig/RemoteConfig.swift +++ b/Stepic/Legacy/Model/RemoteConfig/RemoteConfig.swift @@ -11,7 +11,12 @@ import FirebaseRemoteConfig import Foundation final class RemoteConfig { + private static let analyticsUserPropertyKeyPrefix = "remote_config_" + private static let defaultShowStreaksNotificationTrigger = ShowStreaksNotificationTrigger.loginAndSubmission + + private static let defaultCoursePurchaseFlowType = CoursePurchaseFlowType.web + static let shared = RemoteConfig() var loadingDoneCallback: (() -> Void)? @@ -23,12 +28,11 @@ final class RemoteConfig { Key.showStreaksNotificationTrigger.rawValue: NSString(string: Self.defaultShowStreaksNotificationTrigger.rawValue), Key.adaptiveBackendUrl.rawValue: NSString(string: StepikApplicationsInfo.adaptiveRatingURL), Key.supportedInAdaptiveModeCourses.rawValue: NSArray(array: StepikApplicationsInfo.adaptiveSupportedCourses), - Key.darkModeAvailable.rawValue: NSNumber(value: true), Key.arQuickLookAvailable.rawValue: NSNumber(value: false), - Key.isDisabledStepsSupported.rawValue: NSNumber(value: false), Key.searchResultsQueryParams.rawValue: NSDictionary(dictionary: ["is_popular": "true", "is_public": "true"]), Key.isCoursePricesEnabled.rawValue: NSNumber(value: false), - Key.isCourseRevenueAvailable.rawValue: NSNumber(value: false) + Key.isCourseRevenueAvailable.rawValue: NSNumber(value: false), + Key.purchaseFlow.rawValue: NSString(string: Self.defaultCoursePurchaseFlowType.rawValue) ] var showStreaksNotificationTrigger: ShowStreaksNotificationTrigger { @@ -81,17 +85,6 @@ final class RemoteConfig { return supportedCourses.compactMap { Int($0) } } - var isDarkModeAvailable: Bool { - if DeviceInfo.current.OSVersion.major < 13 { - return false - } - - return FirebaseRemoteConfig.RemoteConfig - .remoteConfig() - .configValue(forKey: Key.darkModeAvailable.rawValue) - .boolValue - } - var isARQuickLookAvailable: Bool { FirebaseRemoteConfig.RemoteConfig .remoteConfig() @@ -99,13 +92,6 @@ final class RemoteConfig { .boolValue } - var isDisabledStepsSupported: Bool { - FirebaseRemoteConfig.RemoteConfig - .remoteConfig() - .configValue(forKey: Key.isDisabledStepsSupported.rawValue) - .boolValue - } - var searchResultsQueryParams: JSONDictionary { guard let configValue = FirebaseRemoteConfig.RemoteConfig.remoteConfig().configValue( forKey: Key.searchResultsQueryParams.rawValue @@ -138,6 +124,16 @@ final class RemoteConfig { #endif } + var coursePurchaseFlow: CoursePurchaseFlowType { + guard let configValue = FirebaseRemoteConfig.RemoteConfig.remoteConfig().configValue( + forKey: Key.purchaseFlow.rawValue + ).stringValue else { + return Self.defaultCoursePurchaseFlowType + } + + return CoursePurchaseFlowType(rawValue: configValue) ?? Self.defaultCoursePurchaseFlowType + } + init() { self.setConfigDefaults() self.fetchRemoteConfigData() @@ -171,8 +167,13 @@ final class RemoteConfig { } } - self?.fetchComplete = true - self?.loadingDoneCallback?() + guard let strongSelf = self else { + return + } + + strongSelf.fetchComplete = true + strongSelf.loadingDoneCallback?() + strongSelf.updateAnalyticsUserProperties() } } @@ -183,6 +184,19 @@ final class RemoteConfig { FirebaseRemoteConfig.RemoteConfig.remoteConfig().configSettings = debugSettings } + private func updateAnalyticsUserProperties() { + let userProperties: [String: Any] = [ + Key.showStreaksNotificationTrigger.analyticsUserPropertyKey: self.showStreaksNotificationTrigger.rawValue, + Key.adaptiveBackendUrl.analyticsUserPropertyKey: self.adaptiveBackendURL, + Key.supportedInAdaptiveModeCourses.analyticsUserPropertyKey: self.supportedInAdaptiveModeCourses, + Key.arQuickLookAvailable.analyticsUserPropertyKey: self.isARQuickLookAvailable, + Key.searchResultsQueryParams.analyticsUserPropertyKey: self.searchResultsQueryParams, + Key.isCoursePricesEnabled.analyticsUserPropertyKey: self.isCoursePricesEnabled, + Key.isCourseRevenueAvailable.analyticsUserPropertyKey: self.isCourseRevenueAvailable + ] + AnalyticsUserProperties.shared.setRemoteConfigUserProperties(userProperties) + } + // MARK: Inner Types enum ShowStreaksNotificationTrigger: String { @@ -190,15 +204,16 @@ final class RemoteConfig { case submission = "submission" } - enum Key: String { + private enum Key: String { case showStreaksNotificationTrigger = "show_streaks_notification_trigger" case adaptiveBackendUrl = "adaptive_backend_url" case supportedInAdaptiveModeCourses = "supported_adaptive_courses_ios" - case darkModeAvailable = "is_dark_mode_available_ios" case arQuickLookAvailable = "is_ar_quick_look_available_ios" - case isDisabledStepsSupported = "is_disabled_steps_supported" case searchResultsQueryParams = "search_query_params_ios" case isCoursePricesEnabled = "is_course_prices_enabled_ios" case isCourseRevenueAvailable = "is_course_revenue_available_ios" + case purchaseFlow = "purchase_flow_ios" + + var analyticsUserPropertyKey: String { "\(RemoteConfig.analyticsUserPropertyKeyPrefix)\(self.rawValue)" } } } diff --git a/Stepic/Sources/Common/DataFetchPolicy.swift b/Stepic/Sources/Common/DataFetchPolicy.swift new file mode 100644 index 0000000000..064e1c5989 --- /dev/null +++ b/Stepic/Sources/Common/DataFetchPolicy.swift @@ -0,0 +1,12 @@ +import Foundation + +enum DataFetchPolicy { + /// Firstly executes the request against the cache. + /// If requested data is present in the cache, that data is returned. + /// Otherwise, executes the request against the network and returns that data after caching it. + case cacheFirst + /// Firstly executes the request against the network. + /// If server-side returns the result, that data is returned. + /// Otherwise, executes the request against the cache and returns that data. + case remoteFirst +} diff --git a/Stepic/Sources/Controllers/PanModalPresentableViewController.swift b/Stepic/Sources/Controllers/PanModalPresentableViewController.swift index 14e6a371cc..f3770d32db 100644 --- a/Stepic/Sources/Controllers/PanModalPresentableViewController.swift +++ b/Stepic/Sources/Controllers/PanModalPresentableViewController.swift @@ -10,6 +10,15 @@ class PanModalPresentableViewController: UIViewController, PanModalPresentable { : self.longFormHeight } + var longFormHeight: PanModalHeight { + guard let scrollView = self.panScrollable else { + return .maxHeight + } + + scrollView.layoutIfNeeded() + return .contentHeight(scrollView.contentSize.height) + } + var anchorModalToLongForm: Bool { false } var isShortFormEnabled = true diff --git a/Stepic/Sources/Extensions/Foundation/DictionaryExtensions.swift b/Stepic/Sources/Extensions/Foundation/DictionaryExtensions.swift new file mode 100644 index 0000000000..2f39a20e15 --- /dev/null +++ b/Stepic/Sources/Extensions/Foundation/DictionaryExtensions.swift @@ -0,0 +1,19 @@ +import Foundation + +extension Dictionary { + /// Returns a dictionary containing the results of mapping the given closure over the sequence’s elements. + /// - Parameter transform: A mapping closure. `transform` accepts an element of this sequence as its parameter and + /// returns a transformed value of the same or of a different type. + /// - Returns: A dictionary containing the transformed elements of this sequence. + func mapKeysAndValues(_ transform: ((key: Key, value: Value)) throws -> (K, V)) rethrows -> [K: V] { + [K: V](uniqueKeysWithValues: try map(transform)) + } + + /// Returns a dictionary containing the non-`nil` results of calling the given transformation with each element of this sequence. + /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. + /// - Returns: A dictionary of the non-`nil` results of calling `transform` with each element of the sequence. + /// - Complexity: *O(m + n)*, where _m_ is the length of this sequence and _n_ is the length of the result. + func compactMapKeysAndValues(_ transform: ((key: Key, value: Value)) throws -> (K, V)?) rethrows -> [K: V] { + [K: V](uniqueKeysWithValues: try compactMap(transform)) + } +} diff --git a/Stepic/Sources/Model/CoursePurchaseFlowType.swift b/Stepic/Sources/Model/CoursePurchaseFlowType.swift new file mode 100644 index 0000000000..92d53b0cc2 --- /dev/null +++ b/Stepic/Sources/Model/CoursePurchaseFlowType.swift @@ -0,0 +1,6 @@ +import Foundation + +enum CoursePurchaseFlowType: String { + case web + case iap +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalAssembly.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalAssembly.swift new file mode 100644 index 0000000000..4cee4f6a75 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalAssembly.swift @@ -0,0 +1,34 @@ +import UIKit + +final class CourseInfoPurchaseModalAssembly: Assembly { + var moduleInput: CourseInfoPurchaseModalInputProtocol? + + private let courseID: Course.IdType + + private weak var moduleOutput: CourseInfoPurchaseModalOutputProtocol? + + init(courseID: Course.IdType, output: CourseInfoPurchaseModalOutputProtocol? = nil) { + self.courseID = courseID + self.moduleOutput = output + } + + func makeModule() -> UIViewController { + let provider = CourseInfoPurchaseModalProvider( + courseID: self.courseID, + coursesRepository: CoursesRepository.default + ) + let presenter = CourseInfoPurchaseModalPresenter() + let interactor = CourseInfoPurchaseModalInteractor( + courseID: self.courseID, + presenter: presenter, + provider: provider + ) + let viewController = CourseInfoPurchaseModalViewController(interactor: interactor) + + presenter.viewController = viewController + self.moduleInput = interactor + interactor.moduleOutput = self.moduleOutput + + return viewController + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalDataFlow.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalDataFlow.swift new file mode 100644 index 0000000000..6c2af0fae1 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalDataFlow.swift @@ -0,0 +1,18 @@ +import Foundation + +enum CourseInfoPurchaseModal { + enum ModalLoad { + struct Request {} + + struct Response {} + + struct ViewModel { + let state: ViewControllerState + } + } + + enum ViewControllerState { + case loading + case result(data: CourseInfoPurchaseModalViewModel) + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalInteractor.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalInteractor.swift new file mode 100644 index 0000000000..da8fa49a45 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalInteractor.swift @@ -0,0 +1,33 @@ +import Foundation +import PromiseKit + +protocol CourseInfoPurchaseModalInteractorProtocol { + func doModalLoad(request: CourseInfoPurchaseModal.ModalLoad.Request) +} + +final class CourseInfoPurchaseModalInteractor: CourseInfoPurchaseModalInteractorProtocol { + weak var moduleOutput: CourseInfoPurchaseModalOutputProtocol? + + private let presenter: CourseInfoPurchaseModalPresenterProtocol + private let provider: CourseInfoPurchaseModalProviderProtocol + + private let courseID: Course.IdType + + init( + courseID: Course.IdType, + presenter: CourseInfoPurchaseModalPresenterProtocol, + provider: CourseInfoPurchaseModalProviderProtocol + ) { + self.courseID = courseID + self.presenter = presenter + self.provider = provider + } + + func doModalLoad(request: CourseInfoPurchaseModal.ModalLoad.Request) {} + + enum Error: Swift.Error { + case something + } +} + +extension CourseInfoPurchaseModalInteractor: CourseInfoPurchaseModalInputProtocol {} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalPresenter.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalPresenter.swift new file mode 100644 index 0000000000..25788eba34 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalPresenter.swift @@ -0,0 +1,11 @@ +import UIKit + +protocol CourseInfoPurchaseModalPresenterProtocol { + func presentModal(response: CourseInfoPurchaseModal.ModalLoad.Response) +} + +final class CourseInfoPurchaseModalPresenter: CourseInfoPurchaseModalPresenterProtocol { + weak var viewController: CourseInfoPurchaseModalViewControllerProtocol? + + func presentModal(response: CourseInfoPurchaseModal.ModalLoad.Response) {} +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalProvider.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalProvider.swift new file mode 100644 index 0000000000..db7e998797 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalProvider.swift @@ -0,0 +1,57 @@ +import Foundation +import PromiseKit + +protocol CourseInfoPurchaseModalProviderProtocol { + func fetchCourseFromCache() -> Promise + func fetchCourseFromRemote() -> Promise + func fetchCourseFromCacheOrRemote() -> Promise +} + +final class CourseInfoPurchaseModalProvider: CourseInfoPurchaseModalProviderProtocol { + private let courseID: Course.IdType + + private let coursesRepository: CoursesRepositoryProtocol + + init( + courseID: Course.IdType, + coursesRepository: CoursesRepositoryProtocol + ) { + self.courseID = courseID + self.coursesRepository = coursesRepository + } + + func fetchCourseFromCache() -> Promise { + Promise { seal in + self.coursesRepository.fetch(id: self.courseID, dataSourceType: .cache).done { course in + seal.fulfill(course) + }.catch { _ in + seal.reject(Error.persistenceFetchFailed) + } + } + } + + func fetchCourseFromRemote() -> Promise { + Promise { seal in + self.coursesRepository.fetch(id: self.courseID, dataSourceType: .remote).done { course in + seal.fulfill(course) + }.catch { _ in + seal.reject(Error.networkFetchFailed) + } + } + } + + func fetchCourseFromCacheOrRemote() -> Promise { + Promise { seal in + self.coursesRepository.fetch(id: self.courseID, fetchPolicy: .cacheFirst).done { course in + seal.fulfill(course) + }.catch { _ in + seal.reject(Error.networkFetchFailed) + } + } + } + + enum Error: Swift.Error { + case persistenceFetchFailed + case networkFetchFailed + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalViewController.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalViewController.swift new file mode 100644 index 0000000000..a1c49e615e --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalViewController.swift @@ -0,0 +1,177 @@ +import IQKeyboardManagerSwift +import PanModal +import UIKit + +protocol CourseInfoPurchaseModalViewControllerProtocol: AnyObject { + func displayModal(viewModel: CourseInfoPurchaseModal.ModalLoad.ViewModel) +} + +final class CourseInfoPurchaseModalViewController: PanModalPresentableViewController { + private let interactor: CourseInfoPurchaseModalInteractorProtocol + + private var state: CourseInfoPurchaseModal.ViewControllerState + + private var hasLoadedData: Bool { + if case .result = self.state { + return true + } + return false + } + + private var keyboardIsShowing = false + private var keyboardHeight: CGFloat = 0 + + var courseInfoPurchaseModalView: CourseInfoPurchaseModalView? { self.view as? CourseInfoPurchaseModalView } + + override var panScrollable: UIScrollView? { + // Returns nil to prevent PanModal overrides scrollView bottom contentInset. + self.keyboardIsShowing ? nil : self.courseInfoPurchaseModalView?.panScrollable + } + + override var shortFormHeight: PanModalHeight { + if self.hasLoadedData && self.isShortFormEnabled, + let intrinsicContentSize = self.courseInfoPurchaseModalView?.intrinsicContentSize { + return .contentHeight(intrinsicContentSize.height) + } + return super.shortFormHeight + } + + override var longFormHeight: PanModalHeight { + guard self.hasLoadedData else { + return self.shortFormHeight + } + + if self.keyboardIsShowing, + let intrinsicContentSize = self.courseInfoPurchaseModalView?.intrinsicContentSize { + return .contentHeight(intrinsicContentSize.height + self.keyboardHeight) + } + + return super.longFormHeight + } + + init( + interactor: CourseInfoPurchaseModalInteractorProtocol, + initialState: CourseInfoPurchaseModal.ViewControllerState = .loading + ) { + self.interactor = interactor + self.state = initialState + super.init() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let view = CourseInfoPurchaseModalView(frame: UIScreen.main.bounds) + self.view = view + view.delegate = self + } + + override func viewDidLoad() { + super.viewDidLoad() + + NotificationCenter.default.addObserver( + self, + selector: #selector(self.onKeyboardWillShow(notification:)), + name: UIResponder.keyboardWillShowNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(self.onKeyboardWillHide(notification:)), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) + + self.updateState(newState: self.state) + self.interactor.doModalLoad(request: .init()) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + IQKeyboardManager.shared.enable = false + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.view.endEditing(true) + IQKeyboardManager.shared.enable = true + } + + // MARK: Private API + + private func updateState(newState: CourseInfoPurchaseModal.ViewControllerState) { + switch newState { + case .result(let viewModel): + self.courseInfoPurchaseModalView?.hideLoading() + self.courseInfoPurchaseModalView?.configure(viewModel: viewModel) + case .loading: + self.courseInfoPurchaseModalView?.showLoading() + } + + self.state = newState + } + + private func transition(to state: PanModalPresentationController.PresentationState) { + self.panModalSetNeedsLayoutUpdate() + self.panModalTransition(to: state) + } + + @objc + private func onKeyboardWillShow(notification: NSNotification) { + self.keyboardIsShowing = true + + let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect + self.keyboardHeight = keyboardFrame?.size.height ?? 0 + + var newContentInsets = self.courseInfoPurchaseModalView?.contentInsets ?? .zero + newContentInsets.bottom = self.keyboardHeight + self.courseInfoPurchaseModalView?.contentInsets = newContentInsets + + self.transition(to: .longForm) + } + + @objc + private func onKeyboardWillHide(notification: NSNotification) { + self.keyboardIsShowing = false + self.isShortFormEnabled = true + self.transition(to: .shortForm) + } +} + +// MARK: - CourseInfoPurchaseModalViewController: CourseInfoPurchaseModalViewControllerProtocol - + +extension CourseInfoPurchaseModalViewController: CourseInfoPurchaseModalViewControllerProtocol { + func displayModal(viewModel: CourseInfoPurchaseModal.ModalLoad.ViewModel) { + self.updateState(newState: viewModel.state) + self.transition(to: .shortForm) + } +} + +// MARK: - CourseInfoPurchaseModalViewController: CourseInfoPurchaseModalViewDelegate - + +extension CourseInfoPurchaseModalViewController: CourseInfoPurchaseModalViewDelegate { + func courseInfoPurchaseModalViewDidClickCloseButton(_ view: CourseInfoPurchaseModalView) { + self.dismiss(animated: true) + } + + func courseInfoPurchaseModalView(_ view: CourseInfoPurchaseModalView, didClickLink link: URL) { + WebControllerManager.shared.presentWebControllerWithURL( + link, + inController: self, + withKey: .externalLink, + allowsSafari: true, + backButtonStyle: .done + ) + } + + func courseInfoPurchaseModalViewDidClickBuyButton(_ view: CourseInfoPurchaseModalView) { + print(#function) + } + + func courseInfoPurchaseModalViewDidClickWishlistButton(_ view: CourseInfoPurchaseModalView) { + print(#function) + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalViewModel.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalViewModel.swift new file mode 100644 index 0000000000..354dab1c93 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalViewModel.swift @@ -0,0 +1,6 @@ +import Foundation + +struct CourseInfoPurchaseModalViewModel { + let courseTitle: String + let courseCoverImageURL: URL? +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/InputOutput/CourseInfoPurchaseModalInputProtocol.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/InputOutput/CourseInfoPurchaseModalInputProtocol.swift new file mode 100644 index 0000000000..a6ea922bf2 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/InputOutput/CourseInfoPurchaseModalInputProtocol.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol CourseInfoPurchaseModalInputProtocol: AnyObject {} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/InputOutput/CourseInfoPurchaseModalOutputProtocol.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/InputOutput/CourseInfoPurchaseModalOutputProtocol.swift new file mode 100644 index 0000000000..35cfacfc89 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/InputOutput/CourseInfoPurchaseModalOutputProtocol.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol CourseInfoPurchaseModalOutputProtocol: AnyObject {} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/ActionButtons/CourseInfoPurchaseModalActionButton.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/ActionButtons/CourseInfoPurchaseModalActionButton.swift new file mode 100644 index 0000000000..819625a9fb --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/ActionButtons/CourseInfoPurchaseModalActionButton.swift @@ -0,0 +1,138 @@ +import SnapKit +import UIKit + +extension CourseInfoPurchaseModalActionButton { + struct Appearance { + let iconImageViewSize = CGSize(width: 18, height: 22) + let iconImageViewInsets = LayoutInsets(left: 16) + var iconImageViewTintColor: UIColor? + + let textLabelFont = Typography.bodyFont + var textLabelTextColor = UIColor.white + + var backgroundColor = UIColor.stepikVioletFixed + + var borderWidth: CGFloat = 0 + var borderColor: UIColor? + let cornerRadius: CGFloat = 8 + } +} + +final class CourseInfoPurchaseModalActionButton: UIControl { + var appearance: Appearance { + didSet { + self.updateAppearance() + } + } + + private lazy var iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.isHidden = true + return imageView + }() + + private lazy var textLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.numberOfLines = 1 + return label + }() + + var iconImage: UIImage? { + didSet { + self.iconImageView.image = self.iconImage + self.iconImageView.isHidden = self.iconImage == nil + } + } + + var text: String? { + didSet { + self.textLabel.text = self.text + } + } + + var attributedText: NSAttributedString? { + didSet { + self.textLabel.attributedText = self.attributedText + } + } + + override var isHighlighted: Bool { + didSet { + self.alpha = self.isHighlighted ? 0.5 : 1.0 + } + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + + self.updateAppearance() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + self.performBlockIfAppearanceChanged(from: previousTraitCollection) { + self.updateBorder() + } + } + + private func updateAppearance() { + if let iconImageViewTintColor = self.appearance.iconImageViewTintColor { + self.iconImageView.tintColor = iconImageViewTintColor + } + + self.textLabel.font = self.appearance.textLabelFont + self.textLabel.textColor = self.appearance.textLabelTextColor + + self.backgroundColor = self.appearance.backgroundColor + + self.updateBorder() + } + + private func updateBorder() { + self.layer.borderWidth = self.appearance.borderWidth + self.layer.borderColor = self.appearance.borderColor?.cgColor + } +} + +extension CourseInfoPurchaseModalActionButton: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.layer.cornerRadius = self.appearance.cornerRadius + self.layer.masksToBounds = true + self.clipsToBounds = true + } + + func addSubviews() { + self.addSubview(self.iconImageView) + self.addSubview(self.textLabel) + } + + func makeConstraints() { + self.iconImageView.translatesAutoresizingMaskIntoConstraints = false + self.iconImageView.snp.makeConstraints { make in + make.leading.equalToSuperview().offset(self.appearance.iconImageViewInsets.left) + make.size.equalTo(self.appearance.iconImageViewSize) + make.centerY.equalTo(self.textLabel.snp.centerY) + } + + self.textLabel.translatesAutoresizingMaskIntoConstraints = false + self.textLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/ActionButtons/CourseInfoPurchaseModalActionButtonsView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/ActionButtons/CourseInfoPurchaseModalActionButtonsView.swift new file mode 100644 index 0000000000..e2ffea9ed5 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/ActionButtons/CourseInfoPurchaseModalActionButtonsView.swift @@ -0,0 +1,150 @@ +import SnapKit +import UIKit + +extension CourseInfoPurchaseModalActionButtonsView { + struct Appearance { + let actionButtonHeight: CGFloat = 44 + + let wishlistButtonBorderWidth: CGFloat = 1 + + let stackViewSpacing: CGFloat = 16 + let stackViewInsets = LayoutInsets(horizontal: 16) + } +} + +final class CourseInfoPurchaseModalActionButtonsView: UIView { + let appearance: Appearance + + private lazy var buyButton = CourseInfoPurchaseModalActionButton() + + private lazy var wishlistButton = CourseInfoPurchaseModalActionButton() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = self.appearance.stackViewSpacing + return stackView + }() + + var style = Style.violet { + didSet { + self.updateStyle() + } + } + + var onBuyButtonClick: (() -> Void)? + var onWishlistButtonClick: (() -> Void)? + + override var intrinsicContentSize: CGSize { + let stackViewIntrinsicContentSize = self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return CGSize(width: UIView.noIntrinsicMetric, height: stackViewIntrinsicContentSize.height) + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + + self.updateStyle() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc + private func buyButtonClicked() { + self.onBuyButtonClick?() + } + + @objc + private func wishlistButtonClicked() { + self.onWishlistButtonClick?() + } + + private func updateStyle() { + self.buyButton.appearance = .init( + textLabelTextColor: self.style.buyButtonTextColor, + backgroundColor: self.style.buyButtonBackgroundColor + ) + self.wishlistButton.appearance = .init( + iconImageViewTintColor: self.style.wishlistButtonTextColor, + textLabelTextColor: self.style.wishlistButtonTextColor, + backgroundColor: self.style.wishlistButtonBackgroundColor, + borderWidth: self.appearance.wishlistButtonBorderWidth, + borderColor: self.style.wishlistButtonBorderColor + ) + } + + enum Style { + case violet + case green + + fileprivate var buyButtonTextColor: UIColor { .white } + + fileprivate var buyButtonBackgroundColor: UIColor { + switch self { + case .violet: + return .stepikVioletFixed + case .green: + return .stepikGreenFixed + } + } + + fileprivate var wishlistButtonTextColor: UIColor { + switch self { + case .violet: + return .stepikVioletFixed + case .green: + return .stepikGreenFixed + } + } + + fileprivate var wishlistButtonBackgroundColor: UIColor { .clear } + + fileprivate var wishlistButtonBorderColor: UIColor { + switch self { + case .violet: + return .stepikVioletFixed + case .green: + return .stepikGreenFixed + } + } + } +} + +extension CourseInfoPurchaseModalActionButtonsView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.buyButton.addTarget(self, action: #selector(self.buyButtonClicked), for: .touchUpInside) + self.wishlistButton.addTarget(self, action: #selector(self.wishlistButtonClicked), for: .touchUpInside) + } + + func addSubviews() { + self.addSubview(self.stackView) + + self.stackView.addArrangedSubview(self.buyButton) + self.stackView.addArrangedSubview(self.wishlistButton) + } + + func makeConstraints() { + self.stackView.translatesAutoresizingMaskIntoConstraints = false + self.stackView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(self.appearance.stackViewInsets.edgeInsets) + } + + [self.buyButton, self.wishlistButton].forEach { actionButton in + actionButton.translatesAutoresizingMaskIntoConstraints = false + actionButton.snp.makeConstraints { make in + make.height.equalTo(self.appearance.actionButtonHeight) + } + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalCourseCoverView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalCourseCoverView.swift new file mode 100644 index 0000000000..27d2e3155d --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalCourseCoverView.swift @@ -0,0 +1,92 @@ +import SnapKit +import UIKit + +extension CourseInfoPurchaseModalCourseCoverView { + struct Appearance { + let coverImageViewSize = CGSize(width: 48, height: 48) + let coverImageViewCornerRadius: CGFloat = 8 + let coverImageViewInsets = LayoutInsets(left: 16) + + let titleFont = UIFont.systemFont(ofSize: 19, weight: .semibold) + let titleTextColor = UIColor.stepikMaterialPrimaryText + let titleInsets = LayoutInsets(horizontal: 16) + } +} + +final class CourseInfoPurchaseModalCourseCoverView: UIView { + let appearance: Appearance + + private lazy var coverImageView: CourseCoverImageView = { + let view = CourseCoverImageView() + view.setRoundedCorners(cornerRadius: self.appearance.coverImageViewCornerRadius) + return view + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = self.appearance.titleFont + label.textColor = self.appearance.titleTextColor + label.numberOfLines = 0 + return label + }() + + var coverURL: URL? { + didSet { + self.coverImageView.loadImage(url: self.coverURL) + } + } + + var titleText: String? { + didSet { + self.titleLabel.text = self.titleText + self.invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + CGSize( + width: UIView.noIntrinsicMetric, + height: max(self.appearance.coverImageViewSize.height, self.titleLabel.intrinsicContentSize.height) + ) + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension CourseInfoPurchaseModalCourseCoverView: ProgrammaticallyInitializableViewProtocol { + func addSubviews() { + self.addSubview(self.coverImageView) + self.addSubview(self.titleLabel) + } + + func makeConstraints() { + self.coverImageView.translatesAutoresizingMaskIntoConstraints = false + self.coverImageView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.leading.equalToSuperview().offset(self.appearance.coverImageViewInsets.left) + make.size.equalTo(self.appearance.coverImageViewSize) + } + + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.titleLabel.snp.makeConstraints { make in + make.top.equalTo(self.coverImageView.snp.top) + make.leading.equalTo(self.coverImageView.snp.trailing).offset(self.appearance.titleInsets.left) + make.bottom.lessThanOrEqualToSuperview() + make.trailing.equalToSuperview().offset(-self.appearance.titleInsets.right) + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalDisclaimerView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalDisclaimerView.swift new file mode 100644 index 0000000000..8d2064b372 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalDisclaimerView.swift @@ -0,0 +1,112 @@ +import Atributika +import SnapKit +import UIKit + +extension CourseInfoPurchaseModalDisclaimerView { + struct Appearance { + let font = Typography.caption1Font + let textColor = UIColor.stepikMaterialPrimaryText + let linkTextColor = UIColor.stepikVioletFixed + let insets = LayoutInsets.default + } +} + +final class CourseInfoPurchaseModalDisclaimerView: UIView { + let appearance: Appearance + + private let htmlToAttributedStringConverter: HTMLToAttributedStringConverterProtocol + + private lazy var topSeparatorView = SeparatorView() + + private lazy var attributedLabel: AttributedLabel = { + let label = AttributedLabel() + label.font = self.appearance.font + label.textColor = self.appearance.textColor + label.onClick = self.handleAttributedLabelClicked + label.numberOfLines = 0 + return label + }() + + var onLinkClick: ((URL) -> Void)? + + override var intrinsicContentSize: CGSize { + let topSeparatorHeight = self.topSeparatorView.intrinsicContentSize.height + let attributedLabelSize = self.attributedLabel.sizeThatFits(CGSize(width: self.bounds.width, height: .infinity)) + return CGSize( + width: UIView.noIntrinsicMetric, + height: topSeparatorHeight + self.appearance.insets.top + attributedLabelSize.height + ) + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance(), + htmlToAttributedStringConverter: HTMLToAttributedStringConverterProtocol? = nil + ) { + self.appearance = appearance + + if let htmlToAttributedStringConverter = htmlToAttributedStringConverter { + self.htmlToAttributedStringConverter = htmlToAttributedStringConverter + } else { + self.htmlToAttributedStringConverter = HTMLToAttributedStringConverter( + font: appearance.font, + tagStyles: [ + Style("a") + .foregroundColor(appearance.linkTextColor, .normal) + .foregroundColor(appearance.linkTextColor.withAlphaComponent(0.5), .highlighted) + ] + ) + } + + super.init(frame: frame) + + self.addSubviews() + self.makeConstraints() + + self.attributedLabel.attributedText = self.htmlToAttributedStringConverter.convertToAttributedText( + htmlString: NSLocalizedString("CourseInfoPurchaseModalDisclaimer", comment: "") + ) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func handleAttributedLabelClicked(label: AttributedLabel, detection: Detection) { + switch detection.type { + case .link(let url): + self.onLinkClick?(url) + case .tag(let tag): + if tag.name == "a", + let href = tag.attributes["href"], + let url = URL(string: href) { + self.onLinkClick?(url) + } + default: + break + } + } +} + +extension CourseInfoPurchaseModalDisclaimerView: ProgrammaticallyInitializableViewProtocol { + func addSubviews() { + self.addSubview(self.topSeparatorView) + self.addSubview(self.attributedLabel) + } + + func makeConstraints() { + self.topSeparatorView.translatesAutoresizingMaskIntoConstraints = false + self.topSeparatorView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(self.appearance.insets.edgeInsets) + } + + self.attributedLabel.translatesAutoresizingMaskIntoConstraints = false + self.attributedLabel.snp.makeConstraints { make in + make.top.equalTo(self.topSeparatorView.snp.bottom).offset(self.appearance.insets.top) + make.bottom.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(self.appearance.insets.edgeInsets) + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalHeaderView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalHeaderView.swift new file mode 100644 index 0000000000..c2b94a2f17 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalHeaderView.swift @@ -0,0 +1,98 @@ +import SnapKit +import UIKit + +extension CourseInfoPurchaseModalHeaderView { + struct Appearance { + let titleFont = Typography.headlineFont + let titleTextColor = UIColor.stepikMaterialPrimaryText + + let closeButtonFont = Typography.bodyFont + let closeButtonTextColor = UIColor.stepikVioletFixed + + let titleLabelInsets = LayoutInsets(vertical: 16) + let closeButtonInsets = LayoutInsets(right: 16) + } +} + +final class CourseInfoPurchaseModalHeaderView: UIView { + let appearance: Appearance + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("CourseInfoPurchaseModalTitle", comment: "") + label.font = self.appearance.titleFont + label.textColor = self.appearance.titleTextColor + label.textAlignment = .center + label.numberOfLines = 1 + return label + }() + + private lazy var closeButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle(NSLocalizedString("Close", comment: ""), for: .normal) + button.titleLabel?.font = self.appearance.closeButtonFont + button.setTitleColor(self.appearance.closeButtonTextColor, for: .normal) + button.addTarget(self, action: #selector(self.closeButtonClicked), for: .touchUpInside) + return button + }() + + private lazy var separatorView = SeparatorView() + + var onCloseClick: (() -> Void)? + + override var intrinsicContentSize: CGSize { + let height = self.appearance.titleLabelInsets.top + + self.titleLabel.intrinsicContentSize.height + + self.appearance.titleLabelInsets.bottom + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc + private func closeButtonClicked() { + self.onCloseClick?() + } +} + +extension CourseInfoPurchaseModalHeaderView: ProgrammaticallyInitializableViewProtocol { + func addSubviews() { + self.addSubview(self.titleLabel) + self.addSubview(self.closeButton) + self.addSubview(self.separatorView) + } + + func makeConstraints() { + self.titleLabel.translatesAutoresizingMaskIntoConstraints = false + self.titleLabel.snp.makeConstraints { make in + make.center.equalToSuperview() + make.top.greaterThanOrEqualToSuperview().offset(self.appearance.titleLabelInsets.top) + make.bottom.lessThanOrEqualToSuperview().offset(-self.appearance.titleLabelInsets.bottom) + } + + self.closeButton.translatesAutoresizingMaskIntoConstraints = false + self.closeButton.snp.makeConstraints { make in + make.centerY.equalTo(self.titleLabel.snp.centerY) + make.trailing.equalToSuperview().offset(-self.appearance.closeButtonInsets.right) + } + + self.separatorView.translatesAutoresizingMaskIntoConstraints = false + self.separatorView.snp.makeConstraints { make in + make.leading.bottom.trailing.equalToSuperview() + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalView.swift new file mode 100644 index 0000000000..992e7d3ae6 --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/CourseInfoPurchaseModalView.swift @@ -0,0 +1,170 @@ +import SnapKit +import UIKit + +protocol CourseInfoPurchaseModalViewDelegate: AnyObject { + func courseInfoPurchaseModalViewDidClickCloseButton(_ view: CourseInfoPurchaseModalView) + func courseInfoPurchaseModalView(_ view: CourseInfoPurchaseModalView, didClickLink link: URL) + func courseInfoPurchaseModalViewDidClickBuyButton(_ view: CourseInfoPurchaseModalView) + func courseInfoPurchaseModalViewDidClickWishlistButton(_ view: CourseInfoPurchaseModalView) +} + +extension CourseInfoPurchaseModalView { + struct Appearance { + let backgroundColor = UIColor.stepikBackground + + let stackViewSpacing: CGFloat = 16 + let stackViewInsets = LayoutInsets(inset: 16) + } +} + +final class CourseInfoPurchaseModalView: UIView { + weak var delegate: CourseInfoPurchaseModalViewDelegate? + + let appearance: Appearance + + private lazy var headerView = CourseInfoPurchaseModalHeaderView() + + private lazy var coverView = CourseInfoPurchaseModalCourseCoverView() + + private lazy var promoCodeView = CourseInfoPurchaseModalPromoCodeView() + + private lazy var disclaimerView = CourseInfoPurchaseModalDisclaimerView() + + private lazy var actionButtonsView = CourseInfoPurchaseModalActionButtonsView() + + private lazy var loadingIndicator: UIActivityIndicatorView = { + let loadingIndicatorView = UIActivityIndicatorView(style: .stepikGray) + loadingIndicatorView.hidesWhenStopped = true + loadingIndicatorView.startAnimating() + return loadingIndicatorView + }() + + private lazy var scrollableStackView: ScrollableStackView = { + let scrollableStackView = ScrollableStackView(orientation: .vertical) + scrollableStackView.spacing = self.appearance.stackViewSpacing + return scrollableStackView + }() + + var contentInsets: UIEdgeInsets { + get { + self.scrollableStackView.contentInsets + } + set { + self.scrollableStackView.contentInsets = newValue + } + } + + override var intrinsicContentSize: CGSize { + if self.loadingIndicator.isAnimating { + return CGSize( + width: UIView.noIntrinsicMetric, + height: self.loadingIndicator.intrinsicContentSize.height + ) + } + + let contentSize = self.scrollableStackView.contentSize + let height = contentSize.height + self.appearance.stackViewSpacing + + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func showLoading() { + self.scrollableStackView.isHidden = true + self.loadingIndicator.startAnimating() + } + + func hideLoading() { + self.scrollableStackView.isHidden = false + self.loadingIndicator.stopAnimating() + } + + func configure(viewModel: CourseInfoPurchaseModalViewModel) { + self.coverView.coverURL = viewModel.courseCoverImageURL + self.coverView.titleText = viewModel.courseTitle + } +} + +extension CourseInfoPurchaseModalView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.backgroundColor = self.appearance.backgroundColor + + self.headerView.onCloseClick = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.delegate?.courseInfoPurchaseModalViewDidClickCloseButton(strongSelf) + } + + self.disclaimerView.onLinkClick = { [weak self] link in + guard let strongSelf = self else { + return + } + + strongSelf.delegate?.courseInfoPurchaseModalView(strongSelf, didClickLink: link) + } + + self.actionButtonsView.onBuyButtonClick = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.delegate?.courseInfoPurchaseModalViewDidClickBuyButton(strongSelf) + } + self.actionButtonsView.onWishlistButtonClick = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.delegate?.courseInfoPurchaseModalViewDidClickWishlistButton(strongSelf) + } + } + + func addSubviews() { + self.addSubview(self.scrollableStackView) + self.addSubview(self.loadingIndicator) + + self.scrollableStackView.addArrangedView(self.headerView) + self.scrollableStackView.addArrangedView(self.coverView) + self.scrollableStackView.addArrangedView(self.promoCodeView) + self.scrollableStackView.addArrangedView(self.disclaimerView) + self.scrollableStackView.addArrangedView(self.actionButtonsView) + } + + func makeConstraints() { + self.loadingIndicator.translatesAutoresizingMaskIntoConstraints = false + self.loadingIndicator.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.centerY.equalToSuperview().multipliedBy(0.4) + } + + self.scrollableStackView.translatesAutoresizingMaskIntoConstraints = false + self.scrollableStackView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.trailing.equalTo(self.safeAreaLayoutGuide) + } + } +} + +// MARK: - CourseInfoPurchaseModalView: PanModalScrollable - + +extension CourseInfoPurchaseModalView: PanModalScrollable { + var panScrollable: UIScrollView? { self.scrollableStackView.panScrollable } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/PromoCode/CourseInfoPurchaseModalPromoCodeRightDetailView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/PromoCode/CourseInfoPurchaseModalPromoCodeRightDetailView.swift new file mode 100644 index 0000000000..2df286dece --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/PromoCode/CourseInfoPurchaseModalPromoCodeRightDetailView.swift @@ -0,0 +1,160 @@ +import SnapKit +import UIKit + +extension CourseInfoPurchaseModalPromoCodeRightDetailView { + struct Appearance { + let iconImageViewSize = CGSize(width: 18, height: 22) + + let cornerRadius: CGFloat = 8 + } +} + +final class CourseInfoPurchaseModalPromoCodeRightDetailView: UIControl { + let appearance: Appearance + + private lazy var iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + var viewState = ViewState.idle { + didSet { + self.updateViewState() + } + } + + override var isHighlighted: Bool { + didSet { + self.alpha = self.isHighlighted ? 0.5 : 1.0 + } + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + + self.updateViewState() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + self.performBlockIfAppearanceChanged(from: previousTraitCollection) { + self.updateBorder() + } + } + + private func updateViewState() { + self.backgroundColor = self.viewState.backgroundColor + self.updateBorder() + + self.iconImageView.image = self.viewState.iconImage + self.iconImageView.tintColor = self.viewState.iconTintColor + } + + private func updateBorder() { + self.layer.borderWidth = self.viewState.borderWidth + self.layer.borderColor = self.viewState.borderColor?.cgColor + } + + enum ViewState { + case idle + case loading + case error + case success + + fileprivate var backgroundColor: UIColor { + switch self { + case .idle: + return .stepikVioletFixed + case .loading: + return .clear + case .error: + return .stepikDiscountPriceText.withAlphaComponent(0.12) + case .success: + return .stepikGreenFixed.withAlphaComponent(0.12) + } + } + + fileprivate var borderWidth: CGFloat { + switch self { + case .idle, .error, .success: + return 0 + case .loading: + return 1 + } + } + + fileprivate var borderColor: UIColor? { + switch self { + case .idle, .error, .success: + return nil + case .loading: + return .stepikVioletFixed + } + } + + fileprivate var iconImage: UIImage? { + let imageName: String + + switch self { + case .idle: + imageName = "arrow.right" + case .loading: + imageName = "course-info-syllabus-in-progress" + case .error: + imageName = "quiz-mark-wrong" + case .success: + imageName = "quiz-mark-correct" + } + + return UIImage(named: imageName)?.withRenderingMode(.alwaysTemplate) + } + + fileprivate var iconTintColor: UIColor { + switch self { + case .idle: + return .white + case .loading: + return .stepikVioletFixed + case .error: + return .stepikDiscountPriceText + case .success: + return .stepikGreenFixed + } + } + } +} + +extension CourseInfoPurchaseModalPromoCodeRightDetailView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.layer.cornerRadius = self.appearance.cornerRadius + self.layer.masksToBounds = true + self.clipsToBounds = true + } + + func addSubviews() { + self.addSubview(self.iconImageView) + } + + func makeConstraints() { + self.iconImageView.translatesAutoresizingMaskIntoConstraints = false + self.iconImageView.snp.makeConstraints { make in + make.center.equalToSuperview() + make.size.equalTo(self.appearance.iconImageViewSize) + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/PromoCode/CourseInfoPurchaseModalPromoCodeView.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/PromoCode/CourseInfoPurchaseModalPromoCodeView.swift new file mode 100644 index 0000000000..cc7712928c --- /dev/null +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/Views/PromoCode/CourseInfoPurchaseModalPromoCodeView.swift @@ -0,0 +1,264 @@ +import SnapKit +import UIKit + +extension CourseInfoPurchaseModalPromoCodeView { + struct Appearance { + let revealInputButtonFont = Typography.bodyFont + let revealInputButtonTintColor = UIColor.stepikGreenFixed + let revealInputButtonImageSize = CGSize(width: 15, height: 15) + let revealInputButtonImageInsets = UIEdgeInsets(top: 4, left: 8, bottom: 0, right: 0) + let revealInputButtonInsets = UIEdgeInsets(top: 16, left: 0, bottom: 0, right: 16) + + let textFieldFont = Typography.bodyFont + let textFieldPlaceholderColor = UIColor.stepikMaterialDisabledText + let textFieldTextColor = UIColor.stepikMaterialPrimaryText + let textFieldCornerRadius: CGFloat = 8 + let textFieldBorderWidth: CGFloat = 1 + let textFieldBorderColor = UIColor.stepikSeparator + let textFieldInsets = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) + let textFieldClearButtonInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16) + + let inputStackViewHeight: CGFloat = 44 + let rightDetailViewWidth: CGFloat = 52 + + let statusLabelFont = Typography.caption1Font + + let stackViewSpacing: CGFloat = 16 + let stackViewInsets = LayoutInsets(horizontal: 16) + } +} + +final class CourseInfoPurchaseModalPromoCodeView: UIView { + let appearance: Appearance + + private lazy var revealInputButton: ImageButton = { + let button = ImageButton() + button.tintColor = self.appearance.revealInputButtonTintColor + button.title = NSLocalizedString("CourseInfoPurchaseModalRevealPromoCodeInputTitle", comment: "") + button.font = self.appearance.revealInputButtonFont + button.image = UIImage(named: "code-quiz-arrow-down")?.withRenderingMode(.alwaysTemplate) + button.imageSize = self.appearance.revealInputButtonImageSize + button.imageInsets = self.appearance.revealInputButtonImageInsets + button.imagePosition = .right + button.addTarget(self, action: #selector(self.revealInputButtonClicked), for: .touchUpInside) + return button + }() + + private lazy var textField: TableInputTextField = { + let textField = TableInputTextField() + textField.placeholderColor = self.appearance.textFieldPlaceholderColor + textField.placeholder = NSLocalizedString("CourseInfoPurchaseModalInputPlaceholder", comment: "") + textField.textColor = self.appearance.textFieldTextColor + textField.font = self.appearance.textFieldFont + textField.textInsets = self.appearance.textFieldInsets + textField.setRoundedCorners( + cornerRadius: self.appearance.textFieldCornerRadius, + borderWidth: self.appearance.textFieldBorderWidth, + borderColor: self.appearance.textFieldBorderColor + ) + textField.clearButtonInsets = self.appearance.textFieldClearButtonInsets + textField.clearButtonMode = .whileEditing + textField.addTarget(self, action: #selector(self.textFieldTextChanged), for: .editingChanged) + + // Disable features + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.spellCheckingType = .no + textField.smartDashesType = .no + textField.smartQuotesType = .no + textField.smartInsertDeleteType = .no + + return textField + }() + + private lazy var rightDetailView = CourseInfoPurchaseModalPromoCodeRightDetailView() + + private lazy var statusLabel: UILabel = { + let label = UILabel() + label.font = self.appearance.statusLabelFont + label.textAlignment = .left + label.numberOfLines = 1 + return label + }() + + // textField -> rightDetailView + private lazy var inputStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = self.appearance.stackViewSpacing + return stackView + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = self.appearance.stackViewSpacing + return stackView + }() + + var state = State.idle { + didSet { + if oldValue != self.state { + self.updateState() + } + } + } + + override var intrinsicContentSize: CGSize { + let stackViewIntrinsicContentSize = self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return CGSize(width: UIView.noIntrinsicMetric, height: stackViewIntrinsicContentSize.height) + } + + init( + frame: CGRect = .zero, + appearance: Appearance = Appearance() + ) { + self.appearance = appearance + super.init(frame: frame) + + self.setupView() + self.addSubviews() + self.makeConstraints() + + self.updateState() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateState() { + switch self.state { + case .idle: + self.revealInputButton.isHidden = false + self.inputStackView.isHidden = true + self.rightDetailView.isHidden = true + self.statusLabel.isHidden = true + case .typing: + self.revealInputButton.isHidden = true + self.inputStackView.isHidden = false + self.statusLabel.isHidden = true + + self.rightDetailView.isUserInteractionEnabled = true + self.rightDetailView.viewState = .idle + + self.isUserInteractionEnabled = true + case .loading: + self.statusLabel.isHidden = false + + self.rightDetailView.isUserInteractionEnabled = false + self.rightDetailView.viewState = .loading + + self.isUserInteractionEnabled = false + case .error: + self.statusLabel.isHidden = false + + self.rightDetailView.isUserInteractionEnabled = false + self.rightDetailView.viewState = .error + + self.isUserInteractionEnabled = true + case .success: + self.statusLabel.isHidden = false + + self.rightDetailView.isUserInteractionEnabled = false + self.rightDetailView.viewState = .success + + self.isUserInteractionEnabled = true + } + + self.statusLabel.text = self.state.statusLabelText + if let statusLabelTextColor = self.state.statusLabelTextColor { + self.statusLabel.textColor = statusLabelTextColor + } + } + + @objc + private func revealInputButtonClicked() { + self.state = .typing + } + + @objc + private func rightDetailViewClicked() { + guard self.state == .typing else { + return + } + + self.state = .loading + } + + @objc + private func textFieldTextChanged() { + let text = self.textField.text ?? "" + self.rightDetailView.isHidden = text.isEmpty + } + + enum State { + case idle + case typing + case loading + case error + case success + + fileprivate var statusLabelText: String? { + switch self { + case .idle, .typing: + return nil + case .loading: + return NSLocalizedString("CourseInfoPurchaseModalPromoCodeStatusLoading", comment: "") + case .error: + return NSLocalizedString("CourseInfoPurchaseModalPromoCodeStatusError", comment: "") + case .success: + return NSLocalizedString("CourseInfoPurchaseModalPromoCodeStatusSuccess", comment: "") + } + } + + fileprivate var statusLabelTextColor: UIColor? { + switch self { + case .idle, .typing: + return nil + case .loading: + return .stepikVioletFixed + case .error: + return .stepikDiscountPriceText + case .success: + return .stepikGreenFixed + } + } + } +} + +extension CourseInfoPurchaseModalPromoCodeView: ProgrammaticallyInitializableViewProtocol { + func setupView() { + self.rightDetailView.addTarget(self, action: #selector(self.rightDetailViewClicked), for: .touchUpInside) + } + + func addSubviews() { + self.addSubview(self.stackView) + + self.stackView.addArrangedSubview(self.revealInputButton) + self.stackView.addArrangedSubview(self.inputStackView) + self.stackView.addArrangedSubview(self.statusLabel) + + self.inputStackView.addArrangedSubview(self.textField) + self.inputStackView.addArrangedSubview(self.rightDetailView) + } + + func makeConstraints() { + self.stackView.translatesAutoresizingMaskIntoConstraints = false + self.stackView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(self.appearance.stackViewInsets.edgeInsets) + } + + self.inputStackView.translatesAutoresizingMaskIntoConstraints = false + self.inputStackView.snp.makeConstraints { make in + make.height.equalTo(self.appearance.inputStackViewHeight) + } + + self.rightDetailView.translatesAutoresizingMaskIntoConstraints = false + self.rightDetailView.snp.makeConstraints { make in + make.width.equalTo(self.appearance.rightDetailViewWidth) + } + } +} diff --git a/Stepic/Sources/Modules/NewProfileSubmodules/Certificates/NewProfileCertificatesInteractor.swift b/Stepic/Sources/Modules/NewProfileSubmodules/Certificates/NewProfileCertificatesInteractor.swift index 5647dc4725..4b365074d6 100644 --- a/Stepic/Sources/Modules/NewProfileSubmodules/Certificates/NewProfileCertificatesInteractor.swift +++ b/Stepic/Sources/Modules/NewProfileSubmodules/Certificates/NewProfileCertificatesInteractor.swift @@ -80,7 +80,7 @@ final class NewProfileCertificatesInteractor: NewProfileCertificatesInteractorPr isOnline: Bool ) -> Promise { Promise { seal in - firstly { + DispatchQueue.main.promise { () -> Promise<[Certificate]> in isOnline && self.didLoadFromCache ? self.provider.fetchRemote(userID: userID) : self.provider.fetchCached(userID: userID) diff --git a/Stepic/Sources/Modules/Settings/SettingsAssembly.swift b/Stepic/Sources/Modules/Settings/SettingsAssembly.swift index ec39dda0a6..1ff15eea04 100644 --- a/Stepic/Sources/Modules/Settings/SettingsAssembly.swift +++ b/Stepic/Sources/Modules/Settings/SettingsAssembly.swift @@ -31,7 +31,6 @@ final class SettingsAssembly: Assembly { provider: provider, analytics: StepikAnalytics.shared, userAccountService: UserAccountService(), - remoteConfig: .shared, downloadsDeletionService: DownloadsDeletionService(), dataBackUpdateService: DataBackUpdateService.default ) diff --git a/Stepic/Sources/Modules/Settings/SettingsInteractor.swift b/Stepic/Sources/Modules/Settings/SettingsInteractor.swift index 05f9a55199..7d719c0021 100644 --- a/Stepic/Sources/Modules/Settings/SettingsInteractor.swift +++ b/Stepic/Sources/Modules/Settings/SettingsInteractor.swift @@ -35,7 +35,6 @@ final class SettingsInteractor: SettingsInteractorProtocol { private let analytics: Analytics private let userAccountService: UserAccountServiceProtocol - private let remoteConfig: RemoteConfig private let downloadsDeletionService: DownloadsDeletionServiceProtocol private let dataBackUpdateService: DataBackUpdateServiceProtocol @@ -50,7 +49,7 @@ final class SettingsInteractor: SettingsInteractorProtocol { shouldUseCellularDataForDownloads: self.provider.shouldUseCellularDataForDownloads, isAutoplayEnabled: self.provider.isAutoplayEnabled, isAdaptiveModeEnabled: self.provider.isAdaptiveModeEnabled, - isDarkModeAvailable: self.remoteConfig.isDarkModeAvailable, + isDarkModeAvailable: self.provider.isDarkModeAvailable, isAuthorized: self.isAuthorized ) } @@ -66,7 +65,6 @@ final class SettingsInteractor: SettingsInteractorProtocol { provider: SettingsProviderProtocol, analytics: Analytics, userAccountService: UserAccountServiceProtocol, - remoteConfig: RemoteConfig, downloadsDeletionService: DownloadsDeletionServiceProtocol, dataBackUpdateService: DataBackUpdateServiceProtocol ) { @@ -74,7 +72,6 @@ final class SettingsInteractor: SettingsInteractorProtocol { self.provider = provider self.analytics = analytics self.userAccountService = userAccountService - self.remoteConfig = remoteConfig self.downloadsDeletionService = downloadsDeletionService self.dataBackUpdateService = dataBackUpdateService } diff --git a/Stepic/Sources/Modules/Settings/SettingsProvider.swift b/Stepic/Sources/Modules/Settings/SettingsProvider.swift index 73a71ed618..415b4dc367 100644 --- a/Stepic/Sources/Modules/Settings/SettingsProvider.swift +++ b/Stepic/Sources/Modules/Settings/SettingsProvider.swift @@ -15,6 +15,7 @@ protocol SettingsProviderProtocol: AnyObject { var globalStepFontSize: StepFontSize { get set } var availableStepFontSizes: [StepFontSize] { get } // ApplicationTheme + var isDarkModeAvailable: Bool { get } var globalApplicationTheme: ApplicationTheme { get set } var availableApplicationThemes: [ApplicationTheme] { get } @@ -82,6 +83,13 @@ final class SettingsProvider: SettingsProviderProtocol { var availableStepFontSizes: [StepFontSize] { StepFontSize.allCases } + var isDarkModeAvailable: Bool { + if #available(iOS 13.0, *) { + return true + } + return false + } + var globalApplicationTheme: ApplicationTheme { get { self.applicationThemeService.theme diff --git a/Stepic/Sources/Modules/Step/StepAssembly.swift b/Stepic/Sources/Modules/Step/StepAssembly.swift index 2bb8b879f9..7ab95f404a 100644 --- a/Stepic/Sources/Modules/Step/StepAssembly.swift +++ b/Stepic/Sources/Modules/Step/StepAssembly.swift @@ -30,8 +30,7 @@ final class StepAssembly: Assembly { stepID: self.stepID, presenter: presenter, provider: provider, - analytics: StepikAnalytics.shared, - remoteConfig: RemoteConfig.shared + analytics: StepikAnalytics.shared ) let viewController = StepViewController( interactor: interactor, diff --git a/Stepic/Sources/Modules/Step/StepDataFlow.swift b/Stepic/Sources/Modules/Step/StepDataFlow.swift index 19957a6c3e..4e0a4c0beb 100644 --- a/Stepic/Sources/Modules/Step/StepDataFlow.swift +++ b/Stepic/Sources/Modules/Step/StepDataFlow.swift @@ -9,7 +9,6 @@ enum StepDataFlow { let step: Step let stepFontSize: StepFontSize let storedImages: [StoredImage] - let isDisabledStepsSupported: Bool } struct Response { diff --git a/Stepic/Sources/Modules/Step/StepInteractor.swift b/Stepic/Sources/Modules/Step/StepInteractor.swift index ff16281cf7..10a7fd8e7c 100644 --- a/Stepic/Sources/Modules/Step/StepInteractor.swift +++ b/Stepic/Sources/Modules/Step/StepInteractor.swift @@ -23,8 +23,6 @@ final class StepInteractor: StepInteractorProtocol { private let provider: StepProviderProtocol private let analytics: Analytics - private let remoteConfig: RemoteConfig - private let stepID: Step.IdType private var currentStepPlainObject: StepPlainObject? @@ -38,13 +36,11 @@ final class StepInteractor: StepInteractorProtocol { stepID: Step.IdType, presenter: StepPresenterProtocol, provider: StepProviderProtocol, - analytics: Analytics, - remoteConfig: RemoteConfig + analytics: Analytics ) { self.presenter = presenter self.provider = provider self.analytics = analytics - self.remoteConfig = remoteConfig self.stepID = stepID } @@ -277,8 +273,7 @@ final class StepInteractor: StepInteractorProtocol { let data = StepDataFlow.StepLoad.Data( step: step, stepFontSize: stepFontSize, - storedImages: storedImages, - isDisabledStepsSupported: self.remoteConfig.isDisabledStepsSupported + storedImages: storedImages ) return .value(data) diff --git a/Stepic/Sources/Modules/Step/StepPresenter.swift b/Stepic/Sources/Modules/Step/StepPresenter.swift index 57cee1b84d..4ce3e9f6c9 100644 --- a/Stepic/Sources/Modules/Step/StepPresenter.swift +++ b/Stepic/Sources/Modules/Step/StepPresenter.swift @@ -28,7 +28,7 @@ final class StepPresenter: StepPresenterProtocol { func presentStep(response: StepDataFlow.StepLoad.Response) { switch response.result { case .success(let data): - if !data.step.isEnabled && data.isDisabledStepsSupported { + if !data.step.isEnabled { let viewModel = self.makeDisabledStepViewModel( step: data.step, stepFontSize: data.stepFontSize, diff --git a/Stepic/Sources/Services/ApplicationThemeService.swift b/Stepic/Sources/Services/ApplicationThemeService.swift index 3cacd44a5b..98a9aefb2e 100644 --- a/Stepic/Sources/Services/ApplicationThemeService.swift +++ b/Stepic/Sources/Services/ApplicationThemeService.swift @@ -22,8 +22,10 @@ final class ApplicationThemeService: ApplicationThemeServiceProtocol { get { if let userSelectedApplicationTheme = self.userSelectedApplicationTheme { return userSelectedApplicationTheme + } else if #available(iOS 13.0, *) { + return .default } else { - return self.remoteConfig.isDarkModeAvailable ? .default : .light + return .light } } set { @@ -40,10 +42,6 @@ final class ApplicationThemeService: ApplicationThemeServiceProtocol { func registerDefaultTheme() { if #available(iOS 13.0, *) { - guard self.remoteConfig.isDarkModeAvailable else { - return self.applyTheme(.light) - } - if let userSelectedApplicationTheme = self.userSelectedApplicationTheme { self.applyTheme(userSelectedApplicationTheme) } else { diff --git a/Stepic/Sources/Services/Models/Repository/CoursesRepository.swift b/Stepic/Sources/Services/Models/Repository/CoursesRepository.swift new file mode 100644 index 0000000000..1ea8dfa0ef --- /dev/null +++ b/Stepic/Sources/Services/Models/Repository/CoursesRepository.swift @@ -0,0 +1,66 @@ +import Foundation +import PromiseKit + +protocol CoursesRepositoryProtocol: AnyObject { + func fetch(id: Course.IdType, dataSourceType: DataSourceType) -> Promise +} + +extension CoursesRepositoryProtocol { + func fetch(id: Course.IdType, fetchPolicy: DataFetchPolicy) -> Promise { + switch fetchPolicy { + case .cacheFirst: + return Guarantee( + self.fetch(id: id, dataSourceType: .cache), + fallback: nil + ).then { cachedCourseOrNil -> Promise in + if let cachedCourse = cachedCourseOrNil?.flatMap({ $0 }) { + return .value(cachedCourse) + } else { + return self.fetch(id: id, dataSourceType: .remote) + } + } + case .remoteFirst: + return Guarantee( + self.fetch(id: id, dataSourceType: .remote), + fallback: nil + ).then { remoteCourseOrNil -> Promise in + if let remoteCourse = remoteCourseOrNil?.flatMap({ $0 }) { + return .value(remoteCourse) + } else { + return self.fetch(id: id, dataSourceType: .cache) + } + } + } + } +} + +final class CoursesRepository: CoursesRepositoryProtocol { + private let coursesNetworkService: CoursesNetworkServiceProtocol + private let coursesPersistenceService: CoursesPersistenceServiceProtocol + + init( + coursesNetworkService: CoursesNetworkServiceProtocol, + coursesPersistenceService: CoursesPersistenceServiceProtocol + ) { + self.coursesNetworkService = coursesNetworkService + self.coursesPersistenceService = coursesPersistenceService + } + + func fetch(id: Course.IdType, dataSourceType: DataSourceType) -> Promise { + switch dataSourceType { + case .remote: + return self.coursesNetworkService.fetch(id: id) + case .cache: + return self.coursesPersistenceService.fetch(id: id) + } + } +} + +extension CoursesRepository { + static var `default`: CoursesRepository { + CoursesRepository( + coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()), + coursesPersistenceService: CoursesPersistenceService() + ) + } +} diff --git a/Stepic/Sources/Views/TableInputTextField.swift b/Stepic/Sources/Views/TableInputTextField.swift index 8ae95be94f..cc7b788f41 100644 --- a/Stepic/Sources/Views/TableInputTextField.swift +++ b/Stepic/Sources/Views/TableInputTextField.swift @@ -47,6 +47,12 @@ class TableInputTextField: UITextField { self.pinnedPlaceholderLabel.frame.width } + var clearButtonInsets = UIEdgeInsets.zero { + didSet { + self.setNeedsDisplay() + } + } + override var font: UIFont? { didSet { self.pinnedPlaceholderLabel.font = self.font @@ -132,6 +138,17 @@ class TableInputTextField: UITextField { } } + override func clearButtonRect(forBounds bounds: CGRect) -> CGRect { + let rect = super.clearButtonRect(forBounds: bounds) + return CGRect( + origin: CGPoint( + x: rect.origin.x + self.clearButtonInsets.left - self.clearButtonInsets.right, + y: rect.origin.y + self.clearButtonInsets.top - self.clearButtonInsets.bottom + ), + size: rect.size + ) + } + private func updateLeftViewMode() { if self.shouldAlwaysShowPlaceholder { self.leftViewMode = .always diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 9aef49b98b..596a2e26e1 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -526,6 +526,15 @@ CourseInfoTabNewsDateOnEventSent = "%@ - %@"; CourseInfoTabNewsDateOneTimeScheduled = "Scheduled for %@"; CourseInfoTabNewsDateOneTimeSending = "From %@"; +/* CourseInfoPurchaseModal */ +CourseInfoPurchaseModalTitle = "Course purchase"; +CourseInfoPurchaseModalRevealPromoCodeInputTitle = "I have a promo code"; +CourseInfoPurchaseModalInputPlaceholder = "Enter a promo code"; +CourseInfoPurchaseModalPromoCodeStatusLoading = "Checking the promo code"; +CourseInfoPurchaseModalPromoCodeStatusError = "Invalid promo code"; +CourseInfoPurchaseModalPromoCodeStatusSuccess = "Promo code applied"; +CourseInfoPurchaseModalDisclaimer = "В цену включена 15% комиссия App Store. Оплачивая доступ к этому курсу вы соглашаетесь с условиями пользовательского соглашения."; + /* Course revenue */ CourseRevenueTitle = "Revenue"; CourseRevenueTabPurchasesAndRefunds = "Purchases and Refunds"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index a673d01440..51dd1bbd7c 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -528,6 +528,15 @@ CourseInfoTabNewsDateOnEventSent = "%@ - %@"; CourseInfoTabNewsDateOneTimeScheduled = "Запланирована на %@"; CourseInfoTabNewsDateOneTimeSending = "С %@"; +/* CourseInfoPurchaseModal */ +CourseInfoPurchaseModalTitle = "Покупка курса"; +CourseInfoPurchaseModalRevealPromoCodeInputTitle = "У меня есть промокод"; +CourseInfoPurchaseModalInputPlaceholder = "Введите промокод"; +CourseInfoPurchaseModalPromoCodeStatusLoading = "Проверка промокода"; +CourseInfoPurchaseModalPromoCodeStatusError = "Неверный промокод"; +CourseInfoPurchaseModalPromoCodeStatusSuccess = "Промокод применен"; +CourseInfoPurchaseModalDisclaimer = "В цену включена 15% комиссия App Store. Оплачивая доступ к этому курсу вы соглашаетесь с условиями пользовательского соглашения."; + /* Course revenue */ CourseRevenueTitle = "Доходы"; CourseRevenueTabPurchasesAndRefunds = "Покупки и возвраты"; diff --git a/StepicTests/Info-Develop.plist b/StepicTests/Info-Develop.plist index ab5a6f9d31..e027baa388 100644 --- a/StepicTests/Info-Develop.plist +++ b/StepicTests/Info-Develop.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.194-develop + 1.195-develop CFBundleSignature ???? CFBundleVersion - 383 + 385 diff --git a/StepicTests/Info-Production.plist b/StepicTests/Info-Production.plist index 658cbef207..273c927f9f 100644 --- a/StepicTests/Info-Production.plist +++ b/StepicTests/Info-Production.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.194 + 1.195 CFBundleSignature ???? CFBundleVersion - 383 + 385 diff --git a/StepicTests/Info-Release.plist b/StepicTests/Info-Release.plist index b803db4610..c3424d0dff 100644 --- a/StepicTests/Info-Release.plist +++ b/StepicTests/Info-Release.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.194-release + 1.195-release CFBundleSignature ???? CFBundleVersion - 383 + 385 diff --git a/StepicTests/Sources/Tests/ExtensionsTests/DictionaryExtensionsTests.swift b/StepicTests/Sources/Tests/ExtensionsTests/DictionaryExtensionsTests.swift new file mode 100644 index 0000000000..fea37544cc --- /dev/null +++ b/StepicTests/Sources/Tests/ExtensionsTests/DictionaryExtensionsTests.swift @@ -0,0 +1,39 @@ +@testable +import Stepic + +import XCTest + +final class DictionaryExtensionsTests: XCTestCase { + func testMapKeysAndValues() { + let intToString = [0: "0", 1: "1", 2: "2", 3: "3", 4: "4", 5: "5", 6: "6", 7: "7", 8: "8", 9: "9"] + + let stringToInt: [String: Int] = intToString.mapKeysAndValues { key, value in + return (String(describing: key), Int(value)!) + } + + XCTAssertEqual(stringToInt, ["0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9]) + } + + func testCompactMapKeysAndValues() { + enum IntWord: String { + case zero + case one + case two + } + + let strings = [ + 0: "zero", + 1: "one", + 2: "two", + 3: "three" + ] + let words: [String: IntWord] = strings.compactMapKeysAndValues { key, value in + guard let word = IntWord(rawValue: value) else { + return nil + } + return (String(describing: key), word) + } + + XCTAssertEqual(words, ["0": .zero, "1": .one, "2": .two]) + } +} diff --git a/StepicTests/Sources/Tests/ModulesTests/NewStepViewControllerTests.swift b/StepicTests/Sources/Tests/ModulesTests/NewStepViewControllerTests.swift index b993e73669..2984a7dbbc 100644 --- a/StepicTests/Sources/Tests/ModulesTests/NewStepViewControllerTests.swift +++ b/StepicTests/Sources/Tests/ModulesTests/NewStepViewControllerTests.swift @@ -91,8 +91,7 @@ class NewStepViewControllerSpec: QuickSpec { StepDataFlow.StepLoad.Data( step: step, stepFontSize: .small, - storedImages: [], - isDisabledStepsSupported: false + storedImages: [] ) ) ) @@ -167,8 +166,7 @@ class NewStepViewControllerSpec: QuickSpec { StepDataFlow.StepLoad.Data( step: step, stepFontSize: .small, - storedImages: [], - isDisabledStepsSupported: false + storedImages: [] ) ) ) @@ -246,8 +244,7 @@ class NewStepViewControllerSpec: QuickSpec { StepDataFlow.StepLoad.Data( step: step, stepFontSize: .small, - storedImages: [], - isDisabledStepsSupported: false + storedImages: [] ) ) ) diff --git a/StepicUITests/Info-Develop.plist b/StepicUITests/Info-Develop.plist index ea788da32d..23ea99684f 100644 --- a/StepicUITests/Info-Develop.plist +++ b/StepicUITests/Info-Develop.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.194-develop + 1.195-develop CFBundleVersion - 383 + 385 diff --git a/StepicUITests/Info-Production.plist b/StepicUITests/Info-Production.plist index 75dba20c0b..35619ea4f7 100644 --- a/StepicUITests/Info-Production.plist +++ b/StepicUITests/Info-Production.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.194 + 1.195 CFBundleVersion - 383 + 385 diff --git a/StepicUITests/Info-Release.plist b/StepicUITests/Info-Release.plist index 4e8f1e15a5..4a05641132 100644 --- a/StepicUITests/Info-Release.plist +++ b/StepicUITests/Info-Release.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.194-release + 1.195-release CFBundleVersion - 383 + 385 diff --git a/StepicUITests/Sources/LoginUITests.swift b/StepicUITests/Sources/LoginUITests.swift new file mode 100644 index 0000000000..e29b268270 --- /dev/null +++ b/StepicUITests/Sources/LoginUITests.swift @@ -0,0 +1,176 @@ +import XCTest + +class LoginUITests: XCTestCase { + override func setUp() { + super.setUp() + + self.continueAfterFailure = false + + XCUIApplication().launchArguments += ["-AppleLanguages", "(en)"] + XCUIApplication().launchArguments += ["-AppleLocale", "en_EN"] + } + + override func tearDown() { + XCUIApplication().terminate() + super.tearDown() + } + + func testUserCanLogInWithEmail() throws { + let app = XCUIApplication() + app.launch() + + if Common.isUserLoggedIn(app: app) { + Common.logOut(app: app) + } + + Common.registerNewUserIfNeeded() + + app.launch() + app.tabBars["Tab Bar"].buttons["Profile"].tap() + app.buttons["Sign In"].staticTexts["Sign In"].tap() + app.buttons["Sign In with e-mail"].tap() + + app.textFields["Email"].tap() + sleep(2) + Common.pasteTextFieldText( + app: app, + element: app.textFields["Email"], + value: currentUserEmail + ) + + app.secureTextFields["Password"].tap() + sleep(2) + Common.pasteTextFieldText( + app: app, + element: app.secureTextFields["Password"], + value: kCurrentUserPassword + ) + + app.buttons["Log in"].tap() + // Check user profile loaded + if app.tabBars["Tab Bar"].buttons["Profile"].waitForExistence(timeout: 5) { + app.tabBars["Tab Bar"].buttons["Profile"].tap() + } + + let elementsQuery = app.scrollViews.otherElements + + if elementsQuery.staticTexts[currentUserName].waitForExistence(timeout: 5) { + XCTAssertTrue(elementsQuery.staticTexts["Activity"].exists, "No Activity section") + XCTAssertTrue(elementsQuery.staticTexts["Achievements"].exists, "No Achievements section") + } else { + XCTFail("User could not login") + } + + app.terminate() + } + + func testUserCanLogInWithGoogle() throws { + // Adding Notification alert interruption + self.addUIInterruptionMonitor(withDescription: "“Stepik” Wants to Use “google.com” to Sign In") { alert in + let alertButton = alert.buttons["Continue"] + if alertButton.exists { + alertButton.tap() + return true + } + return false + } + let app = XCUIApplication() + app.launch() + if Common.isUserLoggedIn(app: app) { + Common.logOut(app: app) + } + app.launch() + app.tabBars["Tab Bar"].buttons["Profile"].tap() + app.buttons["Sign In"].staticTexts["Sign In"].tap() + app.collectionViews.children(matching: .cell).element(boundBy: 2).tap() + if app.webViews.webViews.webViews.staticTexts["Сменить аккаунт"].waitForExistence(timeout: 1) { + app.webViews.webViews.webViews.staticTexts["Сменить аккаунт"].tap() + } + app.tap() + app.webViews.webViews.webViews.textFields.element(boundBy: 0).waitForExistence(timeout: 5) + app.webViews.webViews.webViews.textFields.element(boundBy: 0).tap() + app.webViews.webViews.webViews.textFields.element(boundBy: 0).typeText("stepik.qa4@gmail.com") + app.tap() + app.webViews.webViews.webViews.buttons.element(boundBy: 2).tap() + sleep(3) + app.webViews.webViews.webViews.secureTextFields.element(boundBy: 0).tap() + app.webViews.webViews.webViews.secureTextFields.element(boundBy: 0).typeText("Qq0987654321Qq") + app.tap() + app.webViews.webViews.webViews.buttons.element(boundBy: 0).tap() + sleep(5) + if !Common.isUserLoggedIn(app: app) { + XCTFail("Login with google account failed") + } + } + + func testUserCanLogInWithFacebook() throws { + let app = XCUIApplication() + + // Adding Notification alert interruption + self.addUIInterruptionMonitor(withDescription: "“Stepik” Wants to Use “facebook.com” to Sign In") { alert in + let alertButton = alert.buttons["Continue"] + if alertButton.exists { + alertButton.tap() + return true + } + return false + } + app.launch() + if Common.isUserLoggedIn(app: app) { + Common.logOut(app: app) + } + app.launch() + app.tabBars["Tab Bar"].buttons["Profile"].tap() + app.buttons["Sign In"].staticTexts["Sign In"].tap() + app.buttons["More"].tap() + app.collectionViews.children(matching: .cell).element(boundBy: 3).tap() + sleep(5) + if app.collectionViews.children(matching: .cell).element(boundBy: 3).exists { + app.tap() + } + /* + app.webViews.webViews.webViews.staticTexts["..."].waitForExistence(timeout: 5) + if app.webViews.webViews.webViews.staticTexts["..."].exists { + app.webViews.webViews.webViews.staticTexts["..."].tap() + app.webViews.webViews.webViews.otherElements["main"].children(matching: .other).element(boundBy: 3).staticTexts["English (US)"].tap() + app.webViews.webViews.webViews.staticTexts["Accept All"].tap() + } + app.webViews.webViews.webViews.staticTexts["Русский"].tap() + */ + let textField = app.webViews.webViews.webViews.textFields.element(boundBy: 0) + textField.tap() + textField.typeText("ios_cmvthcs_autotest@tfbnw.net") + app.webViews.webViews.webViews.secureTextFields.element(boundBy: 0).tap() + app.webViews.webViews.webViews.secureTextFields.element(boundBy: 0).typeText("Test999Test") + + app.webViews.webViews.webViews.buttons.element(boundBy: 0).tap() + sleep(3) + + app.webViews.webViews.webViews.buttons.element(boundBy: 0).tap() + + if !Common.isUserLoggedIn(app: app) { + XCTFail("Login with facebook account failed") + } else { + Common.logOut(app: app) + } + } + + func testUserCanLogout() throws { + let app = XCUIApplication() + app.launch() + + if !Common.isUserLoggedIn(app: app) { + Common.registerNewUserIfNeeded() + app.launch() + Common.logIn(app: app) + } + + Common.logOut(app: app) + + if Common.isUserLoggedIn(app: app) { + XCTFail("User was not logged out") + } + + app.terminate() + } +} diff --git a/StepicUITests/Sources/StepicUITests.swift b/StepicUITests/Sources/UnregisteredUITests.swift similarity index 81% rename from StepicUITests/Sources/StepicUITests.swift rename to StepicUITests/Sources/UnregisteredUITests.swift index d215371215..836b49298d 100644 --- a/StepicUITests/Sources/StepicUITests.swift +++ b/StepicUITests/Sources/UnregisteredUITests.swift @@ -1,11 +1,6 @@ import XCTest -/* WARNING! - This tests needs make sure 'Hardware -> Keyboard -> Connect hardware keyboard' is off IN SIMULATOR. - Use "defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool false" in CI - */ - -class StepicUITests: XCTestCase { +class UnregisteredUITests: XCTestCase { override func setUp() { super.setUp() @@ -252,72 +247,4 @@ class StepicUITests: XCTestCase { } app.terminate() } - - func testUserCanLogIn() throws { - let app = XCUIApplication() - app.launch() - - if Common.isUserLoggedIn(app: app) { - Common.logOut(app: app) - } - - Common.registerNewUserIfNeeded() - - app.launch() - app.tabBars["Tab Bar"].buttons["Profile"].tap() - app.buttons["Sign In"].staticTexts["Sign In"].tap() - app.buttons["Sign In with e-mail"].tap() - - app.textFields["Email"].tap() - sleep(2) - Common.pasteTextFieldText( - app: app, - element: app.textFields["Email"], - value: currentUserEmail - ) - - app.secureTextFields["Password"].tap() - sleep(2) - Common.pasteTextFieldText( - app: app, - element: app.secureTextFields["Password"], - value: kCurrentUserPassword - ) - - app.buttons["Log in"].tap() - // Check user profile loaded - if app.tabBars["Tab Bar"].buttons["Profile"].waitForExistence(timeout: 5) { - app.tabBars["Tab Bar"].buttons["Profile"].tap() - } - - let elementsQuery = app.scrollViews.otherElements - - if elementsQuery.staticTexts[currentUserName].waitForExistence(timeout: 5) { - XCTAssertTrue(elementsQuery.staticTexts["Activity"].exists, "No Activity section") - XCTAssertTrue(elementsQuery.staticTexts["Achievements"].exists, "No Achievements section") - } else { - XCTFail("User could not login") - } - - app.terminate() - } - - func testUserCanLogout() throws { - let app = XCUIApplication() - app.launch() - - if !Common.isUserLoggedIn(app: app) { - Common.registerNewUserIfNeeded() - app.launch() - Common.logIn(app: app) - } - - Common.logOut(app: app) - - if Common.isUserLoggedIn(app: app) { - XCTFail("User was not logged out") - } - - app.terminate() - } } diff --git a/StepicWidget/Info-Develop.plist b/StepicWidget/Info-Develop.plist index 058c75d26a..7545dd070d 100644 --- a/StepicWidget/Info-Develop.plist +++ b/StepicWidget/Info-Develop.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.194-develop + 1.195-develop CFBundleVersion - 383 + 385 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Production.plist b/StepicWidget/Info-Production.plist index 0bcafaedb2..e6613b584c 100644 --- a/StepicWidget/Info-Production.plist +++ b/StepicWidget/Info-Production.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.194 + 1.195 CFBundleVersion - 383 + 385 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Release.plist b/StepicWidget/Info-Release.plist index 1f892f0e71..a74eb0f4ba 100644 --- a/StepicWidget/Info-Release.plist +++ b/StepicWidget/Info-Release.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.194-release + 1.195-release CFBundleVersion - 383 + 385 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Develop.plist b/StickerPackExtension/Info-Develop.plist index ee70d11417..bce6d70e06 100644 --- a/StickerPackExtension/Info-Develop.plist +++ b/StickerPackExtension/Info-Develop.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.194-develop + 1.195-develop CFBundleVersion - 383 + 385 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Production.plist b/StickerPackExtension/Info-Production.plist index 31ff296cec..80c3bc93d3 100644 --- a/StickerPackExtension/Info-Production.plist +++ b/StickerPackExtension/Info-Production.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.194 + 1.195 CFBundleVersion - 383 + 385 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Release.plist b/StickerPackExtension/Info-Release.plist index f347826897..71a59ef791 100644 --- a/StickerPackExtension/Info-Release.plist +++ b/StickerPackExtension/Info-Release.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.194-release + 1.195-release CFBundleVersion - 383 + 385 NSExtension NSExtensionPointIdentifier diff --git a/fastlane/release-notes.txt b/fastlane/release-notes.txt index 20e4447ed7..b46d6772a1 100644 --- a/fastlane/release-notes.txt +++ b/fastlane/release-notes.txt @@ -1,8 +1,4 @@ Что тестировать: -- Белый экран после перехода в шаг из поиска по курсу APPS-3471 -- Нет кнопки перехода на следующий урок APPS-3472 -- Рецензирование: - * Поддержка квизов APPS-3403 - Доходы по курсу: * Контейнер экрана доходов по курсу APPS-3337 * UI списка транзакций и экран с детальной информацией о транзакции APPS-3369