diff --git a/Gemfile b/Gemfile index a128f02c10..1d1c1e0cfc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" ruby "2.6.5" -gem "fastlane", "2.197.0" +gem "fastlane", "2.198.1" gem "cocoapods", "1.11.2" gem "generamba", "1.5.0" diff --git a/Gemfile.lock b/Gemfile.lock index 843b40855d..f0463b8e1b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.4) + CFPropertyList (3.0.5) rexml activesupport (6.1.4.1) concurrent-ruby (~> 1.0, >= 1.0.2) @@ -17,17 +17,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.518.0) - aws-sdk-core (3.121.3) + aws-partitions (1.534.0) + aws-sdk-core (3.123.0) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) + aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.50.0) - aws-sdk-core (~> 3, >= 3.121.2) + aws-sdk-kms (1.51.0) + aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.104.0) - aws-sdk-core (~> 3, >= 3.121.2) + aws-sdk-s3 (1.107.0) + aws-sdk-core (~> 3, >= 3.122.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -86,7 +86,7 @@ GEM escape (0.0.4) ethon (0.15.0) ffi (>= 1.15.0) - excon (0.87.0) + excon (0.88.0) faraday (1.8.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -112,7 +112,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.5) - fastlane (2.197.0) + fastlane (2.198.1) 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.12.0) + google-apis-androidpublisher_v3 (0.13.0) google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.1) addressable (~> 2.5, >= 2.5.1) @@ -175,11 +175,11 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.7.0) + google-apis-iamcredentials_v1 (0.8.0) google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.5.0) + google-apis-playcustomapp_v1 (0.6.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.8.0) + google-apis-storage_v1 (0.9.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) @@ -206,7 +206,7 @@ GEM http-cookie (1.0.4) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.8.10) + i18n (1.8.11) concurrent-ruby (~> 1.0) jmespath (1.4.0) json (2.6.1) @@ -224,7 +224,7 @@ GEM naturally (2.2.1) netrc (0.11.0) optparse (0.1.1) - os (1.1.1) + os (1.1.4) plist (3.6.0) public_suffix (4.0.6) rake (13.0.6) @@ -250,7 +250,7 @@ GEM terminal-notifier (2.0.0) terminal-table (1.4.5) thor (0.19.1) - trailblazer-option (0.1.1) + trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) @@ -283,7 +283,7 @@ PLATFORMS DEPENDENCIES cocoapods (= 1.11.2) - fastlane (= 2.197.0) + fastlane (= 2.198.1) fastlane-plugin-firebase_app_distribution generamba (= 1.5.0) diff --git a/Podfile b/Podfile index a76838e06e..15d6c7eba2 100644 --- a/Podfile +++ b/Podfile @@ -18,8 +18,8 @@ def shared_pods pod 'SwiftyJSON', '5.0.0' pod 'SDWebImage', '5.12.1' pod 'SVGKit', :git => 'https://github.com/SVGKit/SVGKit.git', :branch => '2.x' - pod 'DeviceKit', '4.5.1' - pod 'PromiseKit', '6.15.3' + pod 'DeviceKit', '4.5.2' + pod 'PromiseKit', :git => 'https://github.com/mxcl/PromiseKit.git', :tag => '6.16.2' pod 'SwiftLint', '0.45.0' if ENV['FASTLANE_BETA_PROFILE'] == 'true' @@ -43,15 +43,15 @@ def all_pods pod 'SnapKit', '5.0.1' # Firebase - pod 'Firebase/Core', '8.8.0' - pod 'Firebase/Messaging', '8.8.0' - pod 'Firebase/Analytics', '8.8.0' - pod 'Firebase/Crashlytics', '8.8.0' - pod 'Firebase/RemoteConfig', '8.8.0' + pod 'Firebase/Core', '8.9.1' + pod 'Firebase/Messaging', '8.9.1' + pod 'Firebase/Analytics', '8.9.1' + pod 'Firebase/Crashlytics', '8.9.1' + pod 'Firebase/RemoteConfig', '8.9.1' pod 'YandexMobileMetrica/Dynamic', '3.17.0' - pod 'Amplitude', '8.4.0' - pod 'Branch', '1.40.1' + pod 'Amplitude', '8.5.0' + pod 'Branch', '1.40.2' pod 'BEMCheckBox', '1.4.1' diff --git a/Podfile.lock b/Podfile.lock index b8a373a677..8fb5c5c655 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -3,7 +3,7 @@ PODS: - Agrume (5.6.13): - SwiftyGif - Alamofire (5.4.4) - - Amplitude (8.4.0) + - Amplitude (8.5.0) - AppAuth (1.4.0): - AppAuth/Core (= 1.4.0) - AppAuth/ExternalUserAgent (= 1.4.0) @@ -11,14 +11,14 @@ PODS: - AppAuth/ExternalUserAgent (1.4.0) - Atributika (4.10.1) - BEMCheckBox (1.4.1) - - Branch (1.40.1) + - Branch (1.40.2) - Charts (3.6.0): - Charts/Core (= 3.6.0) - Charts/Core (3.6.0) - CocoaLumberjack (3.7.2): - CocoaLumberjack/Core (= 3.7.2) - CocoaLumberjack/Core (3.7.2) - - DeviceKit (4.5.1) + - DeviceKit (4.5.2) - DownloadButton (0.1.0) - EasyTipView (2.1.0) - FBSDKCoreKit (8.2.0): @@ -31,98 +31,98 @@ PODS: - FBSDKLoginKit/Login (= 8.2.0) - FBSDKLoginKit/Login (8.2.0): - FBSDKCoreKit (~> 8.2.0) - - Firebase/Analytics (8.8.0): + - Firebase/Analytics (8.9.1): - Firebase/Core - - Firebase/Core (8.8.0): + - Firebase/Core (8.9.1): - Firebase/CoreOnly - - FirebaseAnalytics (~> 8.8.0) - - Firebase/CoreOnly (8.8.0): - - FirebaseCore (= 8.8.0) - - Firebase/Crashlytics (8.8.0): + - FirebaseAnalytics (~> 8.9.1) + - Firebase/CoreOnly (8.9.1): + - FirebaseCore (= 8.9.1) + - Firebase/Crashlytics (8.9.1): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 8.8.0) - - Firebase/Messaging (8.8.0): + - FirebaseCrashlytics (~> 8.9.0) + - Firebase/Messaging (8.9.1): - Firebase/CoreOnly - - FirebaseMessaging (~> 8.8.0) - - Firebase/RemoteConfig (8.8.0): + - FirebaseMessaging (~> 8.9.0) + - Firebase/RemoteConfig (8.9.1): - Firebase/CoreOnly - - FirebaseRemoteConfig (~> 8.8.0) - - FirebaseABTesting (8.8.0): + - FirebaseRemoteConfig (~> 8.9.0) + - FirebaseABTesting (8.9.0): - FirebaseCore (~> 8.0) - - FirebaseAnalytics (8.8.0): - - FirebaseAnalytics/AdIdSupport (= 8.8.0) + - FirebaseAnalytics (8.9.1): + - FirebaseAnalytics/AdIdSupport (= 8.9.1) - FirebaseCore (~> 8.0) - FirebaseInstallations (~> 8.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.4) - - GoogleUtilities/MethodSwizzler (~> 7.4) - - GoogleUtilities/Network (~> 7.4) - - "GoogleUtilities/NSData+zlib (~> 7.4)" + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/MethodSwizzler (~> 7.6) + - GoogleUtilities/Network (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" - nanopb (~> 2.30908.0) - - FirebaseAnalytics/AdIdSupport (8.8.0): + - FirebaseAnalytics/AdIdSupport (8.9.1): - FirebaseCore (~> 8.0) - FirebaseInstallations (~> 8.0) - - GoogleAppMeasurement (= 8.8.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.4) - - GoogleUtilities/MethodSwizzler (~> 7.4) - - GoogleUtilities/Network (~> 7.4) - - "GoogleUtilities/NSData+zlib (~> 7.4)" + - GoogleAppMeasurement (= 8.9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/MethodSwizzler (~> 7.6) + - GoogleUtilities/Network (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" - nanopb (~> 2.30908.0) - - FirebaseCore (8.8.0): + - FirebaseCore (8.9.1): - FirebaseCoreDiagnostics (~> 8.0) - - GoogleUtilities/Environment (~> 7.4) - - GoogleUtilities/Logger (~> 7.4) - - FirebaseCoreDiagnostics (8.8.0): - - GoogleDataTransport (~> 9.0) - - GoogleUtilities/Environment (~> 7.4) - - GoogleUtilities/Logger (~> 7.4) + - GoogleUtilities/Environment (~> 7.6) + - GoogleUtilities/Logger (~> 7.6) + - FirebaseCoreDiagnostics (8.9.0): + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.6) + - GoogleUtilities/Logger (~> 7.6) - nanopb (~> 2.30908.0) - - FirebaseCrashlytics (8.8.0): + - FirebaseCrashlytics (8.9.0): - FirebaseCore (~> 8.0) - FirebaseInstallations (~> 8.0) - - GoogleDataTransport (~> 9.0) - - GoogleUtilities/Environment (~> 7.4) + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.6) - nanopb (~> 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - FirebaseInstallations (8.8.0): + - FirebaseInstallations (8.9.0): - FirebaseCore (~> 8.0) - - GoogleUtilities/Environment (~> 7.4) - - GoogleUtilities/UserDefaults (~> 7.4) + - GoogleUtilities/Environment (~> 7.6) + - GoogleUtilities/UserDefaults (~> 7.6) - PromisesObjC (< 3.0, >= 1.2) - - FirebaseMessaging (8.8.0): + - FirebaseMessaging (8.9.0): - FirebaseCore (~> 8.0) - FirebaseInstallations (~> 8.0) - - GoogleDataTransport (~> 9.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.4) - - GoogleUtilities/Environment (~> 7.4) - - GoogleUtilities/Reachability (~> 7.4) - - GoogleUtilities/UserDefaults (~> 7.4) + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/Environment (~> 7.6) + - GoogleUtilities/Reachability (~> 7.6) + - GoogleUtilities/UserDefaults (~> 7.6) - nanopb (~> 2.30908.0) - - FirebaseRemoteConfig (8.8.0): + - FirebaseRemoteConfig (8.9.0): - FirebaseABTesting (~> 8.0) - FirebaseCore (~> 8.0) - FirebaseInstallations (~> 8.0) - - GoogleUtilities/Environment (~> 7.4) - - "GoogleUtilities/NSData+zlib (~> 7.4)" + - GoogleUtilities/Environment (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" - FLEX (4.4.1) - - GoogleAppMeasurement (8.8.0): - - GoogleAppMeasurement/AdIdSupport (= 8.8.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.4) - - GoogleUtilities/MethodSwizzler (~> 7.4) - - GoogleUtilities/Network (~> 7.4) - - "GoogleUtilities/NSData+zlib (~> 7.4)" + - GoogleAppMeasurement (8.9.1): + - GoogleAppMeasurement/AdIdSupport (= 8.9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/MethodSwizzler (~> 7.6) + - GoogleUtilities/Network (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" - nanopb (~> 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (8.8.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 8.8.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.4) - - GoogleUtilities/MethodSwizzler (~> 7.4) - - GoogleUtilities/Network (~> 7.4) - - "GoogleUtilities/NSData+zlib (~> 7.4)" + - GoogleAppMeasurement/AdIdSupport (8.9.1): + - GoogleAppMeasurement/WithoutAdIdSupport (= 8.9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/MethodSwizzler (~> 7.6) + - GoogleUtilities/Network (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" - nanopb (~> 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (8.8.0): - - GoogleUtilities/AppDelegateSwizzler (~> 7.4) - - GoogleUtilities/MethodSwizzler (~> 7.4) - - GoogleUtilities/Network (~> 7.4) - - "GoogleUtilities/NSData+zlib (~> 7.4)" + - GoogleAppMeasurement/WithoutAdIdSupport (8.9.1): + - GoogleUtilities/AppDelegateSwizzler (~> 7.6) + - GoogleUtilities/MethodSwizzler (~> 7.6) + - GoogleUtilities/Network (~> 7.6) + - "GoogleUtilities/NSData+zlib (~> 7.6)" - nanopb (~> 2.30908.0) - GoogleDataTransport (9.1.2): - GoogleUtilities/Environment (~> 7.2) @@ -132,24 +132,24 @@ PODS: - AppAuth (~> 1.2) - GTMAppAuth (~> 1.0) - GTMSessionFetcher/Core (~> 1.1) - - GoogleUtilities/AppDelegateSwizzler (7.5.2): + - GoogleUtilities/AppDelegateSwizzler (7.6.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.5.2): + - GoogleUtilities/Environment (7.6.0): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.5.2): + - GoogleUtilities/Logger (7.6.0): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.5.2): + - GoogleUtilities/MethodSwizzler (7.6.0): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.5.2): + - GoogleUtilities/Network (7.6.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.5.2)" - - GoogleUtilities/Reachability (7.5.2): + - "GoogleUtilities/NSData+zlib (7.6.0)" + - GoogleUtilities/Reachability (7.6.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.5.2): + - GoogleUtilities/UserDefaults (7.6.0): - GoogleUtilities/Logger - GTMAppAuth (1.2.2): - AppAuth/Core (~> 1.4) @@ -179,14 +179,14 @@ PODS: - PanModal (1.2.7) - pop (1.0.12) - Presentr (1.9) - - PromiseKit (6.15.3): - - PromiseKit/CorePromise (= 6.15.3) - - PromiseKit/Foundation (= 6.15.3) - - PromiseKit/UIKit (= 6.15.3) - - PromiseKit/CorePromise (6.15.3) - - PromiseKit/Foundation (6.15.3): + - PromiseKit (6.16.1): + - PromiseKit/CorePromise (= 6.16.1) + - PromiseKit/Foundation (= 6.16.1) + - PromiseKit/UIKit (= 6.16.1) + - PromiseKit/CorePromise (6.16.1) + - PromiseKit/Foundation (6.16.1): - PromiseKit/CorePromise - - PromiseKit/UIKit (6.15.3): + - PromiseKit/UIKit (6.16.1): - PromiseKit/CorePromise - PromisesObjC (2.0.0) - Quick (4.0.0) @@ -219,21 +219,21 @@ DEPENDENCIES: - ActionSheetPicker-3.0 (= 2.7.1) - Agrume (= 5.6.13) - Alamofire (= 5.4.4) - - Amplitude (= 8.4.0) + - Amplitude (= 8.5.0) - Atributika (= 4.10.1) - BEMCheckBox (= 1.4.1) - - Branch (= 1.40.1) + - Branch (= 1.40.2) - Charts (= 3.6.0) - - DeviceKit (= 4.5.1) + - DeviceKit (= 4.5.2) - DownloadButton (= 0.1.0) - EasyTipView (= 2.1.0) - FBSDKCoreKit (= 8.2.0) - FBSDKLoginKit (= 8.2.0) - - Firebase/Analytics (= 8.8.0) - - Firebase/Core (= 8.8.0) - - Firebase/Crashlytics (= 8.8.0) - - Firebase/Messaging (= 8.8.0) - - Firebase/RemoteConfig (= 8.8.0) + - Firebase/Analytics (= 8.9.1) + - Firebase/Core (= 8.9.1) + - Firebase/Crashlytics (= 8.9.1) + - Firebase/Messaging (= 8.9.1) + - Firebase/RemoteConfig (= 8.9.1) - FLEX (from `https://github.com/ivan-magda/FLEX.git`, branch `master`) - GoogleSignIn (= 5.0.2) - Highlightr (from `https://github.com/ivan-magda/Highlightr.git`, tag `v2.1.3`) @@ -246,7 +246,7 @@ DEPENDENCIES: - Nuke (= 9.5.0) - PanModal (from `https://github.com/ivan-magda/PanModal.git`, branch `remove-presenting-appearance-transitions`) - Presentr (from `https://github.com/ivan-magda/Presentr.git`, tag `v1.9.1`) - - PromiseKit (= 6.15.3) + - PromiseKit (from `https://github.com/mxcl/PromiseKit.git`, tag `6.16.2`) - Quick (= 4.0.0) - SDWebImage (= 5.12.1) - SnapKit (= 5.0.1) @@ -303,7 +303,6 @@ SPEC REPOS: - Nuke - Pageboy - pop - - PromiseKit - PromisesObjC - Quick - SDWebImage @@ -337,6 +336,9 @@ EXTERNAL SOURCES: Presentr: :git: https://github.com/ivan-magda/Presentr.git :tag: v1.9.1 + PromiseKit: + :git: https://github.com/mxcl/PromiseKit.git + :tag: 6.16.2 SVGKit: :branch: 2.x :git: https://github.com/SVGKit/SVGKit.git @@ -357,6 +359,9 @@ CHECKOUT OPTIONS: Presentr: :git: https://github.com/ivan-magda/Presentr.git :tag: v1.9.1 + PromiseKit: + :git: https://github.com/mxcl/PromiseKit.git + :tag: 6.16.2 SVGKit: :commit: c40671b9a264f8f71831c4e0452736debfae2164 :git: https://github.com/SVGKit/SVGKit.git @@ -365,32 +370,32 @@ SPEC CHECKSUMS: ActionSheetPicker-3.0: 36da254b97a09ff89679ecb8b8510bd3e5bdc773 Agrume: 21b96a1138abc0f890211bfcb12f8b1e3464b4c1 Alamofire: f3b09a368f1582ab751b3fff5460276e0d2cf5c9 - Amplitude: d0e5cf12748455c3b95cd9375e4293c4bb95a6a4 + Amplitude: ef9ed339ddd33c9183edf63fa4bbaa86cf873321 AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7 Atributika: 47e778507cfb3cd2c996278b0285221a62e97d71 BEMCheckBox: 5ba6e37ade3d3657b36caecc35c8b75c6c2b1a4e - Branch: e9cf368f79b6da7537fd1866b81037aec32ceb5b + Branch: c1b244bf1170b0ea5c5eefa897648de8d14ff0d2 Charts: b1e3a1f5a1c9ba5394438ca3b91bd8c9076310af CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da - DeviceKit: 8664c09f619f53e48000b41ee3bb7573158a333b + DeviceKit: c622fc19f795f3e0b4d75d6d11b26604338cdab3 DownloadButton: 49a21a89e0d7d1b42d9134f79aaa40e727cd57c3 EasyTipView: a92b6edc377b81c5ac18e9fd35d5ee78e9409488 FBSDKCoreKit: 4afd6ff53d8133a433dbcda44451c9498f8c6ce4 FBSDKLoginKit: 7181765f2524d7ebf82d9629066c8e6caafc99d0 - Firebase: 629510f1a9ddb235f3a7c5c8ceb23ba887f0f814 - FirebaseABTesting: 981336dd14d84787e33466e4247f77ec2343f8d9 - FirebaseAnalytics: 5506ea8b867d8423485a84b4cd612d279f7b0b8a - FirebaseCore: 98b29e3828f0a53651c363937a7f7d92a19f1ba2 - FirebaseCoreDiagnostics: fe77f42da6329d6d83d21fd9d621a6b704413bfc - FirebaseCrashlytics: 3660c045c8e45cc4276110562a0ef44cf43c8157 - FirebaseInstallations: 2563cb18a723ef9c6ef18318a49519b75dce613c - FirebaseMessaging: 419b5c9d84f294a753c6501d8cfb9ced1ce37304 - FirebaseRemoteConfig: f6365883d7950d784ee97bcdbbf1e442d4fa6119 + Firebase: fb5114cd2bf96e2ff7bcb01d0d9a156cf5fd2f07 + FirebaseABTesting: 9de50b34bf9eb4a07d4edb7af82c14152fd905aa + FirebaseAnalytics: 4ab446ce08a3fe52e8a4303dd997cf26276bf968 + FirebaseCore: c5aab092d9c4b8efea894946166b04c9d9ef0e68 + FirebaseCoreDiagnostics: 5daa63f1c1409d981a2d5007daa100b36eac6a34 + FirebaseCrashlytics: 40efbd81157dae307ec95612fa1328347284d2c2 + FirebaseInstallations: caa7c8e0d3e2345b8829d2fa9ca1b4dfbf2fcc85 + FirebaseMessaging: 82c4a48638f53f7b184f3cc9f6cd2cbe533ab316 + FirebaseRemoteConfig: a75c1bd44ebd3ed4ad3fa1ff09414a8b133be405 FLEX: 75ca95cff4bd57592c6e75adee7651ace29f9c25 - GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 + GoogleAppMeasurement: 837649ad3987936c232f6717c5680216f6243d24 GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213 - GoogleUtilities: 8de2a97a17e15b6b98e38e8770e2d129a57c0040 + GoogleUtilities: 684ee790a24f73ebb2d1d966e9711c203f2a4237 GTMAppAuth: ad5c2b70b9a8689e1a04033c9369c4915bfcbe89 GTMSessionFetcher: 43748f93435c2aa068b1cbe39655aaf600652e91 Highlightr: 9fbc57afd1921d274d5df911caabf91eaf25f0f3 @@ -406,7 +411,7 @@ SPEC CHECKSUMS: PanModal: 3e16ead1a907fb06f4df3f13492fd00149fa4974 pop: d582054913807fd11fd50bfe6a539d91c7e1a55a Presentr: 931d50e158060ea88fbf8f3dd202b17e0bb53eb0 - PromiseKit: 3b2b6995e51a954c46dbc550ce3da44fbfb563c5 + PromiseKit: ad27b174e5d8587cf799888f5e738f88e17055d8 PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 Quick: 6473349e43b9271a8d43839d9ba1c442ed1b7ac4 SDWebImage: 4dc3e42d9ec0c1028b960a33ac6b637bb432207b @@ -425,6 +430,6 @@ SPEC CHECKSUMS: VK-ios-sdk: 5bcf00a2014a7323f98db9328b603d4f96635caa YandexMobileMetrica: 9e713c16bb6aca0ba63b84c8d7b8b86d32f4ecc4 -PODFILE CHECKSUM: b3710e5ece056cbc0e2aa1c1bf1d7a9dcae8cdae +PODFILE CHECKSUM: 91c5501d3a27c4d1d80ff880978ef3b8111e3b6d COCOAPODS: 1.11.2 diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index aed634cd93..60c90209e8 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -443,6 +443,10 @@ 2C0FE8E225F81D9C00626289 /* InstructionsNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0FE8E125F81D9C00626289 /* InstructionsNetworkService.swift */; }; 2C0FF78D26B2A3E800BF9E32 /* NSManagedObjectContextExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0FF78C26B2A3E800BF9E32 /* NSManagedObjectContextExtensions.swift */; }; 2C10100B239EFAD700440651 /* DiscountingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C10100A239EFAD700440651 /* DiscountingPolicy.swift */; }; + 2C1036C1272826CD00D5E9AE /* MobileTiersAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1036C0272826CD00D5E9AE /* MobileTiersAPI.swift */; }; + 2C1036C527282C7700D5E9AE /* MobileTierPlainObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1036C427282C7700D5E9AE /* MobileTierPlainObject.swift */; }; + 2C1036C827282F5D00D5E9AE /* MobileTierCalculateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1036C727282F5D00D5E9AE /* MobileTierCalculateRequest.swift */; }; + 2C1036CA2728304000D5E9AE /* MobileTierCalculateResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1036C92728304000D5E9AE /* MobileTierCalculateResponse.swift */; }; 2C104B682069064D0026FEB9 /* autocomplete_suggestions.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2C104B672069064D0026FEB9 /* autocomplete_suggestions.plist */; }; 2C10650825ECE1610094FC39 /* CourseInfoTabInfoSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C10650725ECE1610094FC39 /* CourseInfoTabInfoSkeletonView.swift */; }; 2C10C458221496F300FA3E13 /* CourseReviewsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C10C457221496F300FA3E13 /* CourseReviewsAPI.swift */; }; @@ -462,7 +466,6 @@ 2C1883A125C44C5E00820DEE /* NextStepButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1883A025C44C5E00820DEE /* NextStepButton.swift */; }; 2C18AD4D23D5F76E00FD89F3 /* DownloadingServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C18AD4C23D5F76E00FD89F3 /* DownloadingServiceFactory.swift */; }; 2C191C482721C9F800C42946 /* EditRemoteConfigTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C191C472721C9F800C42946 /* EditRemoteConfigTableViewController.swift */; }; - 2C192CCD2668C994009221F6 /* WishlistService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C192CCC2668C994009221F6 /* WishlistService.swift */; }; 2C1948B626298455007D3486 /* FLEXManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1948B526298455007D3486 /* FLEXManager.swift */; }; 2C1966A926E7981B000D5B06 /* Announcement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1966A826E7981B000D5B06 /* Announcement.swift */; }; 2C1966AB26E79823000D5B06 /* Announcement+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1966AA26E79823000D5B06 /* Announcement+CoreDataProperties.swift */; }; @@ -556,7 +559,6 @@ 2C313E5526AFE636004ECBD2 /* StepQuizReviewStatusCircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C313E5426AFE636004ECBD2 /* StepQuizReviewStatusCircleView.swift */; }; 2C32E71320FF6C1D008BB909 /* Auth.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2CC351841F6827BE004255B6 /* Auth.storyboard */; }; 2C381BEF25505EC90084AD90 /* CourseListFilterBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C381BEE25505EC90084AD90 /* CourseListFilterBarButtonItem.swift */; }; - 2C38A7052666B0DB00F3528A /* WishlistStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C38A7042666B0DB00F3528A /* WishlistStorageManager.swift */; }; 2C3A035624AE3DCB007D28F7 /* NewProfileStreakNotificationsSwitchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3A035524AE3DCB007D28F7 /* NewProfileStreakNotificationsSwitchView.swift */; }; 2C3A035924AE3E1C007D28F7 /* NewProfileStreakNotificationsTimeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3A035824AE3E1C007D28F7 /* NewProfileStreakNotificationsTimeSelectionView.swift */; }; 2C3A052025B763370007FBCA /* WidgetURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3A051F25B763370007FBCA /* WidgetURL.swift */; }; @@ -654,6 +656,7 @@ 2C5793DD26838EA8001A19F3 /* ScrollablePageViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5793DC26838EA8001A19F3 /* ScrollablePageViewProtocol.swift */; }; 2C57B29F24092868008284F0 /* SubmissionsPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C57B29E24092868008284F0 /* SubmissionsPersistenceService.swift */; }; 2C57B2A1240945B2008284F0 /* SubmissionsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C57B2A0240945B2008284F0 /* SubmissionsRepository.swift */; }; + 2C580B812754FE7800BAEE60 /* WishlistEntriesPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C580B802754FE7800BAEE60 /* WishlistEntriesPersistenceService.swift */; }; 2C5967EB23E7828800072800 /* SubmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5967EA23E7828800072800 /* SubmissionTests.swift */; }; 2C5B3813257280C6007BF21E /* AuthorsCourseListWidgetCoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5B3812257280C6007BF21E /* AuthorsCourseListWidgetCoverView.swift */; }; 2C5BE9DC233C0A110098EB2F /* katex in Resources */ = {isa = PBXBuildFile; fileRef = 2C5BE9DB233C0A100098EB2F /* katex */; }; @@ -882,6 +885,10 @@ 2C9BBE4124903AB500FFED49 /* UserCoursesObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9BBE4024903AB500FFED49 /* UserCoursesObserver.swift */; }; 2C9BBE432490462C00FFED49 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9BBE422490462C00FFED49 /* Debouncer.swift */; }; 2C9BD78E1FC43C6B00F89CBE /* NotificationsBadgesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9BD78D1FC43C6B00F89CBE /* NotificationsBadgesManager.swift */; }; + 2C9CF0BC2754B79B001089AD /* WishListsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CF0BB2754B79B001089AD /* WishListsAPI.swift */; }; + 2C9CF0BE2754B927001089AD /* WishlistEntryPlainObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CF0BD2754B927001089AD /* WishlistEntryPlainObject.swift */; }; + 2C9CF0C22754DA9E001089AD /* WishlistEntryEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CF0C12754DA9E001089AD /* WishlistEntryEntity.swift */; }; + 2C9CF0C42754DAAE001089AD /* WishlistEntryEntity+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9CF0C32754DAAE001089AD /* WishlistEntryEntity+CoreDataProperties.swift */; }; 2C9D699B2617303900A0641F /* StepikAcademyCourseListWidgetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9D699A2617303900A0641F /* StepikAcademyCourseListWidgetViewModel.swift */; }; 2C9D69B72617334100A0641F /* CatalogBlockHorizontalCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9D69B62617334100A0641F /* CatalogBlockHorizontalCollectionViewFlowLayout.swift */; }; 2C9E3F3C1F7A80A300DDF1AA /* Notification+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9E3F3B1F7A80A300DDF1AA /* Notification+CoreDataProperties.swift */; }; @@ -1049,6 +1056,7 @@ 2CC5AA71242A34FA00C09F94 /* AdaptiveStatsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC5AA6E242A34F900C09F94 /* AdaptiveStatsManager.swift */; }; 2CC5AA72242A34FA00C09F94 /* AdaptiveRatingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC5AA6F242A34FA00C09F94 /* AdaptiveRatingHelper.swift */; }; 2CC5AA74242A351100C09F94 /* TapProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC5AA73242A351100C09F94 /* TapProxyView.swift */; }; + 2CC8322B27561A4C00F525F6 /* WishlistRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC8322A27561A4C00F525F6 /* WishlistRepository.swift */; }; 2CC8678725E1504000762416 /* SubmissionsFilterQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC8678625E1504000762416 /* SubmissionsFilterQuery.swift */; }; 2CCB4B1526E76F220056C44E /* AnnouncementPlainObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCB4B1426E76F220056C44E /* AnnouncementPlainObject.swift */; }; 2CCB4B1826E772810056C44E /* AnnouncementStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCB4B1726E772810056C44E /* AnnouncementStatus.swift */; }; @@ -1066,6 +1074,12 @@ 2CD04082250F669F004D284F /* ProcessedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD04081250F669F004D284F /* ProcessedContentView.swift */; }; 2CD04084250F67D0004D284F /* ProcessedContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD04083250F67D0004D284F /* ProcessedContent.swift */; }; 2CD063E6242A8D950052134F /* UIActivityIndicatorViewStyle+Fallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD063E5242A8D950052134F /* UIActivityIndicatorViewStyle+Fallback.swift */; }; + 2CD1D9E4272A960D0031C78E /* MobileTiersNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD1D9E3272A960D0031C78E /* MobileTiersNetworkService.swift */; }; + 2CD1D9E8272A9D580031C78E /* MobileTier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD1D9E7272A9D580031C78E /* MobileTier.swift */; }; + 2CD1D9EA272A9D680031C78E /* MobileTier+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD1D9E9272A9D680031C78E /* MobileTier+CoreDataProperties.swift */; }; + 2CD1D9EC272AA8D80031C78E /* PaymentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD1D9EB272AA8D80031C78E /* PaymentStore.swift */; }; + 2CD1D9EE272AA9A80031C78E /* MobileTiersPersistenceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD1D9ED272AA9A80031C78E /* MobileTiersPersistenceService.swift */; }; + 2CD1D9F0272AC0660031C78E /* MobileTiersRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD1D9EF272AC0660031C78E /* MobileTiersRepository.swift */; }; 2CD3374B268228760073C867 /* CourseRevenueSubmoduleProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD3374A268228760073C867 /* CourseRevenueSubmoduleProtocol.swift */; }; 2CD462EA226F4279004E4725 /* FetchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD462E9226F4279004E4725 /* FetchResult.swift */; }; 2CD514EA2540773F0087115C /* TableQuizSelectColumnsOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD514E92540773F0087115C /* TableQuizSelectColumnsOutputProtocol.swift */; }; @@ -1105,6 +1119,7 @@ 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 */; }; + 2CDE5E0F27551A7E009D1F8E /* WishListsNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDE5E0E27551A7E009D1F8E /* WishListsNetworkService.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 */; }; @@ -2413,6 +2428,10 @@ 2C0FF78C26B2A3E800BF9E32 /* NSManagedObjectContextExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSManagedObjectContextExtensions.swift; sourceTree = ""; }; 2C101009239E7A1E00440651 /* Model_discount_policy.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_discount_policy.xcdatamodel; sourceTree = ""; }; 2C10100A239EFAD700440651 /* DiscountingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscountingPolicy.swift; sourceTree = ""; }; + 2C1036C0272826CD00D5E9AE /* MobileTiersAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileTiersAPI.swift; sourceTree = ""; }; + 2C1036C427282C7700D5E9AE /* MobileTierPlainObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileTierPlainObject.swift; sourceTree = ""; }; + 2C1036C727282F5D00D5E9AE /* MobileTierCalculateRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileTierCalculateRequest.swift; sourceTree = ""; }; + 2C1036C92728304000D5E9AE /* MobileTierCalculateResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileTierCalculateResponse.swift; sourceTree = ""; }; 2C104B672069064D0026FEB9 /* autocomplete_suggestions.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = autocomplete_suggestions.plist; sourceTree = ""; }; 2C10650725ECE1610094FC39 /* CourseInfoTabInfoSkeletonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseInfoTabInfoSkeletonView.swift; sourceTree = ""; }; 2C10C457221496F300FA3E13 /* CourseReviewsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseReviewsAPI.swift; sourceTree = ""; }; @@ -2433,7 +2452,6 @@ 2C18AD4C23D5F76E00FD89F3 /* DownloadingServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadingServiceFactory.swift; sourceTree = ""; }; 2C19023B219F24B100FAD9AF /* Model_course_certificate_details_v28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_course_certificate_details_v28.xcdatamodel; sourceTree = ""; }; 2C191C472721C9F800C42946 /* EditRemoteConfigTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRemoteConfigTableViewController.swift; sourceTree = ""; }; - 2C192CCC2668C994009221F6 /* WishlistService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishlistService.swift; sourceTree = ""; }; 2C1948B526298455007D3486 /* FLEXManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLEXManager.swift; sourceTree = ""; }; 2C1966A626E797B3000D5B06 /* Model_announcements_v88.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_announcements_v88.xcdatamodel; sourceTree = ""; }; 2C1966A826E7981B000D5B06 /* Announcement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Announcement.swift; sourceTree = ""; }; @@ -2541,7 +2559,6 @@ 2C35C4D41F4DA471002F3BF4 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = en; path = LeaderboardNames/en.lproj/nouns_m.plist; sourceTree = ""; }; 2C36E4772501F28E00D63C41 /* Model_user_remove_level_v65.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_user_remove_level_v65.xcdatamodel; sourceTree = ""; }; 2C381BEE25505EC90084AD90 /* CourseListFilterBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseListFilterBarButtonItem.swift; sourceTree = ""; }; - 2C38A7042666B0DB00F3528A /* WishlistStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishlistStorageManager.swift; sourceTree = ""; }; 2C3A035524AE3DCB007D28F7 /* NewProfileStreakNotificationsSwitchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileStreakNotificationsSwitchView.swift; sourceTree = ""; }; 2C3A035824AE3E1C007D28F7 /* NewProfileStreakNotificationsTimeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProfileStreakNotificationsTimeSelectionView.swift; sourceTree = ""; }; 2C3A051F25B763370007FBCA /* WidgetURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetURL.swift; sourceTree = ""; }; @@ -2649,6 +2666,7 @@ 2C5793DC26838EA8001A19F3 /* ScrollablePageViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollablePageViewProtocol.swift; sourceTree = ""; }; 2C57B29E24092868008284F0 /* SubmissionsPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionsPersistenceService.swift; sourceTree = ""; }; 2C57B2A0240945B2008284F0 /* SubmissionsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionsRepository.swift; sourceTree = ""; }; + 2C580B802754FE7800BAEE60 /* WishlistEntriesPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishlistEntriesPersistenceService.swift; sourceTree = ""; }; 2C5967EA23E7828800072800 /* SubmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionTests.swift; sourceTree = ""; }; 2C5AB2B122F9BC78005E7AA0 /* Model_step_options_limits.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_step_options_limits.xcdatamodel; sourceTree = ""; }; 2C5B3812257280C6007BF21E /* AuthorsCourseListWidgetCoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorsCourseListWidgetCoverView.swift; sourceTree = ""; }; @@ -2886,6 +2904,11 @@ 2C9BBE4024903AB500FFED49 /* UserCoursesObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCoursesObserver.swift; sourceTree = ""; }; 2C9BBE422490462C00FFED49 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = ""; }; 2C9BD78D1FC43C6B00F89CBE /* NotificationsBadgesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsBadgesManager.swift; sourceTree = ""; }; + 2C9CF0BB2754B79B001089AD /* WishListsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishListsAPI.swift; sourceTree = ""; }; + 2C9CF0BD2754B927001089AD /* WishlistEntryPlainObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishlistEntryPlainObject.swift; sourceTree = ""; }; + 2C9CF0BF2754D9C8001089AD /* Model_wish_lists_v91.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_wish_lists_v91.xcdatamodel; sourceTree = ""; }; + 2C9CF0C12754DA9E001089AD /* WishlistEntryEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishlistEntryEntity.swift; sourceTree = ""; }; + 2C9CF0C32754DAAE001089AD /* WishlistEntryEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WishlistEntryEntity+CoreDataProperties.swift"; sourceTree = ""; }; 2C9D699A2617303900A0641F /* StepikAcademyCourseListWidgetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepikAcademyCourseListWidgetViewModel.swift; sourceTree = ""; }; 2C9D69B62617334100A0641F /* CatalogBlockHorizontalCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogBlockHorizontalCollectionViewFlowLayout.swift; sourceTree = ""; }; 2C9E3F351F7A79E600DDF1AA /* Model_notifications.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_notifications.xcdatamodel; sourceTree = ""; }; @@ -3065,6 +3088,7 @@ 2CC5AA6E242A34F900C09F94 /* AdaptiveStatsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveStatsManager.swift; sourceTree = ""; }; 2CC5AA6F242A34FA00C09F94 /* AdaptiveRatingHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveRatingHelper.swift; sourceTree = ""; }; 2CC5AA73242A351100C09F94 /* TapProxyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapProxyView.swift; sourceTree = ""; }; + 2CC8322A27561A4C00F525F6 /* WishlistRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishlistRepository.swift; sourceTree = ""; }; 2CC8678625E1504000762416 /* SubmissionsFilterQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmissionsFilterQuery.swift; sourceTree = ""; }; 2CC8683624CAD452004845AB /* Model_new_profile_organization_v57.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_new_profile_organization_v57.xcdatamodel; sourceTree = ""; }; 2CC925F9685393FA44977549 /* NewProfileCertificatesAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NewProfileCertificatesAssembly.swift; sourceTree = ""; }; @@ -3085,6 +3109,12 @@ 2CD04081250F669F004D284F /* ProcessedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedContentView.swift; sourceTree = ""; }; 2CD04083250F67D0004D284F /* ProcessedContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessedContent.swift; sourceTree = ""; }; 2CD063E5242A8D950052134F /* UIActivityIndicatorViewStyle+Fallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivityIndicatorViewStyle+Fallback.swift"; sourceTree = ""; }; + 2CD1D9E3272A960D0031C78E /* MobileTiersNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileTiersNetworkService.swift; sourceTree = ""; }; + 2CD1D9E7272A9D580031C78E /* MobileTier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileTier.swift; sourceTree = ""; }; + 2CD1D9E9272A9D680031C78E /* MobileTier+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MobileTier+CoreDataProperties.swift"; sourceTree = ""; }; + 2CD1D9EB272AA8D80031C78E /* PaymentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentStore.swift; sourceTree = ""; }; + 2CD1D9ED272AA9A80031C78E /* MobileTiersPersistenceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileTiersPersistenceService.swift; sourceTree = ""; }; + 2CD1D9EF272AC0660031C78E /* MobileTiersRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MobileTiersRepository.swift; sourceTree = ""; }; 2CD3374A268228760073C867 /* CourseRevenueSubmoduleProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseRevenueSubmoduleProtocol.swift; sourceTree = ""; }; 2CD462E9226F4279004E4725 /* FetchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchResult.swift; sourceTree = ""; }; 2CD514E92540773F0087115C /* TableQuizSelectColumnsOutputProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableQuizSelectColumnsOutputProtocol.swift; sourceTree = ""; }; @@ -3125,6 +3155,7 @@ 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 = ""; }; + 2CDE5E0E27551A7E009D1F8E /* WishListsNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WishListsNetworkService.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 = ""; }; @@ -3176,6 +3207,7 @@ 2CECFEE8252F1EDD006BA883 /* VisitedCoursesAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitedCoursesAPI.swift; sourceTree = ""; }; 2CECFEF0252F1F4C006BA883 /* VisitedCourse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitedCourse.swift; sourceTree = ""; }; 2CECFEF8252F235A006BA883 /* VisitedCoursesNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitedCoursesNetworkService.swift; sourceTree = ""; }; + 2CED149D2756D83E00BB013D /* Model_mobile_tiers_v92.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_mobile_tiers_v92.xcdatamodel; sourceTree = ""; }; 2CED386226710EFE00854DA3 /* AutoplayNavigationDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoplayNavigationDirection.swift; sourceTree = ""; }; 2CEDB4042600BD7700CB077F /* StoryPart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryPart.swift; sourceTree = ""; }; 2CEDC6412608906F00B0B018 /* CourseRecommendationsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseRecommendationsAPI.swift; sourceTree = ""; }; @@ -4403,6 +4435,16 @@ path = PromoPriceButton; sourceTree = ""; }; + 2C1036C627282F3D00D5E9AE /* MobileTiers */ = { + isa = PBXGroup; + children = ( + 2C1036C727282F5D00D5E9AE /* MobileTierCalculateRequest.swift */, + 2C1036C92728304000D5E9AE /* MobileTierCalculateResponse.swift */, + 2C1036C427282C7700D5E9AE /* MobileTierPlainObject.swift */, + ); + path = MobileTiers; + sourceTree = ""; + }; 2C10AFE8217E04780019966D /* Registration */ = { isa = PBXGroup; children = ( @@ -4493,8 +4535,10 @@ 2C26BF09240810A3000EE23C /* AttemptsRepository.swift */, 2C1AC31D255B476A00E6ECA9 /* CatalogBlocksRepository.swift */, 2CDD3FDB2715F6D30029AF40 /* CoursesRepository.swift */, + 2CD1D9EF272AC0660031C78E /* MobileTiersRepository.swift */, 2C98424626DE78EA0098E36B /* SearchResultsRepository.swift */, 2C57B2A0240945B2008284F0 /* SubmissionsRepository.swift */, + 2CC8322A27561A4C00F525F6 /* WishlistRepository.swift */, ); path = Repository; sourceTree = ""; @@ -5271,6 +5315,7 @@ 2C47914526BBD17400920ED2 /* InstructionType.swift */, 2C130E3B2512402B00389AEB /* LaunchArguments.swift */, 2CD80A7D269C47670047AE3C /* PaginationState.swift */, + 2CD1D9EB272AA8D80031C78E /* PaymentStore.swift */, 2CEDC660260898F700B0B018 /* PlatformType.swift */, 2C5D340725C98A6800372C61 /* PromoCode.swift */, 2C20778322BBA54800D44DC0 /* QuizStatus.swift */, @@ -5464,6 +5509,15 @@ path = Events; sourceTree = ""; }; + 2C9CF0C02754DA7F001089AD /* WishlistEntryEntity */ = { + isa = PBXGroup; + children = ( + 2C9CF0C12754DA9E001089AD /* WishlistEntryEntity.swift */, + 2C9CF0C32754DAAE001089AD /* WishlistEntryEntity+CoreDataProperties.swift */, + ); + path = WishlistEntryEntity; + sourceTree = ""; + }; 2C9D69AB2617323700A0641F /* Views */ = { isa = PBXGroup; children = ( @@ -6631,6 +6685,15 @@ path = SearchResult; sourceTree = ""; }; + 2CD1D9E6272A9D450031C78E /* MobileTier */ = { + isa = PBXGroup; + children = ( + 2CD1D9E7272A9D580031C78E /* MobileTier.swift */, + 2CD1D9E9272A9D680031C78E /* MobileTier+CoreDataProperties.swift */, + ); + path = MobileTier; + sourceTree = ""; + }; 2CD30B49256D69EF00E5912B /* CatalogBlocksSubmodules */ = { isa = PBXGroup; children = ( @@ -7561,6 +7624,7 @@ 2CFF9022242A262800FD7311 /* LastCodeLanguage */, 2CFF9023242A263700FD7311 /* LastStep */, 2CFF9024242A264400FD7311 /* Lesson */, + 2CD1D9E6272A9D450031C78E /* MobileTier */, 2CFF9025242A265000FD7311 /* Notification */, 2CDA1B2A2613B80700E36BF7 /* ProctorSession */, 2CFF9026242A265F00FD7311 /* Profile */, @@ -7579,6 +7643,7 @@ 2C94C7F8248F816400E4104E /* UserCourse */, 2CFF902E242A26C200FD7311 /* Video */, 2CFF902F242A26CE00FD7311 /* VideoURL */, + 2C9CF0C02754DA7F001089AD /* WishlistEntryEntity */, ); path = Entities; sourceTree = ""; @@ -7971,6 +8036,7 @@ 080CE1481E9562430089A27F /* LessonsAPI.swift */, 2C42EFB52476F28B00423695 /* MagicLinksAPI.swift */, 2C5418C924A28ECF00B2DCE2 /* MetricsAPI.swift */, + 2C1036C0272826CD00D5E9AE /* MobileTiersAPI.swift */, 2C9E3F3D1F7A930100DDF1AA /* NotificationsAPI.swift */, 86624A721FC76578008E7E6C /* NotificationStatusesAPI.swift */, 2CDA1B392613BDEA00E36BF7 /* ProctorSessionsAPI.swift */, @@ -7998,6 +8064,7 @@ 080CE1571E9566220089A27F /* ViewsAPI.swift */, 2CECFEE8252F1EDD006BA883 /* VisitedCoursesAPI.swift */, 086A8B271D21796800F45C45 /* VotesAPI.swift */, + 2C9CF0BB2754B79B001089AD /* WishListsAPI.swift */, ); path = Endpoints; sourceTree = ""; @@ -8055,9 +8122,11 @@ 2C97761C25D6C20F008778D6 /* UnitPlainObject.swift */, 2CB1C3AC240050F9001DA83E /* UserCodeRun.swift */, 2CECFEF0252F1F4C006BA883 /* VisitedCourse.swift */, + 2C9CF0BD2754B927001089AD /* WishlistEntryPlainObject.swift */, 2CCB4B1626E7724E0056C44E /* Announcements */, 2C97764225D6CBD2008778D6 /* Block */, 2CA3DAA62179DF7300F43888 /* Discussions */, + 2C1036C627282F3D00D5E9AE /* MobileTiers */, 2CF8DF4525F22F9900F577C2 /* Review */, 2CCEB1A326D8CFDF001C83B6 /* SearchResult */, 2C1661012358D3290020B7F4 /* StepOptions */, @@ -8301,6 +8370,7 @@ 62E989EE577036747B419AAC /* LastCodeLanguagePersistenceService.swift */, 62E98AC352F76C23F60E12A6 /* LastStepPersistenceService.swift */, 62E985EA7905BE734BB77FBD /* LessonsPersistenceService.swift */, + 2CD1D9ED272AA9A80031C78E /* MobileTiersPersistenceService.swift */, 62E98D6C6FA9C92ABDECEF12 /* NotificationsPersistenceService.swift */, 2CDA1B512613BF1300E36BF7 /* ProctorSessionsPersistenceService.swift */, 2C87A7A324465B5800933CA4 /* ProfilesPersistenceService.swift */, @@ -8319,6 +8389,7 @@ 2C87A7A02446502900933CA4 /* UsersPersistenceService.swift */, 2CB510ED248C226A00DB8344 /* VideosPersistenceService.swift */, 2CE9BF4B248D0EC5004F6659 /* VideoURLsPersistenceService.swift */, + 2C580B802754FE7800BAEE60 /* WishlistEntriesPersistenceService.swift */, ); path = Persistence; sourceTree = ""; @@ -8660,6 +8731,7 @@ 2C0FE8E125F81D9C00626289 /* InstructionsNetworkService.swift */, 62E98B1D8C5FD917DC6E6053 /* LessonsNetworkService.swift */, 2C42EFB92476FADB00423695 /* MagicLinksNetworkService.swift */, + 2CD1D9E3272A960D0031C78E /* MobileTiersNetworkService.swift */, 2CDA1B452613BE8600E36BF7 /* ProctorSessionsNetworkService.swift */, 2C06E094223FF46500AF4DA2 /* ProfilesNetworkService.swift */, 62E989FAD86F79364CC2EF89 /* ProgressesNetworkService.swift */, @@ -8684,6 +8756,7 @@ 2CB9529B229F29F000A6117A /* ViewsNetworkService.swift */, 2CECFEF8252F235A006BA883 /* VisitedCoursesNetworkService.swift */, 2C85C6A422D38A3800FDBAFE /* VotesNetworkService.swift */, + 2CDE5E0E27551A7E009D1F8E /* WishListsNetworkService.swift */, ); path = Network; sourceTree = ""; @@ -8954,7 +9027,6 @@ 2C89FF8724AEE90500168F1B /* StreakNotificationsStorageManager.swift */, 62E9881CFD7892350EA31213 /* TooltipStorageManager.swift */, 2CF7E3332417C1A700B9188E /* UseCellularDataForDownloadsStorageManager.swift */, - 2C38A7042666B0DB00F3528A /* WishlistStorageManager.swift */, 2C8F3ADE23CCAB88004D113A /* Video */, ); path = StorageManagers; @@ -9460,7 +9532,6 @@ 62E98EBA0AF48AD90775FF7E /* UserAccountService.swift */, 2C9BBE4024903AB500FFED49 /* UserCoursesObserver.swift */, 2C2E44D424FE5FEA006B7303 /* VisitedCoursesCleaner.swift */, - 2C192CCC2668C994009221F6 /* WishlistService.swift */, 2C273264248BB4CC00BD065F /* AppData */, 2C6277BB270C660400FDAFD9 /* ApplicationShortcuts */, 2C273263248BB49500BD065F /* CodeEditor */, @@ -11108,6 +11179,7 @@ 2CF1B33E2163BE720008DA0C /* StoriesAssembly.swift in Sources */, 2C04BA542407C3BF00D74D4B /* AttemptsPersistenceService.swift in Sources */, 2C71BC0926B34D43000AF7B8 /* BasePersistenceService.swift in Sources */, + 2C1036C527282C7700D5E9AE /* MobileTierPlainObject.swift in Sources */, 083AE48320BD72CA00102FE4 /* PersonalDeadlinesService.swift in Sources */, 080DCF171C4518BC00DE3E2E /* SearchResultPlainObject.swift in Sources */, 2CC3518F1F682B6C004255B6 /* SocialAuthHeaderView.swift in Sources */, @@ -11124,6 +11196,7 @@ 2CAD8B9D2170CDB4003F420B /* LocalNotificationProtocol.swift in Sources */, 08DF1D921BDAB93900BA35EA /* StringExtensions.swift in Sources */, 2C4AD01923E304140049B7B0 /* DiscussionThreadsNetworkService.swift in Sources */, + 2CD1D9F0272AC0660031C78E /* MobileTiersRepository.swift in Sources */, 08FA62222121BEF900F00275 /* GrowPresentAnimationController.swift in Sources */, 2CF959D926208C3D00D14B62 /* DebugMenuViewModel.swift in Sources */, 083F2B2A1E9EC17F00714173 /* LoadingPaginationView.swift in Sources */, @@ -11308,6 +11381,7 @@ 088E73F42061BDAA00D458E3 /* StepikModelView.swift in Sources */, 2C89FF8424AED52900168F1B /* NewProfileStreakNotificationsFooterView.swift in Sources */, 2CC3519A1F68339A004255B6 /* AuthTextField.swift in Sources */, + 2CC8322B27561A4C00F525F6 /* WishlistRepository.swift in Sources */, 2C8908A0216F465200083341 /* NotificationsService.swift in Sources */, 08CBA3151F57562A00302154 /* SwitchMenuBlockTableViewCell.swift in Sources */, 08E43EA3214C2C5800E3CB50 /* ModalStackRouter.swift in Sources */, @@ -11320,7 +11394,6 @@ 08D1EF731BB5636700BE84E6 /* Course+CoreDataProperties.swift in Sources */, 2CF8DF5125F2586400F577C2 /* ReviewSessionResponse.swift in Sources */, 2C941505256D7DCC0010B9A6 /* DefaultSimpleCourseListWidgetView.swift in Sources */, - 2C192CCD2668C994009221F6 /* WishlistService.swift in Sources */, 2C22042720E277E50060117A /* SkeletonView.swift in Sources */, 08F555651C4FF22600C877E8 /* QuizControllerDelegate.swift in Sources */, 0891424B1BCEE4EF0000BCB0 /* VideoURL.swift in Sources */, @@ -11351,7 +11424,6 @@ 08F485AA1C580D61000165AA /* MathReply.swift in Sources */, 08E43E99214C279700E3CB50 /* PushRouterSourceProtocol.swift in Sources */, 2C6F475725E54AB000403A6F /* SubmissionsSearchBar.swift in Sources */, - 2C38A7052666B0DB00F3528A /* WishlistStorageManager.swift in Sources */, 2C2F0BE72186EEB8007DCA0A /* StreakNotificationsRequestAlertDataSource.swift in Sources */, 2C9BBE4124903AB500FFED49 /* UserCoursesObserver.swift in Sources */, 2CF4661D25402227002415AF /* TableQuizSelectColumnsView.swift in Sources */, @@ -11369,6 +11441,7 @@ 2CE9BF48248D0950004F6659 /* CodeLimitsPersistenceService.swift in Sources */, 2C48D604228F0EF700739477 /* ContentProcessingRule.swift in Sources */, 2C1864C924F6350F004AFFD1 /* StepikURLFactory.swift in Sources */, + 2CD1D9EA272A9D680031C78E /* MobileTier+CoreDataProperties.swift in Sources */, 083F2B211E9E645000714173 /* CertificateTableViewCell.swift in Sources */, 08CA59F21BBFD65E008DC44D /* User.swift in Sources */, 2CA0DE64266FC0C60008C0D6 /* CourseBenefitStatus.swift in Sources */, @@ -11450,6 +11523,7 @@ 0885F8581BAAD43300F2A188 /* AuthInfo.swift in Sources */, 2CEBEBF2242F8E9900DBFDF0 /* StepikRequestInterceptor.swift in Sources */, 2CF0885A205BEBF500FCB9C0 /* StepikTableView.swift in Sources */, + 2CD1D9E8272A9D580031C78E /* MobileTier.swift in Sources */, 2CE8391320C8102300FE3672 /* AchievementBadgeView.swift in Sources */, 2C96290F26B9856F00C9EA9C /* ReviewsNetworkService.swift in Sources */, 08F4859A1C57868E000165AA /* TextReply.swift in Sources */, @@ -11512,6 +11586,7 @@ 2C20C86822F903D10052E9BF /* CodeEditorThemeService.swift in Sources */, 085DF8D51C99B9FB006809D9 /* Player.swift in Sources */, 2C5DF13F1FED26AC003B1177 /* CardStepViewController.swift in Sources */, + 2C9CF0C22754DA9E001089AD /* WishlistEntryEntity.swift in Sources */, 2C5DF1411FED26B7003B1177 /* CardStepPresenter.swift in Sources */, 2CED386326710EFE00854DA3 /* AutoplayNavigationDirection.swift in Sources */, 084F7AAA1E76EF780088368A /* LastStep+CoreDataProperties.swift in Sources */, @@ -11554,6 +11629,7 @@ 2CD80A83269C681C0047AE3C /* CourseRevenueTabMonthlyTableViewDataSource.swift in Sources */, 2C8E477B258966570084A070 /* GridSimpleCourseListCollectionHeaderContentView.swift in Sources */, 080CE14C1E9562BF0089A27F /* UsersAPI.swift in Sources */, + 2C1036C827282F5D00D5E9AE /* MobileTierCalculateRequest.swift in Sources */, 080217831F55B1B200186245 /* Menu.swift in Sources */, 08E6BB6B1DC8EB45006622EC /* UserActivity.swift in Sources */, 085C4FF61D8C835600B27C95 /* StepsControllerDeepLinkRouter.swift in Sources */, @@ -11575,6 +11651,7 @@ 62E98F431204AD9A46EBBC18 /* CodeEditorSettingsViewController.swift in Sources */, 62E98975B396C752B07036D8 /* CodeEditorSettingsPresenter.swift in Sources */, 62E98C1377148A52D15AAEBF /* CodeEditorPreferencesContainer.swift in Sources */, + 2C9CF0BE2754B927001089AD /* WishlistEntryPlainObject.swift in Sources */, 2C4AD01B23E305FA0049B7B0 /* DiscussionThreadsPersistenceService.swift in Sources */, 2CADFEF424C1325F00008C65 /* AchievementProgressData.swift in Sources */, 62E983397736699787D8DD35 /* UIView+FromNib.swift in Sources */, @@ -11593,6 +11670,7 @@ 62E98622F3B03F7282DC06D8 /* AchievementDescription.swift in Sources */, 2C71BC0726B34D27000AF7B8 /* Identifiable.swift in Sources */, 62E98381EC6AD8DD7613C32D /* AchievementsListViewController.swift in Sources */, + 2CDE5E0F27551A7E009D1F8E /* WishListsNetworkService.swift in Sources */, 2C5E90822333CF4C00288BE3 /* LastCodeLanguage+CoreDataProperties.swift in Sources */, 2C73E24124D0117400340052 /* NewProfileSocialProfilesViewModel.swift in Sources */, 62E98DE7FA0EE5EB23136B52 /* AchievementsListPresenter.swift in Sources */, @@ -11652,6 +11730,7 @@ 2CFED66E252C649400FCAD41 /* DiscussionsTableViewDataSourceDelegate.swift in Sources */, 2CB695382465D4030037BF0D /* NewProfileHeaderView.swift in Sources */, 2CF8DF6525F260BC00F577C2 /* RubricScorePlainObject.swift in Sources */, + 2C9CF0BC2754B79B001089AD /* WishListsAPI.swift in Sources */, 2CF8DF7325F2682100F577C2 /* ReviewSessionsNetworkService.swift in Sources */, 2C0FE8D425F81D0500626289 /* InstructionDataPlainObject.swift in Sources */, 2CD6E259234E0A1B00F49303 /* EmailAddress.swift in Sources */, @@ -11661,6 +11740,7 @@ 62E98AA93C41892F0F141FDF /* ImageButton.swift in Sources */, 62E989EE75A8D4A31D8FAA57 /* DownloadControlView.swift in Sources */, 62E98570B05049B3A8619283 /* CourseRatingView.swift in Sources */, + 2C9CF0C42754DAAE001089AD /* WishlistEntryEntity+CoreDataProperties.swift in Sources */, 62E980CB3EB9E38E716EB433 /* TabSegmentedControlView.swift in Sources */, 62E98ABA0A56F069278EF47D /* CourseReviewSummariesNetworkService.swift in Sources */, 62E98F5E955545F7A75F77BC /* ProgressesNetworkService.swift in Sources */, @@ -11669,6 +11749,7 @@ 62E9826EBCAB31DA927E56FE /* Reusable.swift in Sources */, 62E98E916518CC0446F2DCDF /* ProgrammaticallyInitializableViewProtocol.swift in Sources */, 62E9854D204FADAEDD044CAD /* NibLoadable.swift in Sources */, + 2C1036C1272826CD00D5E9AE /* MobileTiersAPI.swift in Sources */, 62E9824265673F973F160C0A /* ProgressCircleImage.swift in Sources */, 2CDA1B3A2613BDEA00E36BF7 /* ProctorSessionsAPI.swift in Sources */, 62E981DD043FB3AEC3BDB669 /* PaddingLabel.swift in Sources */, @@ -11923,6 +12004,7 @@ 2C85EC162559A57D0059EF97 /* CatalogBlockAppearance.swift in Sources */, 62E989945806203AB464B5EF /* CourseWidgetLabel.swift in Sources */, 2CBC5AF82682479F0000F2D1 /* CourseRevenueTabPurchasesTableViewDataSource.swift in Sources */, + 2CD1D9E4272A960D0031C78E /* MobileTiersNetworkService.swift in Sources */, 62E98E6F295E2C4799957DFB /* CourseWidgetStatsItemView.swift in Sources */, 2C19D9D1261B46CC00181DB0 /* ExploreStepikAcademyBlockContainerView.swift in Sources */, 2C1AC2D7255AD36D00E6ECA9 /* SimpleCourseListsCatalogBlockContentItem.swift in Sources */, @@ -12049,6 +12131,7 @@ 62E9888B20E524751AEB87AC /* TagViewModel.swift in Sources */, 62E98E17CA922F1D87F6DA96 /* FullscreenCourseListAssembly.swift in Sources */, 62E98AF6A9BDB9A4102F56F3 /* FullscreenCourseListDataFlow.swift in Sources */, + 2CD1D9EE272AA9A80031C78E /* MobileTiersPersistenceService.swift in Sources */, 2CF8DF4725F2303300F577C2 /* ReviewSessionPlainObject.swift in Sources */, 2C1AC2DF255AD76E00E6ECA9 /* AuthorsCatalogBlockContentItem.swift in Sources */, 62E9881A9FF61D408A09160F /* FullscreenCourseListInteractor.swift in Sources */, @@ -12149,6 +12232,7 @@ 62E98BE715996F6FF7C93EF1 /* CodeQuizFullscreenViewController.swift in Sources */, 2C29ED5E269F06C500729A97 /* CourseBeneficiariesAPI.swift in Sources */, 62E98ED854756E2499392186 /* CodeQuizFullscreenViewModel.swift in Sources */, + 2C580B812754FE7800BAEE60 /* WishlistEntriesPersistenceService.swift in Sources */, 62E98A9DF433C986B49F7980 /* CodeQuizFullscreenCodeViewController.swift in Sources */, 62E985C03629095C9CABECBD /* CodeQuizFullscreenInstructionView.swift in Sources */, 62E98DAFA9F4AE0523FA75E8 /* CodeQuizFullscreenInstructionViewController.swift in Sources */, @@ -12324,6 +12408,7 @@ 2C5D3452268489F4006D1E9C /* CourseRevenueTabSegmentedControlView.swift in Sources */, 39AB2324EB824124A83D7534 /* UserCoursesInteractor.swift in Sources */, 2CABB842267B929C0070E5E7 /* CourseWidgetPriceView.swift in Sources */, + 2C1036CA2728304000D5E9AE /* MobileTierCalculateResponse.swift in Sources */, 942F44578E193384B1E4DAD9 /* UserCoursesPresenter.swift in Sources */, 86356B693AF04C13CE3C856F /* UserCoursesView.swift in Sources */, 2A7E1C8409B12A6CB09F0393 /* UserCoursesViewController.swift in Sources */, @@ -12357,6 +12442,7 @@ 1403FF0C9A5259CF2D63D98C /* NewProfileUserActivityProvider.swift in Sources */, 79A20D8F6931BCF4BDD7656E /* NewProfileUserActivityView.swift in Sources */, 12CB070461601452E35A6334 /* NewProfileUserActivityViewController.swift in Sources */, + 2CD1D9EC272AA8D80031C78E /* PaymentStore.swift in Sources */, E68D5D39475BA651C54FD4C9 /* NewProfileStreakNotificationsAssembly.swift in Sources */, 2CDA1AFE2613921000E36BF7 /* ExamSession+CoreDataProperties.swift in Sources */, 209E27FAF79E14028BA3E27E /* NewProfileStreakNotificationsDataFlow.swift in Sources */, @@ -12857,7 +12943,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; @@ -12882,7 +12968,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Production.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -13024,7 +13110,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Production.plist"; @@ -13054,7 +13140,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -13145,7 +13231,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13197,7 +13283,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; @@ -13278,7 +13364,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Develop.plist"; @@ -13326,7 +13412,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Develop.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -13847,7 +13933,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -13901,7 +13987,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; @@ -13983,7 +14069,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEVELOPMENT_TEAM = UJ4KC2QN7B; ENABLE_BITCODE = YES; INFOPLIST_FILE = "Stepic/Info-Release.plist"; @@ -14031,7 +14117,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 387; + CURRENT_PROJECT_VERSION = 388; DEVELOPMENT_TEAM = UJ4KC2QN7B; INFOPLIST_FILE = "StickerPackExtension/Info-Release.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -14505,6 +14591,8 @@ 08D1EF6E1BB5618700BE84E6 /* Model.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 2CED149D2756D83E00BB013D /* Model_mobile_tiers_v92.xcdatamodel */, + 2C9CF0BF2754D9C8001089AD /* Model_wish_lists_v91.xcdatamodel */, 2C4B971E270362D600B3AA8F /* Model_user_course_can_be_reviewed_v90.xcdatamodel */, 2C55B7D326FA66ED0022B822 /* Model_announcements_v89.xcdatamodel */, 2C1966A626E797B3000D5B06 /* Model_announcements_v88.xcdatamodel */, @@ -14597,7 +14685,7 @@ 0802AC531C7222B200C4F3E6 /* Model_v2.xcdatamodel */, 08D1EF6F1BB5618700BE84E6 /* Model.xcdatamodel */, ); - currentVersion = 2C4B971E270362D600B3AA8F /* Model_user_course_can_be_reviewed_v90.xcdatamodel */; + currentVersion = 2CED149D2756D83E00BB013D /* Model_mobile_tiers_v92.xcdatamodel */; path = Model.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Develop.xcscheme b/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Develop.xcscheme index 13f5a12887..a4b7659387 100644 --- a/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Develop.xcscheme +++ b/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Develop.xcscheme @@ -103,6 +103,10 @@ argument = "-FIRAnalyticsDebugEnabled" isEnabled = "NO"> + + diff --git a/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Release.xcscheme b/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Release.xcscheme index f1c0154ed1..d7273ef461 100644 --- a/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Release.xcscheme +++ b/Stepic.xcodeproj/xcshareddata/xcschemes/Stepic Release.xcscheme @@ -103,6 +103,10 @@ argument = "-FIRAnalyticsDebugEnabled" isEnabled = "NO"> + + diff --git a/Stepic/Info-Develop.plist b/Stepic/Info-Develop.plist index b87eb0d3cd..9869b1c410 100644 --- a/Stepic/Info-Develop.plist +++ b/Stepic/Info-Develop.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.196-develop + 1.201-develop CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 387 + 388 FacebookAppID 171127739724012 FacebookDisplayName @@ -177,9 +177,9 @@ - NSUserActivityTypes - - $(PRODUCT_BUNDLE_IDENTIFIER).ContinueLearningUserActivity - + NSUserActivityTypes + + $(PRODUCT_BUNDLE_IDENTIFIER).ContinueLearningUserActivity + diff --git a/Stepic/Info-Production.plist b/Stepic/Info-Production.plist index 97adeebdd0..3e328a3f96 100644 --- a/Stepic/Info-Production.plist +++ b/Stepic/Info-Production.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.196 + 1.201 CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 387 + 388 FacebookAppID 171127739724012 FacebookDisplayName @@ -177,9 +177,9 @@ - NSUserActivityTypes - - $(PRODUCT_BUNDLE_IDENTIFIER).ContinueLearningUserActivity - + NSUserActivityTypes + + $(PRODUCT_BUNDLE_IDENTIFIER).ContinueLearningUserActivity + diff --git a/Stepic/Info-Release.plist b/Stepic/Info-Release.plist index ed060bdf5b..a903cd5686 100644 --- a/Stepic/Info-Release.plist +++ b/Stepic/Info-Release.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.196-release + 1.201-release CFBundleSignature ???? CFBundleURLTypes @@ -62,7 +62,7 @@ CFBundleVersion - 387 + 388 FacebookAppID 171127739724012 FacebookDisplayName diff --git a/Stepic/Legacy/Model/Entities/Course/Course+CoreDataProperties.swift b/Stepic/Legacy/Model/Entities/Course/Course+CoreDataProperties.swift index c258164ece..705b123050 100644 --- a/Stepic/Legacy/Model/Entities/Course/Course+CoreDataProperties.swift +++ b/Stepic/Legacy/Model/Entities/Course/Course+CoreDataProperties.swift @@ -15,6 +15,7 @@ extension Course { @NSManaged var managedIsProctored: NSNumber? @NSManaged var managedIsFavorite: NSNumber? @NSManaged var managedIsArchived: NSNumber? + @NSManaged var managedIsInWishlist: NSNumber? @NSManaged var managedLearnersCount: NSNumber? @NSManaged var managedPreviewLessonId: NSNumber? @NSManaged var managedPreviewUnitId: NSNumber? @@ -50,6 +51,8 @@ extension Course { @NSManaged var managedIsPaid: NSNumber? @NSManaged var managedDisplayPrice: String? @NSManaged var managedDisplayPriceIAP: String? + @NSManaged var managedDisplayPriceTierPrice: String? + @NSManaged var managedDisplayPriceTierPromo: String? @NSManaged var managedPriceTier: NSNumber? @NSManaged var managedCurrencyCode: String? @@ -78,6 +81,8 @@ extension Course { @NSManaged var managedCourseBenefitByMonths: NSOrderedSet? @NSManaged var managedCourseBeneficiaries: NSSet? @NSManaged var managedAnnouncements: NSSet? + @NSManaged var managedMobileTiers: NSSet? + @NSManaged var managedWishlistEntries: NSOrderedSet? var id: Int { set(newId) { @@ -282,6 +287,15 @@ extension Course { } } + var isInWishlist: Bool { + get { + self.managedIsInWishlist?.boolValue ?? false + } + set { + self.managedIsInWishlist = NSNumber(value: newValue) + } + } + var isPublic: Bool { set(isPublic) { self.managedPublic = isPublic as NSNumber? @@ -318,6 +332,24 @@ extension Course { } } + var displayPriceTierPrice: String? { + get { + self.managedDisplayPriceTierPrice + } + set { + self.managedDisplayPriceTierPrice = newValue + } + } + + var displayPriceTierPromo: String? { + get { + self.managedDisplayPriceTierPromo + } + set { + self.managedDisplayPriceTierPromo = newValue + } + } + var priceTier: Int? { get { self.managedPriceTier?.intValue @@ -714,6 +746,24 @@ extension Course { } } + var mobileTiers: [MobileTier] { + get { + self.managedMobileTiers?.allObjects as! [MobileTier] + } + set { + self.managedMobileTiers = NSSet(array: newValue) + } + } + + var wishlistEntries: [WishlistEntryEntity] { + get { + self.managedWishlistEntries?.array as? [WishlistEntryEntity] ?? [] + } + set { + self.managedWishlistEntries = NSOrderedSet(array: newValue) + } + } + func addSection(_ section: Section) { let mutableItems = managedSections?.mutableCopy() as! NSMutableOrderedSet mutableItems.add(section) diff --git a/Stepic/Legacy/Model/Entities/Course/Course.swift b/Stepic/Legacy/Model/Entities/Course/Course.swift index c89548116a..61dedd8328 100644 --- a/Stepic/Legacy/Model/Entities/Course/Course.swift +++ b/Stepic/Legacy/Model/Entities/Course/Course.swift @@ -116,6 +116,7 @@ final class Course: NSManagedObject, ManagedObject, IDFetchable { self.isPublic = json[JSONKey.isPublic.rawValue].boolValue self.isFavorite = json[JSONKey.isFavorite.rawValue].boolValue self.isArchived = json[JSONKey.isArchived.rawValue].boolValue + self.isInWishlist = json[JSONKey.isInWishlist.rawValue].boolValue self.isProctored = json[JSONKey.isProctored.rawValue].boolValue self.readiness = json[JSONKey.readiness.rawValue].float @@ -173,50 +174,6 @@ final class Course: NSManagedObject, ManagedObject, IDFetchable { } } - @available(*, deprecated, message: "Legacy") - static func fetch( - _ ids: [Int], - featured: Bool? = nil, - enrolled: Bool? = nil, - isPublic: Bool? = nil - ) -> [Course] { - let request = NSFetchRequest(entityName: "Course") - let descriptor = NSSortDescriptor(key: "managedId", ascending: false) - - let idPredicates = ids.map { - NSPredicate(format: "managedId == %@", $0 as NSNumber) - } - let idCompoundPredicate = NSCompoundPredicate(type: .or, subpredicates: idPredicates) - - var nonIdPredicates = [NSPredicate]() - if let f = featured { - nonIdPredicates += [NSPredicate(format: "managedFeatured == %@", f as NSNumber)] - } - - if let e = enrolled { - nonIdPredicates += [NSPredicate(format: "managedEnrolled == %@", e as NSNumber)] - } - - if let p = isPublic { - nonIdPredicates += [NSPredicate(format: "managedPublic == %@", p as NSNumber)] - } - - let nonIdCompoundPredicate = NSCompoundPredicate(type: .and, subpredicates: nonIdPredicates) - - let predicate = NSCompoundPredicate(type: .and, subpredicates: [idCompoundPredicate, nonIdCompoundPredicate]) - request.predicate = predicate - request.sortDescriptors = [descriptor] - - do { - let results = try CoreDataHelper.shared.context.fetch(request) - let finalResult = results as? [Course] ?? [] - - return finalResult - } catch { - return [] - } - } - @available(*, deprecated, message: "Legacy") static func getAllCourses(enrolled: Bool? = nil) -> [Course] { let request = NSFetchRequest(entityName: "Course") @@ -254,6 +211,7 @@ final class Course: NSManagedObject, ManagedObject, IDFetchable { case isPublic = "is_public" case isFavorite = "is_favorite" case isArchived = "is_archived" + case isInWishlist = "is_in_wishlist" case readiness case summary case workload diff --git a/Stepic/Legacy/Model/Entities/MobileTier/MobileTier+CoreDataProperties.swift b/Stepic/Legacy/Model/Entities/MobileTier/MobileTier+CoreDataProperties.swift new file mode 100644 index 0000000000..f1f072c34c --- /dev/null +++ b/Stepic/Legacy/Model/Entities/MobileTier/MobileTier+CoreDataProperties.swift @@ -0,0 +1,76 @@ +import CoreData +import Foundation + +extension MobileTier { + @NSManaged var managedId: String + @NSManaged var managedCourseId: NSNumber + @NSManaged var managedPriceTier: String? + @NSManaged var managedPromoTier: String? + @NSManaged var managedPriceTierDisplayPrice: String? + @NSManaged var managedPromoTierDisplayPrice: String? + + @NSManaged var managedCourse: Course? + + var id: String { + get { + self.managedId + } + set { + self.managedId = newValue + } + } + + var courseID: Course.IdType { + get { + self.managedCourseId.intValue + } + set { + self.managedCourseId = NSNumber(value: newValue) + } + } + + var priceTier: String? { + get { + self.managedPriceTier + } + set { + self.managedPriceTier = newValue + } + } + + var promoTier: String? { + get { + self.managedPromoTier + } + set { + self.managedPromoTier = newValue + } + } + + var priceTierDisplayPrice: String? { + get { + self.managedPriceTierDisplayPrice + } + set { + self.managedPriceTierDisplayPrice = newValue + } + } + + var promoTierDisplayPrice: String? { + get { + self.managedPromoTierDisplayPrice + } + set { + self.managedPromoTierDisplayPrice = newValue + } + } + + var course: Course? { + get { + self.managedCourse + } + set { + self.managedCourse = newValue + } + } +} diff --git a/Stepic/Legacy/Model/Entities/MobileTier/MobileTier.swift b/Stepic/Legacy/Model/Entities/MobileTier/MobileTier.swift new file mode 100644 index 0000000000..11b680e761 --- /dev/null +++ b/Stepic/Legacy/Model/Entities/MobileTier/MobileTier.swift @@ -0,0 +1,42 @@ +import CoreData +import Foundation + +final class MobileTier: NSManagedObject, ManagedObject, Identifiable { + typealias IdType = String + + static var defaultSortDescriptors: [NSSortDescriptor] { + [NSSortDescriptor(key: #keyPath(managedId), ascending: true)] + } + + var isTiersEmpty: Bool { + (self.priceTier?.isEmpty ?? true) && (self.promoTier?.isEmpty ?? true) + } +} + +// MARK: - MobileTier (PlainObject Support) - + +extension MobileTier { + var plainObject: MobileTierPlainObject { + MobileTierPlainObject( + id: self.id, + courseID: self.courseID, + priceTier: self.priceTier, + promoTier: self.promoTier, + priceTierDisplayPrice: self.priceTierDisplayPrice, + promoTierDisplayPrice: self.promoTierDisplayPrice + ) + } + + static func insert(into context: NSManagedObjectContext, mobileTier: MobileTierPlainObject) -> MobileTier { + let entity: MobileTier = context.insertObject() + entity.update(mobileTier: mobileTier) + return entity + } + + func update(mobileTier: MobileTierPlainObject) { + self.id = mobileTier.id + self.courseID = mobileTier.courseID + self.priceTier = mobileTier.priceTier + self.promoTier = mobileTier.promoTier + } +} diff --git a/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift b/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift index 4f25b6905e..6c8b11b306 100644 --- a/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift +++ b/Stepic/Legacy/Model/Entities/User/User+CoreDataProperties.swift @@ -35,6 +35,7 @@ extension User { @NSManaged var managedCourseBeneficiaries: NSSet? @NSManaged var managedSearchResults: NSSet? @NSManaged var managedAnnouncements: NSSet? + @NSManaged var managedWishlistEntries: NSOrderedSet? @NSManaged var managedProfileEntity: Profile? @NSManaged var managedUserCourse: UserCourse? @@ -353,6 +354,15 @@ extension User { } } + var wishlistEntries: [WishlistEntryEntity] { + get { + self.managedWishlistEntries?.array as? [WishlistEntryEntity] ?? [] + } + set { + self.managedWishlistEntries = NSOrderedSet(array: newValue) + } + } + var authoredCourses: [Course] { get { self.managedAuthoredCourses?.allObjects as! [Course] diff --git a/Stepic/Legacy/Model/Entities/WishlistEntryEntity/WishlistEntryEntity+CoreDataProperties.swift b/Stepic/Legacy/Model/Entities/WishlistEntryEntity/WishlistEntryEntity+CoreDataProperties.swift new file mode 100644 index 0000000000..18076f0fa3 --- /dev/null +++ b/Stepic/Legacy/Model/Entities/WishlistEntryEntity/WishlistEntryEntity+CoreDataProperties.swift @@ -0,0 +1,77 @@ +import CoreData +import Foundation + +extension WishlistEntryEntity { + @NSManaged var managedId: NSNumber + @NSManaged var managedCourseId: NSNumber + @NSManaged var managedUserId: NSNumber + @NSManaged var managedCreateDate: Date? + @NSManaged var managedPlatform: String + + // Relationships + @NSManaged var managedCourse: Course? + @NSManaged var managedUser: User? + + var id: Int { + get { + self.managedId.intValue + } + set { + self.managedId = NSNumber(value: newValue) + } + } + + var courseID: Course.IdType { + get { + self.managedCourseId.intValue + } + set { + self.managedCourseId = NSNumber(value: newValue) + } + } + + var userID: User.IdType { + get { + self.managedUserId.intValue + } + set { + self.managedUserId = NSNumber(value: newValue) + } + } + + var createDate: Date? { + get { + self.managedCreateDate + } + set { + self.managedCreateDate = newValue + } + } + + var platform: String { + get { + self.managedPlatform + } + set { + self.managedPlatform = newValue + } + } + + var course: Course? { + get { + self.managedCourse + } + set { + self.managedCourse = newValue + } + } + + var user: User? { + get { + self.managedUser + } + set { + self.managedUser = newValue + } + } +} diff --git a/Stepic/Legacy/Model/Entities/WishlistEntryEntity/WishlistEntryEntity.swift b/Stepic/Legacy/Model/Entities/WishlistEntryEntity/WishlistEntryEntity.swift new file mode 100644 index 0000000000..cc81a181b8 --- /dev/null +++ b/Stepic/Legacy/Model/Entities/WishlistEntryEntity/WishlistEntryEntity.swift @@ -0,0 +1,44 @@ +import CoreData +import Foundation + +final class WishlistEntryEntity: NSManagedObject, ManagedObject, Identifiable { + typealias IdType = Int + + static var defaultSortDescriptors: [NSSortDescriptor] { + [ + NSSortDescriptor(key: #keyPath(managedCreateDate), ascending: false), + NSSortDescriptor(key: #keyPath(managedId), ascending: false) + ] + } +} + +// MARK: - WishlistEntryEntity (PlainObject Support) - + +extension WishlistEntryEntity { + var plainObject: WishlistEntryPlainObject { + WishlistEntryPlainObject( + id: self.id, + courseID: self.courseID, + userID: self.userID, + createDate: self.createDate, + platform: self.platform + ) + } + + static func insert( + into context: NSManagedObjectContext, + wishlistEntry: WishlistEntryPlainObject + ) -> WishlistEntryEntity { + let entity: WishlistEntryEntity = context.insertObject() + entity.update(wishlistEntry: wishlistEntry) + return entity + } + + func update(wishlistEntry: WishlistEntryPlainObject) { + self.id = wishlistEntry.id + self.courseID = wishlistEntry.courseID + self.userID = wishlistEntry.userID + self.createDate = wishlistEntry.createDate + self.platform = wishlistEntry.platform + } +} diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion b/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion index 3ea438f5c2..91629e1f30 100644 --- a/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model_user_course_can_be_reviewed_v90.xcdatamodel + Model_mobile_tiers_v92.xcdatamodel diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/Model_mobile_tiers_v92.xcdatamodel/contents b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_mobile_tiers_v92.xcdatamodel/contents new file mode 100644 index 0000000000..20b7ae18dd --- /dev/null +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_mobile_tiers_v92.xcdatamodel/contents @@ -0,0 +1,662 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_course_can_be_reviewed_v90.xcdatamodel/contents b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_course_can_be_reviewed_v90.xcdatamodel/contents index 5ec352d817..270df1f96e 100644 --- a/Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_course_can_be_reviewed_v90.xcdatamodel/contents +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_user_course_can_be_reviewed_v90.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/Model_wish_lists_v91.xcdatamodel/contents b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_wish_lists_v91.xcdatamodel/contents new file mode 100644 index 0000000000..a091ff383e --- /dev/null +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_wish_lists_v91.xcdatamodel/contents @@ -0,0 +1,649 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Stepic/Legacy/Model/Network/Endpoints/MobileTiersAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/MobileTiersAPI.swift new file mode 100644 index 0000000000..ef32dc71a5 --- /dev/null +++ b/Stepic/Legacy/Model/Network/Endpoints/MobileTiersAPI.swift @@ -0,0 +1,18 @@ +import Alamofire +import Foundation +import PromiseKit +import SwiftyJSON + +final class MobileTiersAPI: APIEndpoint { + override var name: String { "mobile-tiers" } + + private var calculateRequestEndpoint: String { "\(self.name)/calculate" } + + func calculate(request: MobileTierCalculateRequest) -> Promise { + self.create.request( + requestEndpoint: self.calculateRequestEndpoint, + bodyJSONObject: request.bodyJSON, + withManager: self.manager + ).map(MobileTierCalculateResponse.init) + } +} diff --git a/Stepic/Legacy/Model/Network/Endpoints/WishListsAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/WishListsAPI.swift new file mode 100644 index 0000000000..8130da7934 --- /dev/null +++ b/Stepic/Legacy/Model/Network/Endpoints/WishListsAPI.swift @@ -0,0 +1,62 @@ +import Alamofire +import Foundation +import PromiseKit + +final class WishListsAPI: APIEndpoint { + override var name: String { "wish-lists" } + + func retrieveWishlistEntry(courseID: Course.IdType) -> Promise { + self.retrieve.request( + requestEndpoint: self.name, + paramName: self.name, + params: ["course": courseID], + withManager: self.manager + ).then { wishlistEntries, _ -> Promise in + return .value(wishlistEntries.first) + } + } + + func retrieveWishlist(page: Int = 1) -> Promise<([WishlistEntryPlainObject], Meta)> { + self.retrieve.request( + requestEndpoint: self.name, + paramName: self.name, + params: ["page": page], + withManager: self.manager + ) + } + + func retrieveAllWishlistPages() -> Promise<[WishlistEntryPlainObject]> { + self.retrieve.requestWithCollectAllPages( + requestEndpoint: self.name, + paramName: self.name, + params: [:], + withManager: self.manager + ) + } + + func createWishlistEntry(courseID: Course.IdType) -> Promise { + let wishlistEntryToAdd = WishlistEntryPlainObject( + id: -1, + courseID: courseID, + userID: -1, + createDate: nil, + platform: PlatformType.mobile.stringValue + ) + + return self.create.request( + requestEndpoint: self.name, + paramName: "wish-list", + creatingObject: wishlistEntryToAdd, + withManager: self.manager + ).compactMap { _, json -> WishlistEntryPlainObject? in + if let createdObjectJSON = json[self.name].arrayValue.first { + return WishlistEntryPlainObject(json: createdObjectJSON) + } + return nil + } + } + + func deleteWishlistEntry(wishlistEntryID: WishlistEntryPlainObject.IdType) -> Promise { + self.delete.request(requestEndpoint: self.name, deletingId: wishlistEntryID, withManager: self.manager) + } +} diff --git a/Stepic/Legacy/Model/PlainObjects/MobileTiers/MobileTierCalculateRequest.swift b/Stepic/Legacy/Model/PlainObjects/MobileTiers/MobileTierCalculateRequest.swift new file mode 100644 index 0000000000..c1bf58fbf8 --- /dev/null +++ b/Stepic/Legacy/Model/PlainObjects/MobileTiers/MobileTierCalculateRequest.swift @@ -0,0 +1,31 @@ +import Foundation + +struct MobileTierCalculateRequest { + let params: [Param] + + var bodyJSON: [JSONDictionary] { + self.params.map { param in + var dict: JSONDictionary = [ + JSONKey.course.rawValue: param.courseID, + JSONKey.store.rawValue: PaymentStore.appStore.rawValue + ] + + if let promoCodeName = param.promoCodeName { + dict[JSONKey.promo.rawValue] = promoCodeName + } + + return dict + } + } + + struct Param { + let courseID: Int + let promoCodeName: String? + } + + enum JSONKey: String { + case course + case store + case promo + } +} diff --git a/Stepic/Legacy/Model/PlainObjects/MobileTiers/MobileTierCalculateResponse.swift b/Stepic/Legacy/Model/PlainObjects/MobileTiers/MobileTierCalculateResponse.swift new file mode 100644 index 0000000000..93d25bfec7 --- /dev/null +++ b/Stepic/Legacy/Model/PlainObjects/MobileTiers/MobileTierCalculateResponse.swift @@ -0,0 +1,14 @@ +import Foundation +import SwiftyJSON + +struct MobileTierCalculateResponse { + let mobileTiers: [MobileTierPlainObject] + + init(json: JSON) { + self.mobileTiers = json[JSONKey.mobileTiers.rawValue].arrayValue.map(MobileTierPlainObject.init) + } + + enum JSONKey: String { + case mobileTiers = "mobile-tiers" + } +} diff --git a/Stepic/Legacy/Model/PlainObjects/MobileTiers/MobileTierPlainObject.swift b/Stepic/Legacy/Model/PlainObjects/MobileTiers/MobileTierPlainObject.swift new file mode 100644 index 0000000000..06561384bb --- /dev/null +++ b/Stepic/Legacy/Model/PlainObjects/MobileTiers/MobileTierPlainObject.swift @@ -0,0 +1,28 @@ +import Foundation +import SwiftyJSON + +struct MobileTierPlainObject { + let id: String + let courseID: Int + let priceTier: String? + let promoTier: String? + + var priceTierDisplayPrice: String? + var promoTierDisplayPrice: String? +} + +extension MobileTierPlainObject { + init(json: JSON) { + self.id = json[JSONKey.id.rawValue].stringValue + self.courseID = json[JSONKey.course.rawValue].intValue + self.priceTier = json[JSONKey.priceTier.rawValue].string + self.promoTier = json[JSONKey.promoTier.rawValue].string + } + + enum JSONKey: String { + case id + case course + case priceTier = "price_tier" + case promoTier = "promo_tier" + } +} diff --git a/Stepic/Legacy/Model/PlainObjects/WishlistEntryPlainObject.swift b/Stepic/Legacy/Model/PlainObjects/WishlistEntryPlainObject.swift new file mode 100644 index 0000000000..5183f5525d --- /dev/null +++ b/Stepic/Legacy/Model/PlainObjects/WishlistEntryPlainObject.swift @@ -0,0 +1,37 @@ +import Foundation +import SwiftyJSON + +struct WishlistEntryPlainObject: JSONSerializable { + let id: Int + let courseID: Int + let userID: Int + let createDate: Date? + let platform: String +} + +extension WishlistEntryPlainObject { + var json: JSON { + [ + JSONKey.course.rawValue: self.courseID, + JSONKey.platform.rawValue: self.platform + ] + } + + init(json: JSON) { + self.id = json[JSONKey.id.rawValue].intValue + self.courseID = json[JSONKey.course.rawValue].intValue + self.userID = json[JSONKey.user.rawValue].intValue + self.createDate = Parser.dateFromTimedateJSON(json[JSONKey.createDate.rawValue]) + self.platform = json[JSONKey.platform.rawValue].stringValue + } + + func update(json: JSON) {} + + enum JSONKey: String { + case id + case course + case user + case createDate = "create_date" + case platform + } +} diff --git a/Stepic/Legacy/Model/StorageRecords/StorageRecord.swift b/Stepic/Legacy/Model/StorageRecords/StorageRecord.swift index b79093f6e6..b8e22fe783 100644 --- a/Stepic/Legacy/Model/StorageRecords/StorageRecord.swift +++ b/Stepic/Legacy/Model/StorageRecords/StorageRecord.swift @@ -45,8 +45,6 @@ final class StorageRecord: JSONSerializable { return DeadlineStorageRecordData(json: json) case .personalOffers: return PersonalOfferStorageRecordData(json: json) - case .wishlist: - return WishlistStorageRecordData(json: json) } } diff --git a/Stepic/Legacy/Model/StorageRecords/StorageRecordData.swift b/Stepic/Legacy/Model/StorageRecords/StorageRecordData.swift index cdd7e515a6..b75ccebce0 100644 --- a/Stepic/Legacy/Model/StorageRecords/StorageRecordData.swift +++ b/Stepic/Legacy/Model/StorageRecords/StorageRecordData.swift @@ -95,27 +95,3 @@ final class PersonalOfferStorageRecordData: StorageRecordData { case promoStories = "promo_stories" } } - -// MARK: Wishlist - -final class WishlistStorageRecordData: StorageRecordData { - var coursesIDs: [Course.IdType] - - var dictValue: [String: Any] { - [ - JSONKey.courses.rawValue: self.coursesIDs - ] - } - - init(coursesIDs: [Course.IdType]) { - self.coursesIDs = coursesIDs - } - - init(json: JSON) { - self.coursesIDs = json[JSONKey.courses.rawValue].arrayValue.map(\.intValue) - } - - enum JSONKey: String { - case courses - } -} diff --git a/Stepic/Legacy/Model/StorageRecords/StorageRecordKind.swift b/Stepic/Legacy/Model/StorageRecords/StorageRecordKind.swift index c4fec32138..a716cebdab 100644 --- a/Stepic/Legacy/Model/StorageRecords/StorageRecordKind.swift +++ b/Stepic/Legacy/Model/StorageRecords/StorageRecordKind.swift @@ -3,7 +3,6 @@ import Foundation enum StorageRecordKind { case deadline(courseID: Int) case personalOffers - case wishlist var name: String { switch self { @@ -11,8 +10,6 @@ enum StorageRecordKind { return "deadline_\(courseID)" case .personalOffers: return "personal_offers" - case .wishlist: - return "wishlist" } } @@ -22,8 +19,6 @@ enum StorageRecordKind { return .deadline case .personalOffers: return .personalOffers - case .wishlist: - return .wishlist } } @@ -37,9 +32,6 @@ enum StorageRecordKind { } else if PrefixType(rawValue: string) == .personalOffers { self = .personalOffers return - } else if PrefixType(rawValue: string) == .wishlist { - self = .wishlist - return } return nil } @@ -47,7 +39,6 @@ enum StorageRecordKind { enum PrefixType: String { case deadline case personalOffers = "personal_offers" - case wishlist var prefix: String { switch self { @@ -55,8 +46,6 @@ enum StorageRecordKind { return "deadline_" case .personalOffers: return self.rawValue - case .wishlist: - return self.rawValue } } @@ -66,8 +55,6 @@ enum StorageRecordKind { return "deadline" case .personalOffers: return self.rawValue - case .wishlist: - return self.rawValue } } } diff --git a/Stepic/Legacy/Services/CourseSubscriber.swift b/Stepic/Legacy/Services/CourseSubscriber.swift index a8ae3c4e0c..4331938b72 100644 --- a/Stepic/Legacy/Services/CourseSubscriber.swift +++ b/Stepic/Legacy/Services/CourseSubscriber.swift @@ -15,16 +15,10 @@ enum CourseSubscriptionSource: String { } protocol CourseSubscriberProtocol { - func join(course: Course, source: CourseSubscriptionSource, isWishlisted: Bool?) -> Promise + func join(course: Course, source: CourseSubscriptionSource) -> Promise func leave(course: Course, source: CourseSubscriptionSource) -> Promise } -extension CourseSubscriberProtocol { - func join(course: Course, source: CourseSubscriptionSource) -> Promise { - self.join(course: course, source: source, isWishlisted: nil) - } -} - @available(*, deprecated, message: "Legacy code") final class CourseSubscriber: CourseSubscriberProtocol { enum CourseSubscriptionError: Error { @@ -40,23 +34,22 @@ final class CourseSubscriber: CourseSubscriberProtocol { self.analytics = analytics } - func join(course: Course, source: CourseSubscriptionSource, isWishlisted: Bool?) -> Promise { - self.performCourseJoinActions(course: course, unsubscribe: false, source: source, isWishlisted: isWishlisted) + func join(course: Course, source: CourseSubscriptionSource) -> Promise { + self.performCourseJoinActions(course: course, unsubscribe: false, source: source) } func leave(course: Course, source: CourseSubscriptionSource) -> Promise { - self.performCourseJoinActions(course: course, unsubscribe: true, source: source, isWishlisted: nil) + self.performCourseJoinActions(course: course, unsubscribe: true, source: source) } private func performCourseJoinActions( course: Course, unsubscribe: Bool, - source: CourseSubscriptionSource, - isWishlisted: Bool? + source: CourseSubscriptionSource ) -> Promise { Promise { seal in _ = ApiDataDownloader.enrollments.joinCourse(course, delete: unsubscribe, success: { [weak self] in - guard let progressId = course.progressId else { + guard let progressID = course.progressId else { seal.reject(CourseSubscriptionError.badResponseFormat) return } @@ -66,7 +59,12 @@ final class CourseSubscriber: CourseSubscriberProtocol { AnalyticsUserProperties.shared.decrementCoursesCount() } else { self?.analytics.send( - .courseJoined(source: source, id: course.id, title: course.title, isWishlisted: isWishlisted) + .courseJoined( + source: source, + id: course.id, + title: course.title, + isWishlisted: course.isInWishlist + ) ) AnalyticsUserProperties.shared.incrementCoursesCount() } @@ -81,7 +79,7 @@ final class CourseSubscriber: CourseSubscriberProtocol { } ApiDataDownloader.progresses.retrieve( - ids: [progressId], + ids: [progressID], existing: course.progress != nil ? [course.progress!] : [], refreshMode: .update, success: { progresses in diff --git a/Stepic/Sources/Frameworks/InAppPurchases/IAPProductIDs.plist b/Stepic/Sources/Frameworks/InAppPurchases/IAPProductIDs.plist index c2cc5b760e..3867b1638d 100644 Binary files a/Stepic/Sources/Frameworks/InAppPurchases/IAPProductIDs.plist and b/Stepic/Sources/Frameworks/InAppPurchases/IAPProductIDs.plist differ diff --git a/Stepic/Sources/Frameworks/InAppPurchases/IAPService.swift b/Stepic/Sources/Frameworks/InAppPurchases/IAPService.swift index aa940618ae..6498752462 100644 --- a/Stepic/Sources/Frameworks/InAppPurchases/IAPService.swift +++ b/Stepic/Sources/Frameworks/InAppPurchases/IAPService.swift @@ -8,6 +8,7 @@ protocol IAPServiceProtocol: AnyObject { func getLocalizedPrice(for product: SKProduct) -> String? func getLocalizedPrice(for course: Course) -> Guarantee + func getLocalizedPrices(for mobileTier: MobileTier) -> Guarantee<(price: String?, promo: String?)> func startObservingPayments() func stopObservingPayments() @@ -56,17 +57,7 @@ final class IAPService: IAPServiceProtocol { let productIdentifier = self.productsService.makeProductIdentifier(priceTier: priceTier) - guard self.productsService.canFetchProduct(with: productIdentifier) else { - return Promise(error: Error.unsupportedCourse) - } - - return Promise { seal in - self.productsService.fetchProduct(productIdentifier: productIdentifier).done { product in - seal.fulfill(product) - }.catch { _ in - seal.reject(Error.productsRequestFailed) - } - } + return self.fetchProduct(with: productIdentifier) } func fetchProducts() -> Promise<[SKProduct]> { @@ -138,6 +129,65 @@ final class IAPService: IAPServiceProtocol { } } + func getLocalizedPrices(for mobileTier: MobileTier) -> Guarantee<(price: String?, promo: String?)> { + if mobileTier.isTiersEmpty { + return .value((nil, nil)) + } + + func getLocalizedPrice(for tier: String?) -> Guarantee { + guard let tier = tier else { + return .value(nil) + } + + self.mutex.unbalancedLock() + defer { self.mutex.unbalancedUnlock() } + + if let product = self.products.first(where: { $0.productIdentifier == tier }) { + return .value(self.getLocalizedPrice(for: product)) + } + + return Guarantee { seal in + self.fetchProduct(with: tier).compactMap { $0 }.done { product in + self.mutex.unbalancedLock() + defer { self.mutex.unbalancedUnlock() } + + if !self.products.contains(where: { $0.productIdentifier == tier }) { + self.products.append(product) + } + + seal(self.getLocalizedPrice(for: product)) + }.catch { _ in + seal(nil) + } + } + } + + return Guarantee { seal in + when( + fulfilled: getLocalizedPrice(for: mobileTier.priceTier), + getLocalizedPrice(for: mobileTier.promoTier) + ).done { priceTierLocalizedPrice, promoTierLocalizedPrice in + seal((priceTierLocalizedPrice, promoTierLocalizedPrice)) + }.catch { _ in + seal((nil, nil)) + } + } + } + + private func fetchProduct(with productIdentifier: IAPProductIdentifier) -> Promise { + guard self.productsService.canFetchProduct(with: productIdentifier) else { + return Promise(error: Error.unsupportedCourse) + } + + return Promise { seal in + self.productsService.fetchProduct(productIdentifier: productIdentifier).done { product in + seal.fulfill(product) + }.catch { _ in + seal.reject(Error.productsRequestFailed) + } + } + } + // MARK: Payments func startObservingPayments() { diff --git a/Stepic/Sources/Model/PaymentStore.swift b/Stepic/Sources/Model/PaymentStore.swift new file mode 100644 index 0000000000..9f010bd857 --- /dev/null +++ b/Stepic/Sources/Model/PaymentStore.swift @@ -0,0 +1,15 @@ +import Foundation + +enum PaymentStore: String { + case appStore = "app_store" + case googlePlay = "google_play" + + var intValue: Int { + switch self { + case .appStore: + return 1 + case .googlePlay: + return 2 + } + } +} diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift index d087ba3889..2b0d1ef418 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoAssembly.swift @@ -37,7 +37,9 @@ final class CourseInfoAssembly: Assembly { coursePurchasesPersistenceService: CoursePurchasesPersistenceService(), coursePurchasesNetworkService: CoursePurchasesNetworkService(coursePurchasesAPI: CoursePurchasesAPI()), userCoursesNetworkService: UserCoursesNetworkService(userCoursesAPI: UserCoursesAPI()), - promoCodesNetworkService: PromoCodesNetworkService(promoCodesAPI: PromoCodesAPI()) + promoCodesNetworkService: PromoCodesNetworkService(promoCodesAPI: PromoCodesAPI()), + wishlistRepository: WishlistRepository.default, + mobileTiersRepository: MobileTiersRepository.default ) let presenter = CourseInfoPresenter(urlFactory: StepikURLFactory()) @@ -71,7 +73,6 @@ final class CourseInfoAssembly: Assembly { notificationsRegistrationService: notificationsRegistrationService, spotlightIndexingService: SpotlightIndexingService.shared, visitedCourseListPersistenceService: visitedCourseListPersistenceService.require(), - wishlistService: WishlistService.default, urlFactory: StepikURLFactory(), dataBackUpdateService: dataBackUpdateService, iapService: IAPService.shared, diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift index e182f53d8a..fa6df2c105 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoDataFlow.swift @@ -42,10 +42,11 @@ enum CourseInfo { struct Response { struct Data { let course: Course - let isWishlisted: Bool let isWishlistAvailable: Bool let isCourseRevenueAvailable: Bool + let coursePurchaseFlow: CoursePurchaseFlowType let promoCode: PromoCode? + let mobileTier: MobileTierPlainObject? } var result: StepikResult @@ -67,10 +68,12 @@ enum CourseInfo { enum LessonPresentation { struct Response { let unitID: Unit.IdType + let promoCodeName: String? } struct ViewModel { let unitID: Unit.IdType + let promoCodeName: String? } } @@ -244,10 +247,12 @@ enum CourseInfo { struct Response { let previewLessonID: Lesson.IdType + let promoCodeName: String? } struct ViewModel { let previewLessonID: Lesson.IdType + let promoCodeName: String? } } @@ -293,6 +298,21 @@ enum CourseInfo { } } + /// Present CourseInfoPurchaseModal module + enum PaidCoursePurchaseModalPresentation { + struct Response { + let courseID: Course.IdType + let promoCodeName: String? + let mobileTierID: MobileTier.IdType? + } + + struct ViewModel { + let courseID: Course.IdType + let promoCodeName: String? + let mobileTierID: MobileTier.IdType? + } + } + /// Update remind purchase course notification enum PurchaseNotificationUpdate { struct Request {} diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift index 221e58b17a..6b3ca5a764 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoInteractor.swift @@ -33,7 +33,6 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { private let notificationsRegistrationService: NotificationsRegistrationServiceProtocol private let spotlightIndexingService: SpotlightIndexingServiceProtocol private let visitedCourseListPersistenceService: VisitedCourseListPersistenceServiceProtocol - private let wishlistService: WishlistServiceProtocol private let urlFactory: StepikURLFactory private let analytics: Analytics private let courseViewSource: AnalyticsEvent.CourseViewSource @@ -59,6 +58,8 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { private let promoCodeName: String? private var currentPromoCode: PromoCode? + private var currentMobileTier: MobileTier? + private var courseWebURL: URL? { guard let course = self.currentCourse else { return nil @@ -107,7 +108,6 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { notificationsRegistrationService: NotificationsRegistrationServiceProtocol, spotlightIndexingService: SpotlightIndexingServiceProtocol, visitedCourseListPersistenceService: VisitedCourseListPersistenceServiceProtocol, - wishlistService: WishlistServiceProtocol, urlFactory: StepikURLFactory, dataBackUpdateService: DataBackUpdateServiceProtocol, iapService: IAPServiceProtocol, @@ -126,7 +126,6 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { self.notificationsRegistrationService = notificationsRegistrationService self.spotlightIndexingService = spotlightIndexingService self.visitedCourseListPersistenceService = visitedCourseListPersistenceService - self.wishlistService = wishlistService self.urlFactory = urlFactory self.dataBackUpdateService = dataBackUpdateService self.iapService = iapService @@ -243,12 +242,11 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { } func doWishlistMainAction(request: CourseInfo.CourseWishlistMainAction.Request) { - guard let course = self.currentCourse, - let currentUserID = self.userAccountService.currentUserID else { + guard let course = self.currentCourse else { return } - let targetAction = self.wishlistService.contains(course) + let targetAction = course.isInWishlist ? CourseInfo.CourseWishlistAction.remove : CourseInfo.CourseWishlistAction.add @@ -265,7 +263,7 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { viewSource: self.courseViewSource ) ) - return self.wishlistService.add(course, userID: currentUserID) + return self.provider.addCourseToWishlist() case .remove: self.analytics.send( .wishlistCourseRemoved( @@ -275,7 +273,7 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { viewSource: self.courseViewSource ) ) - return self.wishlistService.remove(course, userID: currentUserID) + return self.provider.deleteCourseFromWishlist() } }.done { self.presenter.presentCourse(response: .init(result: .success(self.makeCourseData()))) @@ -312,37 +310,47 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { ) ) } else { - let isWishlisted = self.wishlistService.contains(self.courseID) // Paid course -> open web page if course.isPaid && !course.isPurchased { self.analytics.send( .buyCoursePressed(id: course.id), - .courseBuyPressed(source: .courseScreen, id: course.id, isWishlisted: isWishlisted) + .courseBuyPressed(source: .courseScreen, id: course.id, isWishlisted: course.isInWishlist) ) - if self.iapService.canBuyCourse(course) { - self.iapService.buy(course: course, delegate: self) - } else { + switch self.remoteConfig.coursePurchaseFlow { + case .web: + if self.iapService.canBuyCourse(course) { + self.iapService.buy(course: course, delegate: self) + } else { + self.presenter.presentWaitingState(response: .init(shouldDismiss: true)) + self.presenter.presentPaidCourseBuying( + response: .init(course: course, courseViewSource: self.courseViewSource) + ) + } + + return self.coursePurchaseReminder.createPurchaseNotification(for: course) + case .iap: self.presenter.presentWaitingState(response: .init(shouldDismiss: true)) - self.presenter.presentPaidCourseBuying( - response: .init(course: course, courseViewSource: self.courseViewSource) + return self.presenter.presentPaidCoursePurchaseModal( + response: .init( + courseID: self.courseID, + promoCodeName: self.promoCodeName, + mobileTierID: self.currentMobileTier?.id + ) ) } - - return self.coursePurchaseReminder.createPurchaseNotification(for: course) } self.analytics.send(.authorizedUserTappedJoinCourse) // Unenrolled course -> join, open last step - self.courseSubscriber.join(course: course, source: .preview, isWishlisted: isWishlisted).done { course in + self.courseSubscriber.join(course: course, source: .preview).done { course in // Refresh course self.currentCourse = course self.presenter.presentCourse(response: .init(result: .success(self.makeCourseData()))) // Remove course from wishlist - if self.wishlistService.contains(course), - let currentUserID = self.userAccountService.currentUserID { - self.wishlistService.remove(course, userID: currentUserID).cauterize() + if course.isInWishlist { + self.provider.deleteCourseFromWishlist().cauterize() } // Present step @@ -365,7 +373,9 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { func doPreviewLessonPresentation(request: CourseInfo.PreviewLessonPresentation.Request) { if let previewLessonID = self.currentCourse?.previewLessonID { - self.presenter.presentPreviewLesson(response: .init(previewLessonID: previewLessonID)) + self.presenter.presentPreviewLesson( + response: .init(previewLessonID: previewLessonID, promoCodeName: self.promoCodeName) + ) } } @@ -395,14 +405,29 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { } private func makeCourseData() -> CourseInfo.CourseLoad.Response.Data { - let isWishlistAvailable = self.userAccountService.isAuthorized - && self.wishlistService.canAdd(self.currentCourse.require()) + let mobileTier: MobileTier? = { + guard self.remoteConfig.coursePurchaseFlow == .iap else { + return nil + } + + if let currentMobileTier = self.currentMobileTier { + return currentMobileTier + } else { + if let promoCodeName = self.promoCodeName, + let promoTier = self.currentCourse?.mobileTiers.first(where: { $0.id.hasSuffix(promoCodeName) }) { + return promoTier + } + return self.currentCourse?.mobileTiers.first(where: { $0.id.hasSuffix("None") }) + } + }() + return .init( course: self.currentCourse.require(), - isWishlisted: self.wishlistService.contains(self.courseID), - isWishlistAvailable: isWishlistAvailable, + isWishlistAvailable: self.userAccountService.isAuthorized && !self.currentCourse.require().enrolled, isCourseRevenueAvailable: self.remoteConfig.isCourseRevenueAvailable, - promoCode: self.currentPromoCode + coursePurchaseFlow: self.remoteConfig.coursePurchaseFlow, + promoCode: self.currentPromoCode, + mobileTier: mobileTier?.plainObject ) } @@ -431,18 +456,8 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { } } - if let course = course, - course.isPaid && self.iapService.canBuyCourse(course) && course.displayPriceIAP?.isEmpty ?? true { - self.iapService.getLocalizedPrice(for: course).done { localizedPrice in - self.currentCourse?.displayPriceIAP = localizedPrice - DispatchQueue.main.async { - self.presenter.presentCourse(response: .init(result: .success(self.makeCourseData()))) - } - } - } - DispatchQueue.main.async { - self.fetchAndPresentPromoCodeIfNeeded() + self.fetchAndPresentPriceInfoIfNeeded() } if !self.didLoadFromCache { @@ -462,6 +477,43 @@ final class CourseInfoInteractor: CourseInfoInteractorProtocol { } } + private func fetchAndPresentPriceInfoIfNeeded() { + switch self.remoteConfig.coursePurchaseFlow { + case .web: + if let course = self.currentCourse, + course.isPaid && self.iapService.canBuyCourse(course) && (course.displayPriceIAP?.isEmpty ?? true) { + self.iapService.getLocalizedPrice(for: course).done { localizedPrice in + self.currentCourse?.displayPriceIAP = localizedPrice + self.presenter.presentCourse(response: .init(result: .success(self.makeCourseData()))) + } + } + + self.fetchAndPresentPromoCodeIfNeeded() + case .iap: + guard self.currentMobileTier == nil else { + return + } + + self.provider + .calculateMobileTier(promoCodeName: self.promoCodeName) + .compactMap { $0 } + .compactMap { mobileTier in + self.currentCourse?.mobileTiers.first(where: { $0.id == mobileTier.id }) + } + .then { mobileTier -> Guarantee<(MobileTier, String?, String?)> in + self.iapService.getLocalizedPrices(for: mobileTier).map { (mobileTier, $0.price, $0.promo) } + } + .done { mobileTier, priceTierLocalizedPrice, promoTierLocalizedPrice in + mobileTier.priceTierDisplayPrice = priceTierLocalizedPrice + mobileTier.promoTierDisplayPrice = promoTierLocalizedPrice + self.currentMobileTier = mobileTier + + self.presenter.presentCourse(response: .init(result: .success(self.makeCourseData()))) + } + .cauterize() + } + } + private func fetchAndPresentPromoCodeIfNeeded() { guard self.currentPromoCode == nil, let course = self.currentCourse, course.isPaid else { @@ -560,31 +612,21 @@ extension CourseInfoInteractor: LessonOutputProtocol { extension CourseInfoInteractor: CourseInfoTabSyllabusOutputProtocol { func presentLesson(in unit: Unit) { - self.presenter.presentLesson( - response: CourseInfo.LessonPresentation.Response(unitID: unit.id) - ) + self.presenter.presentLesson(response: .init(unitID: unit.id, promoCodeName: self.promoCodeName)) } func presentPersonalDeadlinesCreation(for course: Course) { - self.presenter.presentPersonalDeadlinesSettings( - response: .init(action: .create, course: course) - ) + self.presenter.presentPersonalDeadlinesSettings(response: .init(action: .create, course: course)) } func presentPersonalDeadlinesSettings(for course: Course) { - self.presenter.presentPersonalDeadlinesSettings( - response: .init(action: .edit, course: course) - ) + self.presenter.presentPersonalDeadlinesSettings(response: .init(action: .edit, course: course)) } func presentExamLesson() { - guard let urlPath = self.courseWebSyllabusURLPath else { - return + if let courseWebSyllabusURLPath = self.courseWebSyllabusURLPath { + self.presenter.presentExamLesson(response: .init(urlPath: courseWebSyllabusURLPath)) } - - self.presenter.presentExamLesson( - response: .init(urlPath: urlPath) - ) } } diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift index 424fa6ed16..72c66731b3 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoPresenter.swift @@ -14,6 +14,7 @@ protocol CourseInfoPresenterProtocol { func presentCourseRevenue(response: CourseInfo.CourseRevenuePresentation.Response) func presentAuthorization(response: CourseInfo.AuthorizationPresentation.Response) func presentPaidCourseBuying(response: CourseInfo.PaidCourseBuyingPresentation.Response) + func presentPaidCoursePurchaseModal(response: CourseInfo.PaidCoursePurchaseModalPresentation.Response) func presentIAPNotAllowed(response: CourseInfo.IAPNotAllowedPresentation.Response) func presentIAPReceiptValidationFailed(response: CourseInfo.IAPReceiptValidationFailedPresentation.Response) func presentIAPPaymentFailed(response: CourseInfo.IAPPaymentFailedPresentation.Response) @@ -37,10 +38,11 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { case .success(let data): let headerViewModel = self.makeHeaderViewModel( course: data.course, - isWishlisted: data.isWishlisted, + coursePurchaseFlow: data.coursePurchaseFlow, isWishlistAvailable: data.isWishlistAvailable, isCourseRevenueAvailable: data.isCourseRevenueAvailable, - promoCode: data.promoCode + promoCode: data.promoCode, + mobileTier: data.mobileTier ) self.viewController?.displayCourse(viewModel: .init(state: .result(data: headerViewModel))) case .failure: @@ -50,7 +52,7 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { func presentLesson(response: CourseInfo.LessonPresentation.Response) { self.viewController?.displayLesson( - viewModel: CourseInfo.LessonPresentation.ViewModel(unitID: response.unitID) + viewModel: .init(unitID: response.unitID, promoCodeName: response.promoCodeName) ) } @@ -102,7 +104,9 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { } func presentPreviewLesson(response: CourseInfo.PreviewLessonPresentation.Response) { - self.viewController?.displayPreviewLesson(viewModel: .init(previewLessonID: response.previewLessonID)) + self.viewController?.displayPreviewLesson( + viewModel: .init(previewLessonID: response.previewLessonID, promoCodeName: response.promoCodeName) + ) } func presentCourseRevenue(response: CourseInfo.CourseRevenuePresentation.Response) { @@ -125,6 +129,16 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { self.viewController?.displayPaidCourseBuying(viewModel: .init(urlPath: payForCourseURL.absoluteString)) } + func presentPaidCoursePurchaseModal(response: CourseInfo.PaidCoursePurchaseModalPresentation.Response) { + self.viewController?.displayPaidCoursePurchaseModal( + viewModel: .init( + courseID: response.courseID, + promoCodeName: response.promoCodeName, + mobileTierID: response.mobileTierID + ) + ) + } + func presentIAPNotAllowed(response: CourseInfo.IAPNotAllowedPresentation.Response) { if let payForCourseURL = self.urlFactory.makePayForCourse(id: response.course.id) { self.viewController?.displayIAPNotAllowed( @@ -253,10 +267,11 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { private func makeHeaderViewModel( course: Course, - isWishlisted: Bool, + coursePurchaseFlow: CoursePurchaseFlowType, isWishlistAvailable: Bool, isCourseRevenueAvailable: Bool, - promoCode: PromoCode? + promoCode: PromoCode?, + mobileTier: MobileTierPlainObject? ) -> CourseInfoHeaderViewModel { let rating = course.reviewSummary?.rating ?? 0 @@ -280,20 +295,24 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { isEnrolled: course.enrolled, isFavorite: course.isFavorite, isArchived: course.isArchived, - isWishlisted: isWishlisted, + isWishlisted: course.isInWishlist, isWishlistAvailable: isWishlistAvailable, isTryForFreeAvailable: isTryForFreeAvailable, isRevenueAvailable: isCourseRevenueAvailable && course.canViewRevenue, buttonDescription: self.makeButtonDescription( course: course, - promoCode: promoCode + coursePurchaseFlow: coursePurchaseFlow, + promoCode: promoCode, + mobileTier: mobileTier ) ) } private func makeButtonDescription( course: Course, - promoCode: PromoCode? + coursePurchaseFlow: CoursePurchaseFlowType, + promoCode: PromoCode?, + mobileTier: MobileTierPlainObject? ) -> CourseInfoHeaderViewModel.ButtonDescription { let isEnrolled = course.enrolled let isEnabled = isEnrolled ? course.canContinue : true @@ -307,13 +326,26 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { if isNotPurchased { let displayPrice: String? - if let displayPriceIAP = course.displayPriceIAP { - displayPrice = displayPriceIAP - } else if let promoCode = promoCode { - displayPrice = FormatterHelper.price(promoCode.price, currencyCode: promoCode.currencyCode) - isPromo = true - } else { - displayPrice = course.displayPrice + + switch coursePurchaseFlow { + case .web: + if let displayPriceIAP = course.displayPriceIAP { + displayPrice = displayPriceIAP + } else if let promoCode = promoCode { + displayPrice = FormatterHelper.price(promoCode.price, currencyCode: promoCode.currencyCode) + isPromo = true + } else { + displayPrice = course.displayPrice + } + case .iap: + if let promoTierDisplayPrice = mobileTier?.promoTierDisplayPrice { + displayPrice = promoTierDisplayPrice + isPromo = true + } else if let priceTierDisplayPrice = mobileTier?.priceTierDisplayPrice { + displayPrice = priceTierDisplayPrice + } else { + displayPrice = course.displayPrice + } } if let displayPrice = displayPrice { @@ -325,10 +357,16 @@ final class CourseInfoPresenter: CourseInfoPresenterProtocol { }() let subtitle: String? = { - if isNotPurchased && promoCode != nil { + guard isNotPurchased && isPromo else { + return nil + } + + switch coursePurchaseFlow { + case .web: return course.displayPrice + case .iap: + return mobileTier?.priceTierDisplayPrice ?? course.displayPrice } - return nil }() return CourseInfoHeaderViewModel.ButtonDescription( diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoProvider.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoProvider.swift index e2a9320f3b..a8b46c8e24 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoProvider.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoProvider.swift @@ -9,6 +9,10 @@ protocol CourseInfoProviderProtocol { func updateUserCourse(_ userCourse: UserCourse) -> Promise func checkPromoCode(name: String) -> Promise + func calculateMobileTier(promoCodeName: String?) -> Promise + + func addCourseToWishlist() -> Promise + func deleteCourseFromWishlist() -> Promise } final class CourseInfoProvider: CourseInfoProviderProtocol { @@ -30,6 +34,10 @@ final class CourseInfoProvider: CourseInfoProviderProtocol { private let promoCodesNetworkService: PromoCodesNetworkServiceProtocol + private let wishlistRepository: WishlistRepositoryProtocol + + private let mobileTiersRepository: MobileTiersRepositoryProtocol + init( courseID: Course.IdType, coursesPersistenceService: CoursesPersistenceServiceProtocol, @@ -41,7 +49,9 @@ final class CourseInfoProvider: CourseInfoProviderProtocol { coursePurchasesPersistenceService: CoursePurchasesPersistenceServiceProtocol, coursePurchasesNetworkService: CoursePurchasesNetworkServiceProtocol, userCoursesNetworkService: UserCoursesNetworkServiceProtocol, - promoCodesNetworkService: PromoCodesNetworkServiceProtocol + promoCodesNetworkService: PromoCodesNetworkServiceProtocol, + wishlistRepository: WishlistRepositoryProtocol, + mobileTiersRepository: MobileTiersRepositoryProtocol ) { self.courseID = courseID self.coursesNetworkService = coursesNetworkService @@ -54,6 +64,8 @@ final class CourseInfoProvider: CourseInfoProviderProtocol { self.coursePurchasesNetworkService = coursePurchasesNetworkService self.userCoursesNetworkService = userCoursesNetworkService self.promoCodesNetworkService = promoCodesNetworkService + self.wishlistRepository = wishlistRepository + self.mobileTiersRepository = mobileTiersRepository } func fetchCached() -> Promise { @@ -110,6 +122,20 @@ final class CourseInfoProvider: CourseInfoProviderProtocol { self.promoCodesNetworkService.checkPromoCode(courseID: self.courseID, name: name) } + func calculateMobileTier(promoCodeName: String?) -> Promise { + self.mobileTiersRepository.fetch(courseID: self.courseID, promoCodeName: promoCodeName, dataSourceType: .remote) + } + + func addCourseToWishlist() -> Promise { + self.wishlistRepository.addCourseToWishlist(courseID: self.courseID) + } + + func deleteCourseFromWishlist() -> Promise { + self.wishlistRepository.deleteCourseFromWishlist(courseID: self.courseID, sourceType: .remote) + } + + // MARK: Private API + private func fetchAndMergeCourse( courseFetchMethod: @escaping (Course.IdType) -> Promise, progressFetchMethod: @escaping (Progress.IdType) -> Promise, @@ -156,9 +182,10 @@ final class CourseInfoProvider: CourseInfoProviderProtocol { course.reviewSummary = reviewSummary course.purchases = coursePurchases - CoreDataHelper.shared.save() - - seal.fulfill(course) + self.fetchMobileTiers(course: course).done { course in + CoreDataHelper.shared.save() + seal.fulfill(course) + } }.catch { error in seal.reject(error) } @@ -171,6 +198,15 @@ final class CourseInfoProvider: CourseInfoProviderProtocol { } } + private func fetchMobileTiers(course: Course) -> Guarantee { + firstly { () -> Guarantee<[MobileTier]> in + course.isPaid && !course.enrolled ? self.mobileTiersRepository.fetch(courseID: course.id) : .value([]) + }.then { mobileTiers -> Guarantee in + course.mobileTiers = mobileTiers + return .value(course) + } + } + enum Error: Swift.Error { case persistenceFetchFailed case networkFetchFailed diff --git a/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift b/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift index 83cc08aa57..032b92441c 100644 --- a/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift +++ b/Stepic/Sources/Modules/CourseInfo/CourseInfoViewController.swift @@ -18,6 +18,7 @@ protocol CourseInfoViewControllerProtocol: AnyObject { func displayCourseRevenue(viewModel: CourseInfo.CourseRevenuePresentation.ViewModel) func displayAuthorization(viewModel: CourseInfo.AuthorizationPresentation.ViewModel) func displayPaidCourseBuying(viewModel: CourseInfo.PaidCourseBuyingPresentation.ViewModel) + func displayPaidCoursePurchaseModal(viewModel: CourseInfo.PaidCoursePurchaseModalPresentation.ViewModel) func displayIAPNotAllowed(viewModel: CourseInfo.IAPNotAllowedPresentation.ViewModel) func displayIAPReceiptValidationFailed(viewModel: CourseInfo.IAPReceiptValidationFailedPresentation.ViewModel) func displayIAPPaymentFailed(viewModel: CourseInfo.IAPPaymentFailedPresentation.ViewModel) @@ -538,6 +539,7 @@ extension CourseInfoViewController: CourseInfoViewControllerProtocol { func displayLesson(viewModel: CourseInfo.LessonPresentation.ViewModel) { let assembly = LessonAssembly( initialContext: .unit(id: viewModel.unitID), + promoCodeName: viewModel.promoCodeName, moduleOutput: self.interactor as? LessonOutputProtocol ) self.push(module: assembly.makeModule()) @@ -656,6 +658,7 @@ extension CourseInfoViewController: CourseInfoViewControllerProtocol { func displayPreviewLesson(viewModel: CourseInfo.PreviewLessonPresentation.ViewModel) { let assembly = LessonAssembly( initialContext: .lesson(id: viewModel.previewLessonID), + promoCodeName: viewModel.promoCodeName, moduleOutput: self.interactor as? LessonOutputProtocol ) self.push(module: assembly.makeModule()) @@ -692,6 +695,16 @@ extension CourseInfoViewController: CourseInfoViewControllerProtocol { ) } + func displayPaidCoursePurchaseModal(viewModel: CourseInfo.PaidCoursePurchaseModalPresentation.ViewModel) { + let assembly = CourseInfoPurchaseModalAssembly( + courseID: viewModel.courseID, + promoCodeName: viewModel.promoCodeName, + mobileTierID: viewModel.mobileTierID, + output: nil + ) + self.presentIfPanModalWithCustomModalPresentationStyle(assembly.makeModule()) + } + func displayIAPNotAllowed(viewModel: CourseInfo.IAPNotAllowedPresentation.ViewModel) { let alert = UIAlertController(title: viewModel.title, message: viewModel.message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel, handler: nil)) diff --git a/Stepic/Sources/Modules/CourseInfo/Views/CourseInfoHeaderView.swift b/Stepic/Sources/Modules/CourseInfo/Views/CourseInfoHeaderView.swift index e7e53feca0..8ef1795ce9 100644 --- a/Stepic/Sources/Modules/CourseInfo/Views/CourseInfoHeaderView.swift +++ b/Stepic/Sources/Modules/CourseInfo/Views/CourseInfoHeaderView.swift @@ -220,7 +220,9 @@ final class CourseInfoHeaderView: UIView { fullPriceString: viewModel.buttonDescription.subtitle ?? "" ) self.promoPriceButton.isEnabled = viewModel.buttonDescription.isEnabled - self.promoPriceButton.isHidden = !shouldShowPromoPriceButton + self.promoPriceButton.isHidden = false + } else { + self.promoPriceButton.isHidden = true } self.tryForFreeButton.isHidden = !viewModel.isTryForFreeAvailable diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalAssembly.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalAssembly.swift index 4cee4f6a75..83caf11698 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalAssembly.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalAssembly.swift @@ -4,11 +4,20 @@ final class CourseInfoPurchaseModalAssembly: Assembly { var moduleInput: CourseInfoPurchaseModalInputProtocol? private let courseID: Course.IdType + private let promoCodeName: String? + private let mobileTierID: MobileTier.IdType? private weak var moduleOutput: CourseInfoPurchaseModalOutputProtocol? - init(courseID: Course.IdType, output: CourseInfoPurchaseModalOutputProtocol? = nil) { + init( + courseID: Course.IdType, + promoCodeName: String?, + mobileTierID: MobileTier.IdType?, + output: CourseInfoPurchaseModalOutputProtocol? = nil + ) { self.courseID = courseID + self.promoCodeName = promoCodeName + self.mobileTierID = mobileTierID self.moduleOutput = output } @@ -20,6 +29,8 @@ final class CourseInfoPurchaseModalAssembly: Assembly { let presenter = CourseInfoPurchaseModalPresenter() let interactor = CourseInfoPurchaseModalInteractor( courseID: self.courseID, + initialPromoCodeName: self.promoCodeName, + mobileTierID: self.mobileTierID, presenter: presenter, provider: provider ) diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalInteractor.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalInteractor.swift index da8fa49a45..c4557e2a2d 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalInteractor.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalInteractor.swift @@ -12,13 +12,19 @@ final class CourseInfoPurchaseModalInteractor: CourseInfoPurchaseModalInteractor private let provider: CourseInfoPurchaseModalProviderProtocol private let courseID: Course.IdType + private let initialPromoCodeName: String? + private let mobileTierID: MobileTier.IdType? init( courseID: Course.IdType, + initialPromoCodeName: String?, + mobileTierID: MobileTier.IdType?, presenter: CourseInfoPurchaseModalPresenterProtocol, provider: CourseInfoPurchaseModalProviderProtocol ) { self.courseID = courseID + self.initialPromoCodeName = initialPromoCodeName + self.mobileTierID = mobileTierID self.presenter = presenter self.provider = provider } diff --git a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalViewController.swift b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalViewController.swift index a1c49e615e..c3f3d41232 100644 --- a/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalViewController.swift +++ b/Stepic/Sources/Modules/CourseInfoSubmodules/CourseInfoPurchaseModal/CourseInfoPurchaseModalViewController.swift @@ -38,7 +38,7 @@ final class CourseInfoPurchaseModalViewController: PanModalPresentableViewContro override var longFormHeight: PanModalHeight { guard self.hasLoadedData else { - return self.shortFormHeight + return super.longFormHeight } if self.keyboardIsShowing, diff --git a/Stepic/Sources/Modules/CourseList/CourseListAssembly.swift b/Stepic/Sources/Modules/CourseList/CourseListAssembly.swift index 1642b7ce2d..001ffad0a9 100644 --- a/Stepic/Sources/Modules/CourseList/CourseListAssembly.swift +++ b/Stepic/Sources/Modules/CourseList/CourseListAssembly.swift @@ -57,7 +57,10 @@ class CourseListAssembly: Assembly { courseReviewSummariesAPI: CourseReviewSummariesAPI() ), courseListsPersistenceService: CourseListsPersistenceService(), - iapService: IAPService.shared + wishlistEntriesPersistenceService: WishlistEntriesPersistenceService(), + mobileTiersRepository: MobileTiersRepository.default, + iapService: IAPService.shared, + remoteConfig: RemoteConfig.shared ) let dataBackUpdateService = DataBackUpdateService( @@ -78,7 +81,6 @@ class CourseListAssembly: Assembly { courseSubscriber: CourseSubscriber(), userAccountService: UserAccountService(), personalDeadlinesService: PersonalDeadlinesService(), - wishlistService: WishlistService.default, courseListDataBackUpdateService: courseListDataBackUpdateService, analytics: StepikAnalytics.shared, courseViewSource: self.courseViewSource, diff --git a/Stepic/Sources/Modules/CourseList/CourseListDataFlow.swift b/Stepic/Sources/Modules/CourseList/CourseListDataFlow.swift index 511e77c26d..fff585d6a4 100644 --- a/Stepic/Sources/Modules/CourseList/CourseListDataFlow.swift +++ b/Stepic/Sources/Modules/CourseList/CourseListDataFlow.swift @@ -13,7 +13,6 @@ enum CourseList { struct AvailableCourses { var fetchedCourses: ListData<(UniqueIdentifierType, Course)> var availableAdaptiveCourses: Set - var wishlistCoursesIDs: Set } // Use it for module initializing @@ -39,6 +38,7 @@ enum CourseList { struct Response { let isAuthorized: Bool let isCoursePricesEnabled: Bool + let coursePurchaseFlow: CoursePurchaseFlowType let result: AvailableCourses let viewSource: AnalyticsEvent.CourseViewSource } @@ -55,6 +55,7 @@ enum CourseList { struct Response { let isAuthorized: Bool let isCoursePricesEnabled: Bool + let coursePurchaseFlow: CoursePurchaseFlowType let result: StepikResult let viewSource: AnalyticsEvent.CourseViewSource } diff --git a/Stepic/Sources/Modules/CourseList/CourseListPresenter.swift b/Stepic/Sources/Modules/CourseList/CourseListPresenter.swift index f51f759fe3..d8989aa854 100644 --- a/Stepic/Sources/Modules/CourseList/CourseListPresenter.swift +++ b/Stepic/Sources/Modules/CourseList/CourseListPresenter.swift @@ -15,9 +15,9 @@ final class CourseListPresenter: CourseListPresenterProtocol { let courses = self.makeWidgetViewModels( courses: response.result.fetchedCourses.courses, availableInAdaptive: response.result.availableAdaptiveCourses, - wishlistCoursesIDs: response.result.wishlistCoursesIDs, isAuthorized: response.isAuthorized, isCoursePricesEnabled: response.isCoursePricesEnabled, + coursePurchaseFlow: response.coursePurchaseFlow, viewSource: response.viewSource ) @@ -38,9 +38,9 @@ final class CourseListPresenter: CourseListPresenterProtocol { let courses = self.makeWidgetViewModels( courses: data.fetchedCourses.courses, availableInAdaptive: data.availableAdaptiveCourses, - wishlistCoursesIDs: data.wishlistCoursesIDs, isAuthorized: response.isAuthorized, isCoursePricesEnabled: response.isCoursePricesEnabled, + coursePurchaseFlow: response.coursePurchaseFlow, viewSource: response.viewSource ) let listData = CourseList.ListData( @@ -59,9 +59,9 @@ final class CourseListPresenter: CourseListPresenterProtocol { private func makeWidgetViewModels( courses: [(UniqueIdentifierType, Course)], availableInAdaptive: Set, - wishlistCoursesIDs: Set, isAuthorized: Bool, isCoursePricesEnabled: Bool, + coursePurchaseFlow: CoursePurchaseFlowType, viewSource: AnalyticsEvent.CourseViewSource ) -> [CourseWidgetViewModel] { var viewModels: [CourseWidgetViewModel] = [] @@ -71,9 +71,9 @@ final class CourseListPresenter: CourseListPresenterProtocol { uniqueIdentifier: uid, course: course, isAdaptive: isAdaptive, - isWishlisted: wishlistCoursesIDs.contains(course.id), isAuthorized: isAuthorized, isCoursePricesEnabled: isCoursePricesEnabled, + coursePurchaseFlow: coursePurchaseFlow, viewSource: viewSource ) @@ -96,9 +96,9 @@ final class CourseListPresenter: CourseListPresenterProtocol { uniqueIdentifier: UniqueIdentifierType, course: Course, isAdaptive: Bool, - isWishlisted: Bool, isAuthorized: Bool, isCoursePricesEnabled: Bool, + coursePurchaseFlow: CoursePurchaseFlowType, viewSource: AnalyticsEvent.CourseViewSource ) -> CourseWidgetViewModel { let isEnrolled = isAuthorized && course.enrolled @@ -136,18 +136,31 @@ final class CourseListPresenter: CourseListPresenterProtocol { if isCoursePricesEnabled { let priceString: String? = { if course.isPaid { - return course.displayPriceIAP ?? course.displayPrice + switch coursePurchaseFlow { + case .web: + return course.displayPriceIAP ?? course.displayPrice + case .iap: + return course.displayPriceTierPrice ?? course.displayPrice + } } return NSLocalizedString("CourseWidgetPriceFree", comment: "") }() let discountPriceString: String? = { - guard course.isPaid, - let defaultPromoCode = course.defaultPromoCode, - defaultPromoCode.isValid && course.priceTier == nil else { + guard course.isPaid else { return nil } - return FormatterHelper.price(defaultPromoCode.price, currencyCode: defaultPromoCode.currencyCode) + switch coursePurchaseFlow { + case .web: + guard let defaultPromoCode = course.defaultPromoCode, + defaultPromoCode.isValid && course.priceTier == nil else { + return nil + } + + return FormatterHelper.price(defaultPromoCode.price, currencyCode: defaultPromoCode.currencyCode) + case .iap: + return course.displayPriceTierPromo + } }() priceViewModel = CourseWidgetPriceViewModel( @@ -167,7 +180,7 @@ final class CourseListPresenter: CourseListPresenterProtocol { certificateLabelText: certificateLabelText, isAdaptive: isAdaptive, isEnrolled: isEnrolled, - isWishlisted: isWishlisted, + isWishlisted: course.isInWishlist, isWishlistAvailable: isAuthorized && !course.enrolled, progress: progressViewModel, userCourse: userCourseViewModel, diff --git a/Stepic/Sources/Modules/CourseList/Interactor/CourseListInteractor.swift b/Stepic/Sources/Modules/CourseList/Interactor/CourseListInteractor.swift index 1dc7f56d5d..384e26f50e 100644 --- a/Stepic/Sources/Modules/CourseList/Interactor/CourseListInteractor.swift +++ b/Stepic/Sources/Modules/CourseList/Interactor/CourseListInteractor.swift @@ -21,7 +21,6 @@ final class CourseListInteractor: CourseListInteractorProtocol { private let courseSubscriber: CourseSubscriberProtocol private let userAccountService: UserAccountServiceProtocol private let personalDeadlinesService: PersonalDeadlinesServiceProtocol - private let wishlistService: WishlistServiceProtocol private let courseListDataBackUpdateService: CourseListDataBackUpdateServiceProtocol private let analytics: Analytics private let courseViewSource: AnalyticsEvent.CourseViewSource @@ -52,7 +51,6 @@ final class CourseListInteractor: CourseListInteractorProtocol { courseSubscriber: CourseSubscriberProtocol, userAccountService: UserAccountServiceProtocol, personalDeadlinesService: PersonalDeadlinesServiceProtocol, - wishlistService: WishlistServiceProtocol, courseListDataBackUpdateService: CourseListDataBackUpdateServiceProtocol, analytics: Analytics, courseViewSource: AnalyticsEvent.CourseViewSource, @@ -64,7 +62,6 @@ final class CourseListInteractor: CourseListInteractorProtocol { self.courseSubscriber = courseSubscriber self.userAccountService = userAccountService self.personalDeadlinesService = personalDeadlinesService - self.wishlistService = wishlistService self.analytics = analytics self.courseViewSource = courseViewSource self.remoteConfig = remoteConfig @@ -92,6 +89,8 @@ final class CourseListInteractor: CourseListInteractorProtocol { strongSelf.didLoadFromCache ? strongSelf.provider.fetchRemote(page: 1, filterQuery: strongSelf.currentFilterQuery) : strongSelf.provider.fetchCached() + }.then { courses, meta -> Guarantee<([Course], Meta)> in + strongSelf.fetchAndUpdateCurrentWishlistCoursesIDs().map { (courses, meta) } }.done { courses, meta in strongSelf.paginationState = PaginationState( page: meta.page, @@ -99,7 +98,6 @@ final class CourseListInteractor: CourseListInteractorProtocol { ) strongSelf.currentCourses = courses.map { (strongSelf.getUniqueIdentifierForCourse($0), $0) } - strongSelf.currentWishlistCoursesIDs = Set(strongSelf.wishlistService.getWishlist()) // Cache new courses fetched from remote. if strongSelf.didLoadFromCache && strongSelf.currentFilters.isEmpty { @@ -124,13 +122,13 @@ final class CourseListInteractor: CourseListInteractorProtocol { courses: strongSelf.currentCourses, hasNextPage: meta.hasNext ), - availableAdaptiveCourses: strongSelf.getAvailableAdaptiveCourses(from: courses), - wishlistCoursesIDs: strongSelf.currentWishlistCoursesIDs + availableAdaptiveCourses: strongSelf.getAvailableAdaptiveCourses(from: courses) ) let response = CourseList.CoursesLoad.Response( isAuthorized: strongSelf.userAccountService.isAuthorized, isCoursePricesEnabled: strongSelf.remoteConfig.isCoursePricesEnabled, + coursePurchaseFlow: strongSelf.remoteConfig.coursePurchaseFlow, result: courses, viewSource: strongSelf.courseViewSource ) @@ -186,6 +184,7 @@ final class CourseListInteractor: CourseListInteractorProtocol { let response = CourseList.NextCoursesLoad.Response( isAuthorized: strongSelf.userAccountService.isAuthorized, isCoursePricesEnabled: strongSelf.remoteConfig.isCoursePricesEnabled, + coursePurchaseFlow: strongSelf.remoteConfig.coursePurchaseFlow, result: .failure(error), viewSource: strongSelf.courseViewSource ) @@ -286,15 +285,14 @@ final class CourseListInteractor: CourseListInteractorProtocol { // - have no more courses // then ignore request and pass empty list to presenter if !self.isOnline || !self.paginationState.hasNext { - self.currentWishlistCoursesIDs = Set(self.wishlistService.getWishlist()) let result = CourseList.AvailableCourses( fetchedCourses: CourseList.ListData(courses: [], hasNextPage: false), - availableAdaptiveCourses: Set(), - wishlistCoursesIDs: self.currentWishlistCoursesIDs + availableAdaptiveCourses: Set() ) let response = CourseList.NextCoursesLoad.Response( isAuthorized: self.userAccountService.isAuthorized, isCoursePricesEnabled: self.remoteConfig.isCoursePricesEnabled, + coursePurchaseFlow: self.remoteConfig.coursePurchaseFlow, result: .success(result), viewSource: self.courseViewSource ) @@ -307,7 +305,9 @@ final class CourseListInteractor: CourseListInteractorProtocol { self.provider.fetchRemote( page: nextPageNumber, filterQuery: self.currentFilterQuery - ).done { courses, meta in + ).then { courses, meta -> Guarantee<([Course], Meta)> in + self.fetchAndUpdateCurrentWishlistCoursesIDs().map { (courses, meta) } + }.done { courses, meta in self.paginationState = PaginationState( page: meta.page, hasNext: meta.hasNext @@ -315,19 +315,18 @@ final class CourseListInteractor: CourseListInteractorProtocol { let appendedCourses = courses.map { (self.getUniqueIdentifierForCourse($0), $0) } self.currentCourses.append(contentsOf: appendedCourses) - self.currentWishlistCoursesIDs = Set(self.wishlistService.getWishlist()) let courses = CourseList.AvailableCourses( fetchedCourses: CourseList.ListData( courses: appendedCourses, hasNextPage: meta.hasNext ), - availableAdaptiveCourses: self.getAvailableAdaptiveCourses(from: courses), - wishlistCoursesIDs: self.currentWishlistCoursesIDs + availableAdaptiveCourses: self.getAvailableAdaptiveCourses(from: courses) ) let response = CourseList.NextCoursesLoad.Response( isAuthorized: self.userAccountService.isAuthorized, isCoursePricesEnabled: self.remoteConfig.isCoursePricesEnabled, + coursePurchaseFlow: self.remoteConfig.coursePurchaseFlow, result: .success(courses), viewSource: self.courseViewSource ) @@ -383,14 +382,12 @@ final class CourseListInteractor: CourseListInteractorProtocol { courses: self.currentCourses, hasNextPage: self.paginationState.hasNext ), - availableAdaptiveCourses: self.getAvailableAdaptiveCourses( - from: self.currentCourses.map { $0.1 } - ), - wishlistCoursesIDs: self.currentWishlistCoursesIDs + availableAdaptiveCourses: self.getAvailableAdaptiveCourses(from: self.currentCourses.map { $0.1 }) ) let response = CourseList.CoursesLoad.Response( isAuthorized: self.userAccountService.isAuthorized, isCoursePricesEnabled: self.remoteConfig.isCoursePricesEnabled, + coursePurchaseFlow: self.remoteConfig.coursePurchaseFlow, result: courses, viewSource: self.courseViewSource ) @@ -413,6 +410,12 @@ final class CourseListInteractor: CourseListInteractorProtocol { } } + private func fetchAndUpdateCurrentWishlistCoursesIDs() -> Guarantee { + self.provider.fetchWishlist().done { wishlistCoursesIDs in + self.currentWishlistCoursesIDs = Set(wishlistCoursesIDs) + } + } + // MARK: - Enums enum Error: Swift.Error { diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift index 26c96080d2..43047efb6b 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListNetworkService.swift @@ -392,18 +392,18 @@ class BaseCacheCoursesIDsSourceCourseListNetworkService: BaseCourseListNetworkSe } final class WishlistCourseListNetworkService: BaseCacheCoursesIDsSourceCourseListNetworkService { - private let wishlistStorageManager: WishlistStorageManagerProtocol + private let wishlistEntriesPersistenceService: WishlistEntriesPersistenceServiceProtocol init( coursesAPI: CoursesAPI, - wishlistStorageManager: WishlistStorageManagerProtocol + wishlistEntriesPersistenceService: WishlistEntriesPersistenceServiceProtocol ) { - self.wishlistStorageManager = wishlistStorageManager + self.wishlistEntriesPersistenceService = wishlistEntriesPersistenceService super.init(coursesAPI: coursesAPI) } override func getCoursesIDs() -> Promise<[Course.IdType]> { - .value(self.wishlistStorageManager.coursesIDs) + self.wishlistEntriesPersistenceService.fetchAll().mapValues(\.courseID) } } diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceService.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceService.swift index 3747ec76d5..e8555cabba 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceService.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceService.swift @@ -7,18 +7,29 @@ protocol CourseListPersistenceServiceProtocol: AnyObject { } class CourseListPersistenceService: CourseListPersistenceServiceProtocol { - let storage: CourseListPersistenceStorage + fileprivate let storage: CourseListPersistenceStorage + fileprivate let coursesPersistenceService: CoursesPersistenceServiceProtocol - init(storage: CourseListPersistenceStorage) { + init( + storage: CourseListPersistenceStorage, + coursesPersistenceService: CoursesPersistenceServiceProtocol = CoursesPersistenceService() + ) { self.storage = storage + self.coursesPersistenceService = coursesPersistenceService } func fetch() -> Promise<[Course]> { let courseListIDs = self.storage.getCoursesList() + return self.fetchUniqueCourses(ids: courseListIDs) + } - return Promise { seal in - let courses = Course.fetch(courseListIDs) + func update(newCachedList: [Course]) { + let ids = newCachedList.map { $0.id } + self.storage.update(newCachedList: ids) + } + fileprivate func fetchUniqueCourses(ids: [Course.IdType]) -> Promise<[Course]> { + self.coursesPersistenceService.fetch(ids: ids).map { courses -> [Course] in var uniqueCourses = [Course]() for course in courses { if !uniqueCourses.contains(where: { $0.id == course.id }) { @@ -26,17 +37,12 @@ class CourseListPersistenceService: CourseListPersistenceServiceProtocol { } } - let result = uniqueCourses.reordered(order: courseListIDs, transform: { $0.id }) + let result = uniqueCourses.reordered(order: ids, transform: { $0.id }) - seal.fulfill(result) + return result } } - func update(newCachedList: [Course]) { - let ids = newCachedList.map { $0.id } - self.storage.update(newCachedList: ids) - } - enum Error: Swift.Error { case fetchFailed } @@ -101,3 +107,23 @@ final class DownloadedCourseListPersistenceService: CourseListPersistenceService override func update(newCachedList: [Course]) {} } + +// MARK: - WishlistCourseListPersistenceService: CourseListPersistenceService - + +final class WishlistCourseListPersistenceService: CourseListPersistenceService { + private let wishlistEntriesPersistenceService: WishlistEntriesPersistenceServiceProtocol + + init(wishlistEntriesPersistenceService: WishlistEntriesPersistenceServiceProtocol) { + self.wishlistEntriesPersistenceService = wishlistEntriesPersistenceService + super.init(storage: PassiveCourseListPersistenceStorage(cachedList: [])) + } + + override func fetch() -> Promise<[Course]> { + self.wishlistEntriesPersistenceService + .fetchAll() + .mapValues(\.courseID) + .then(self.fetchUniqueCourses(ids:)) + } + + override func update(newCachedList: [Course]) {} +} diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceStorage.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceStorage.swift index 76ad276a19..fde5e8a27a 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceStorage.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListPersistenceStorage.swift @@ -54,9 +54,3 @@ final class CreatedCoursesCourseListPersistenceStorage: CourseListPersistenceSto self.teacherEntity?.createdCoursesArray ?? [] } } - -extension WishlistStorageManager: CourseListPersistenceStorage { - func update(newCachedList: [Course.IdType]) {} - - func getCoursesList() -> [Course.IdType] { self.coursesIDs } -} diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListProvider.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListProvider.swift index 2e840ec075..1ee53b852a 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListProvider.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListProvider.swift @@ -4,8 +4,12 @@ import PromiseKit protocol CourseListProviderProtocol: AnyObject { func fetchCached() -> Promise<([Course], Meta)> func fetchRemote(page: Int, filterQuery: CourseListFilterQuery?) -> Promise<([Course], Meta)> + func cache(courses: [Course]) + func fetchCachedCourseList() -> Guarantee + + func fetchWishlist() -> Guarantee<[Course.IdType]> } extension CourseListProviderProtocol { @@ -22,9 +26,13 @@ final class CourseListProvider: CourseListProviderProtocol { private let progressesNetworkService: ProgressesNetworkServiceProtocol private let reviewSummariesNetworkService: CourseReviewSummariesNetworkServiceProtocol private let courseListsPersistenceService: CourseListsPersistenceServiceProtocol + private let wishlistEntriesPersistenceService: WishlistEntriesPersistenceServiceProtocol + private let mobileTiersRepository: MobileTiersRepositoryProtocol private let iapService: IAPServiceProtocol + private let remoteConfig: RemoteConfig + init( type: CourseListType, networkService: CourseListNetworkServiceProtocol, @@ -32,7 +40,10 @@ final class CourseListProvider: CourseListProviderProtocol { progressesNetworkService: ProgressesNetworkServiceProtocol, reviewSummariesNetworkService: CourseReviewSummariesNetworkServiceProtocol, courseListsPersistenceService: CourseListsPersistenceServiceProtocol, - iapService: IAPServiceProtocol + wishlistEntriesPersistenceService: WishlistEntriesPersistenceServiceProtocol, + mobileTiersRepository: MobileTiersRepositoryProtocol, + iapService: IAPServiceProtocol, + remoteConfig: RemoteConfig ) { self.type = type self.persistenceService = persistenceService @@ -40,7 +51,10 @@ final class CourseListProvider: CourseListProviderProtocol { self.progressesNetworkService = progressesNetworkService self.reviewSummariesNetworkService = reviewSummariesNetworkService self.courseListsPersistenceService = courseListsPersistenceService + self.wishlistEntriesPersistenceService = wishlistEntriesPersistenceService + self.mobileTiersRepository = mobileTiersRepository self.iapService = iapService + self.remoteConfig = remoteConfig } // MARK: - CourseListProviderProtocol @@ -67,27 +81,20 @@ final class CourseListProvider: CourseListProviderProtocol { self.networkService.fetch( page: page, filterQuery: filterQuery - ).then { (courses, meta) -> Promise<([Course], Meta, [Progress], [CourseReviewSummary], [String?])> in + ).then { (courses, meta) -> Promise<([Course], Meta, [Progress], [CourseReviewSummary])> in let progressesIDs = courses.compactMap { $0.progressId } let summariesIDs = courses.compactMap { $0.reviewSummaryId } - let iapPricePromises = courses.map { course -> Promise in - self.iapService.canBuyCourse(course) - ? Promise(self.iapService.getLocalizedPrice(for: course)) - : .value(nil) - } - return when( fulfilled: self.progressesNetworkService.fetch(ids: progressesIDs, page: 1), self.reviewSummariesNetworkService.fetch(ids: summariesIDs, page: 1), - when(fulfilled: iapPricePromises) - ).compactMap { (courses, meta, $0.0, $1.0, $2) } - }.then { (courses, meta, progresses, reviewSummaries, iapPrices) -> Guarantee<([Course], Meta)> in + self.fetchIAPLocalizedPrices(for: courses) + ).compactMap { (courses, meta, $0.0.0, $0.1.0) } + }.then { (courses, meta, progresses, reviewSummaries) -> Guarantee<([Course], Meta)> in self.mergeAsync( courses: courses, progresses: progresses, - reviewSummaries: reviewSummaries, - iapPrices: iapPrices + reviewSummaries: reviewSummaries ).map { ($0, meta) } }.done { courses, meta in seal.fulfill((courses, meta)) @@ -110,13 +117,85 @@ final class CourseListProvider: CourseListProviderProtocol { return self.courseListsPersistenceService.fetch(id: catalogBlockCourseList.courseListID) } + func fetchWishlist() -> Guarantee<[Course.IdType]> { + self.wishlistEntriesPersistenceService.fetchAll().mapValues(\.courseID) + } + // MARK: - Private API + private func fetchIAPLocalizedPrices(for courses: [Course]) -> Guarantee { + firstly { () -> Guarantee<[MobileTierPlainObject]?> in + switch self.remoteConfig.coursePurchaseFlow { + case .web: + return .value(nil) + case .iap: + return Guarantee( + self.mobileTiersRepository.fetch( + coursesIDsWithPromoCodesNames: courses.map { ($0.id, nil) }, + dataSourceType: .remote + ), + fallback: nil + ) + } + }.then { mobileTiersOrNil -> Guarantee in + let mobileTiers = mobileTiersOrNil ?? [] + let mobileTierByCourseID = Dictionary( + mobileTiers.map({ ($0.courseID, $0) }), + uniquingKeysWith: { first, _ in first } + ) + + return when( + guarantees: courses.map { + self.fetchIAPLocalizedPrice(for: $0, mobileTiersMap: mobileTierByCourseID) + } + ) + } + } + + private func fetchIAPLocalizedPrice( + for course: Course, + mobileTiersMap: [Course.IdType: MobileTierPlainObject] + ) -> Guarantee { + switch self.remoteConfig.coursePurchaseFlow { + case .web: + return Guarantee { seal in + if self.iapService.canBuyCourse(course) { + self.iapService.getLocalizedPrice(for: course).done { price in + course.displayPriceIAP = price + seal(()) + } + } else { + course.displayPriceIAP = nil + seal(()) + } + } + case .iap: + return Guarantee { seal in + if let mobileTierPlainObject = mobileTiersMap[course.id], + let mobileTierEntity = course.mobileTiers.first(where: { $0.id == mobileTierPlainObject.id }) { + self.iapService.getLocalizedPrices(for: mobileTierEntity).done { result in + mobileTierEntity.priceTierDisplayPrice = result.price + mobileTierEntity.promoTierDisplayPrice = result.promo + + course.displayPriceTierPrice = result.price + course.displayPriceTierPromo = result.promo + + seal(()) + } + } else { + course.displayPriceTierPrice = nil + course.displayPriceTierPromo = nil + + seal(()) + } + } + } + } + private func mergeAsync( courses: [Course], progresses: [Progress], - reviewSummaries: [CourseReviewSummary], - iapPrices: [String?] + reviewSummaries: [CourseReviewSummary] ) -> Guarantee<[Course]> { Guarantee { seal in let progressesMap: [Progress.IdType: Progress] = progresses @@ -131,7 +210,6 @@ final class CourseListProvider: CourseListProviderProtocol { if let reviewSummaryID = courses[i].reviewSummaryId { courses[i].reviewSummary = reviewSummariesMap[reviewSummaryID] } - courses[i].displayPriceIAP = iapPrices[i] } CoreDataHelper.shared.save() diff --git a/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift b/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift index dbbfe85b05..b8a5455bdf 100644 --- a/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift +++ b/Stepic/Sources/Modules/CourseList/Provider/CourseListTypes.swift @@ -179,7 +179,9 @@ final class CourseListServicesFactory { ) ) } else if self.type is WishlistCourseListType { - return CourseListPersistenceService(storage: WishlistStorageManager()) + return WishlistCourseListPersistenceService( + wishlistEntriesPersistenceService: WishlistEntriesPersistenceService() + ) } else { fatalError("Unsupported course list type") } @@ -233,7 +235,7 @@ final class CourseListServicesFactory { } else if type is WishlistCourseListType { return WishlistCourseListNetworkService( coursesAPI: self.coursesAPI, - wishlistStorageManager: WishlistStorageManager() + wishlistEntriesPersistenceService: WishlistEntriesPersistenceService() ) } else if type is DownloadedCourseListType { return DownloadedCourseListNetworkService( diff --git a/Stepic/Sources/Modules/HomeSubmodules/WishlistWidget/WishlistWidgetAssembly.swift b/Stepic/Sources/Modules/HomeSubmodules/WishlistWidget/WishlistWidgetAssembly.swift index 744f52f20c..b48a168c2d 100644 --- a/Stepic/Sources/Modules/HomeSubmodules/WishlistWidget/WishlistWidgetAssembly.swift +++ b/Stepic/Sources/Modules/HomeSubmodules/WishlistWidget/WishlistWidgetAssembly.swift @@ -5,7 +5,7 @@ final class WishlistWidgetAssembly: Assembly { func makeModule() -> UIViewController { let provider = WishlistWidgetProvider( - wishlistService: WishlistService.default, + wishlistRepository: WishlistRepository.default, userAccountService: UserAccountService() ) let presenter = WishlistWidgetPresenter() diff --git a/Stepic/Sources/Modules/HomeSubmodules/WishlistWidget/WishlistWidgetProvider.swift b/Stepic/Sources/Modules/HomeSubmodules/WishlistWidget/WishlistWidgetProvider.swift index d4864eab12..a1008ed6df 100644 --- a/Stepic/Sources/Modules/HomeSubmodules/WishlistWidget/WishlistWidgetProvider.swift +++ b/Stepic/Sources/Modules/HomeSubmodules/WishlistWidget/WishlistWidgetProvider.swift @@ -6,19 +6,19 @@ protocol WishlistWidgetProviderProtocol { } final class WishlistWidgetProvider: WishlistWidgetProviderProtocol { - private let wishlistService: WishlistServiceProtocol + private let wishlistRepository: WishlistRepositoryProtocol private let userAccountService: UserAccountServiceProtocol init( - wishlistService: WishlistServiceProtocol, + wishlistRepository: WishlistRepositoryProtocol, userAccountService: UserAccountServiceProtocol ) { - self.wishlistService = wishlistService + self.wishlistRepository = wishlistRepository self.userAccountService = userAccountService } func fetchWishlistCoursesIDs(from dataSourceType: DataSourceType) -> Promise<[Course.IdType]> { - guard let currentUserID = self.userAccountService.currentUserID else { + guard self.userAccountService.isAuthorized else { switch dataSourceType { case .cache: return Promise(error: Error.cacheFetchFailed) @@ -27,15 +27,15 @@ final class WishlistWidgetProvider: WishlistWidgetProviderProtocol { } } - switch dataSourceType { - case .cache: - return .value(self.wishlistService.getWishlist()) - case .remote: - return Promise { seal in - self.wishlistService.fetchWishlist(userID: currentUserID).done { - let coursesIDs = self.wishlistService.getWishlist() - seal.fulfill(coursesIDs) - }.catch { _ in + return Promise { seal in + self.wishlistRepository.fetchWishlistEntries(sourceType: dataSourceType).done { wishlistEntries in + let wishlistCoursesIDs = wishlistEntries.map(\.courseID) + seal.fulfill(wishlistCoursesIDs) + }.catch { _ in + switch dataSourceType { + case .cache: + seal.reject(Error.cacheFetchFailed) + case .remote: seal.reject(Error.remoteFetchFailed) } } diff --git a/Stepic/Sources/Modules/Lesson/LessonAssembly.swift b/Stepic/Sources/Modules/Lesson/LessonAssembly.swift index 323db3457a..0d9f853a43 100644 --- a/Stepic/Sources/Modules/Lesson/LessonAssembly.swift +++ b/Stepic/Sources/Modules/Lesson/LessonAssembly.swift @@ -4,15 +4,19 @@ final class LessonAssembly: Assembly { private var initialContext: LessonDataFlow.Context private var startStep: LessonDataFlow.StartStep? + private let promoCodeName: String? + private weak var moduleOutput: LessonOutputProtocol? init( initialContext: LessonDataFlow.Context, startStep: LessonDataFlow.StartStep? = nil, + promoCodeName: String? = nil, moduleOutput: LessonOutputProtocol? ) { self.initialContext = initialContext self.startStep = startStep + self.promoCodeName = promoCodeName self.moduleOutput = moduleOutput } @@ -54,6 +58,7 @@ final class LessonAssembly: Assembly { let interactor = LessonInteractor( initialContext: self.initialContext, startStep: self.startStep, + promoCodeName: self.promoCodeName, presenter: presenter, provider: provider, unitNavigationService: unitNavigationService, diff --git a/Stepic/Sources/Modules/Lesson/LessonDataFlow.swift b/Stepic/Sources/Modules/Lesson/LessonDataFlow.swift index 22d4f051f9..5d79266997 100644 --- a/Stepic/Sources/Modules/Lesson/LessonDataFlow.swift +++ b/Stepic/Sources/Modules/Lesson/LessonDataFlow.swift @@ -136,10 +136,12 @@ enum LessonDataFlow { enum UnitNavigationFinishedDemoAccessPresentation { struct Response { let section: Section + let promoCodeName: String? } struct ViewModel { let sectionID: Section.IdType + let promoCodeName: String? } } diff --git a/Stepic/Sources/Modules/Lesson/LessonInteractor.swift b/Stepic/Sources/Modules/Lesson/LessonInteractor.swift index 0a8c3bd86b..83efca7ec3 100644 --- a/Stepic/Sources/Modules/Lesson/LessonInteractor.swift +++ b/Stepic/Sources/Modules/Lesson/LessonInteractor.swift @@ -34,11 +34,14 @@ final class LessonInteractor: LessonInteractorProtocol { private var lastLoadState: (context: LessonDataFlow.Context, startStep: LessonDataFlow.StartStep?) + private let promoCodeName: String? + private var didLoadFromCache = false init( initialContext: LessonDataFlow.Context, startStep: LessonDataFlow.StartStep?, + promoCodeName: String?, presenter: LessonPresenterProtocol, provider: LessonProviderProtocol, unitNavigationService: UnitNavigationServiceProtocol, @@ -51,6 +54,7 @@ final class LessonInteractor: LessonInteractorProtocol { self.persistenceQueuesService = persistenceQueuesService self.dataBackUpdateService = dataBackUpdateService self.lastLoadState = (initialContext, startStep) + self.promoCodeName = promoCodeName } // MARK: Public API @@ -641,7 +645,7 @@ extension LessonInteractor: StepOutputProtocol { }.done { targetLesson in if !targetLesson.canLearnLesson { self.presenter.presentUnitNavigationFinishedDemoAccessState( - response: .init(section: currentSection) + response: .init(section: currentSection, promoCodeName: self.promoCodeName) ) seal(true) } else { diff --git a/Stepic/Sources/Modules/Lesson/LessonPresenter.swift b/Stepic/Sources/Modules/Lesson/LessonPresenter.swift index da2b071095..081380e143 100644 --- a/Stepic/Sources/Modules/Lesson/LessonPresenter.swift +++ b/Stepic/Sources/Modules/Lesson/LessonPresenter.swift @@ -233,7 +233,7 @@ final class LessonPresenter: LessonPresenterProtocol { response: LessonDataFlow.UnitNavigationFinishedDemoAccessPresentation.Response ) { self.viewController?.displayUnitNavigationFinishedDemoAccessState( - viewModel: .init(sectionID: response.section.id) + viewModel: .init(sectionID: response.section.id, promoCodeName: response.promoCodeName) ) } diff --git a/Stepic/Sources/Modules/Lesson/LessonViewController.swift b/Stepic/Sources/Modules/Lesson/LessonViewController.swift index 802632aad8..09d2018f7b 100644 --- a/Stepic/Sources/Modules/Lesson/LessonViewController.swift +++ b/Stepic/Sources/Modules/Lesson/LessonViewController.swift @@ -647,6 +647,7 @@ extension LessonViewController: LessonViewControllerProtocol { ) { let assembly = LessonFinishedDemoPanModalAssembly( sectionID: viewModel.sectionID, + promoCodeName: viewModel.promoCodeName, output: self ) let viewController = assembly.makeModule() diff --git a/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalAssembly.swift b/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalAssembly.swift index 23c8cc5564..f89ee5a97f 100644 --- a/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalAssembly.swift +++ b/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalAssembly.swift @@ -2,14 +2,17 @@ import UIKit final class LessonFinishedDemoPanModalAssembly: Assembly { private let sectionID: Section.IdType + private let promoCodeName: String? private weak var moduleOutput: LessonFinishedDemoPanModalOutputProtocol? init( sectionID: Section.IdType, + promoCodeName: String? = nil, output: LessonFinishedDemoPanModalOutputProtocol? = nil ) { self.sectionID = sectionID + self.promoCodeName = promoCodeName self.moduleOutput = output } @@ -18,13 +21,17 @@ final class LessonFinishedDemoPanModalAssembly: Assembly { sectionsPersistenceService: SectionsPersistenceService(), sectionsNetworkService: SectionsNetworkService(sectionsAPI: SectionsAPI()), coursesPersistenceService: CoursesPersistenceService(), - coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()) + coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()), + mobileTiersRepository: MobileTiersRepository.default ) let presenter = LessonFinishedDemoPanModalPresenter() let interactor = LessonFinishedDemoPanModalInteractor( presenter: presenter, provider: provider, - sectionID: self.sectionID + sectionID: self.sectionID, + promoCodeName: self.promoCodeName, + iapService: IAPService.shared, + remoteConfig: RemoteConfig.shared ) let viewController = LessonFinishedDemoPanModalViewController(interactor: interactor) diff --git a/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalDataFlow.swift b/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalDataFlow.swift index 79d64d8ff2..b35c415112 100644 --- a/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalDataFlow.swift +++ b/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalDataFlow.swift @@ -7,6 +7,8 @@ enum LessonFinishedDemoPanModal { struct Response { let course: Course let section: Section + let coursePurchaseFlow: CoursePurchaseFlowType + let mobileTier: MobileTier? } struct ViewModel { diff --git a/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalInteractor.swift b/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalInteractor.swift index 82df003e46..4dc5013249 100644 --- a/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalInteractor.swift +++ b/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalInteractor.swift @@ -9,19 +9,31 @@ protocol LessonFinishedDemoPanModalInteractorProtocol { final class LessonFinishedDemoPanModalInteractor: LessonFinishedDemoPanModalInteractorProtocol { weak var moduleOutput: LessonFinishedDemoPanModalOutputProtocol? + private let sectionID: Section.IdType + private let promoCodeName: String? + private let presenter: LessonFinishedDemoPanModalPresenterProtocol private let provider: LessonFinishedDemoPanModalProviderProtocol - private let sectionID: Section.IdType + private let iapService: IAPServiceProtocol + private let remoteConfig: RemoteConfig + + private var currentMobileTier: MobileTier? init( presenter: LessonFinishedDemoPanModalPresenterProtocol, provider: LessonFinishedDemoPanModalProviderProtocol, - sectionID: Section.IdType + sectionID: Section.IdType, + promoCodeName: String?, + iapService: IAPServiceProtocol, + remoteConfig: RemoteConfig ) { self.presenter = presenter self.provider = provider self.sectionID = sectionID + self.promoCodeName = promoCodeName + self.iapService = iapService + self.remoteConfig = remoteConfig } func doModalLoad(request: LessonFinishedDemoPanModal.ModalLoad.Request) { @@ -37,14 +49,24 @@ final class LessonFinishedDemoPanModalInteractor: LessonFinishedDemoPanModalInte .compactMap { $0 } .then { course -> Promise<(Section, Course)> in section.course = course - CoreDataHelper.shared.save() - return .value((section, course)) } } } + .then { section, course -> Guarantee<(Section, Course)> in + self.fetchDisplayPrice(course: course).map { (section, $0) } + } .done { section, course in - self.presenter.presentModal(response: .init(course: course, section: section)) + CoreDataHelper.shared.save() + + self.presenter.presentModal( + response: .init( + course: course, + section: section, + coursePurchaseFlow: self.remoteConfig.coursePurchaseFlow, + mobileTier: self.currentMobileTier + ) + ) } .catch { error in print("LessonFinishedDemoPanModalInteractor :: failed load data with error = \(error)") @@ -54,4 +76,41 @@ final class LessonFinishedDemoPanModalInteractor: LessonFinishedDemoPanModalInte func doModalMainAction(request: LessonFinishedDemoPanModal.MainModalAction.Request) { self.moduleOutput?.handleLessonFinishedDemoPanModalMainAction() } + + // MARK: Private API + + private func fetchDisplayPrice(course: Course) -> Guarantee { + switch self.remoteConfig.coursePurchaseFlow { + case .web: + if course.isPaid && self.iapService.canBuyCourse(course) && (course.displayPriceIAP?.isEmpty ?? true) { + return self.iapService.getLocalizedPrice(for: course).then { localizedPrice in + course.displayPriceIAP = localizedPrice + return .value(course) + } + } + case .iap: + return Guarantee { seal in + self.provider + .calculateMobileTier(courseID: course.id, promoCodeName: self.promoCodeName) + .compactMap { $0 } + .compactMap { mobileTier in + course.mobileTiers.first(where: { $0.id == mobileTier.id }) + } + .then { mobileTier -> Guarantee<(MobileTier, String?, String?)> in + self.iapService.getLocalizedPrices(for: mobileTier).map { (mobileTier, $0.price, $0.promo) } + } + .done { mobileTier, priceTierLocalizedPrice, promoTierLocalizedPrice in + mobileTier.priceTierDisplayPrice = priceTierLocalizedPrice + mobileTier.promoTierDisplayPrice = promoTierLocalizedPrice + self.currentMobileTier = mobileTier + + seal(course) + } + .catch { _ in + seal(course) + } + } + } + return .value(course) + } } diff --git a/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalPresenter.swift b/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalPresenter.swift index ba37f0d281..0248478a8e 100644 --- a/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalPresenter.swift +++ b/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalPresenter.swift @@ -8,14 +8,32 @@ final class LessonFinishedDemoPanModalPresenter: LessonFinishedDemoPanModalPrese weak var viewController: LessonFinishedDemoPanModalViewControllerProtocol? func presentModal(response: LessonFinishedDemoPanModal.ModalLoad.Response) { + let course = response.course + let mobileTier = response.mobileTier + let title = String( format: NSLocalizedString("LessonFinishedDemoPanModalTitle", comment: ""), arguments: [response.section.title] ) + let displayPrice: String? = { + switch response.coursePurchaseFlow { + case .web: + return course.displayPriceIAP ?? course.displayPrice + case .iap: + if let promoTierDisplayPrice = mobileTier?.promoTierDisplayPrice { + return promoTierDisplayPrice + } else if let priceTierDisplayPrice = mobileTier?.priceTierDisplayPrice { + return priceTierDisplayPrice + } else { + return course.displayPrice + } + } + }() + let actionButtonTitle = String( format: NSLocalizedString("WidgetButtonBuy", comment: ""), - arguments: [response.course.displayPrice ?? "N/A"] + arguments: [displayPrice ?? "N/A"] ) self.viewController?.displayModal( diff --git a/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalProvider.swift b/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalProvider.swift index 49fbd907c0..ab4d324f28 100644 --- a/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalProvider.swift +++ b/Stepic/Sources/Modules/LessonPanModals/LessonFinishedDemoPanModal/LessonFinishedDemoPanModalProvider.swift @@ -4,6 +4,8 @@ import PromiseKit protocol LessonFinishedDemoPanModalProviderProtocol { func fetchCourse(id: Course.IdType) -> Promise func fetchSection(id: Section.IdType) -> Promise + + func calculateMobileTier(courseID: Course.IdType, promoCodeName: String?) -> Promise } final class LessonFinishedDemoPanModalProvider: LessonFinishedDemoPanModalProviderProtocol { @@ -13,16 +15,20 @@ final class LessonFinishedDemoPanModalProvider: LessonFinishedDemoPanModalProvid private let coursesPersistenceService: CoursesPersistenceServiceProtocol private let coursesNetworkService: CoursesNetworkServiceProtocol + private let mobileTiersRepository: MobileTiersRepositoryProtocol + init( sectionsPersistenceService: SectionsPersistenceServiceProtocol, sectionsNetworkService: SectionsNetworkServiceProtocol, coursesPersistenceService: CoursesPersistenceServiceProtocol, - coursesNetworkService: CoursesNetworkServiceProtocol + coursesNetworkService: CoursesNetworkServiceProtocol, + mobileTiersRepository: MobileTiersRepositoryProtocol ) { self.sectionsPersistenceService = sectionsPersistenceService self.sectionsNetworkService = sectionsNetworkService self.coursesPersistenceService = coursesPersistenceService self.coursesNetworkService = coursesNetworkService + self.mobileTiersRepository = mobileTiersRepository } func fetchSection(id: Section.IdType) -> Promise { @@ -44,4 +50,8 @@ final class LessonFinishedDemoPanModalProvider: LessonFinishedDemoPanModalProvid } } } + + func calculateMobileTier(courseID: Course.IdType, promoCodeName: String?) -> Promise { + self.mobileTiersRepository.fetch(courseID: courseID, promoCodeName: promoCodeName, dataSourceType: .remote) + } } diff --git a/Stepic/Sources/Services/AppData/LogoutDataClearService.swift b/Stepic/Sources/Services/AppData/LogoutDataClearService.swift index 94de1f88a0..7440ef0875 100644 --- a/Stepic/Sources/Services/AppData/LogoutDataClearService.swift +++ b/Stepic/Sources/Services/AppData/LogoutDataClearService.swift @@ -45,6 +45,7 @@ final class LogoutDataClearService: LogoutDataClearServiceProtocol { private let userCoursesPersistenceService: UserCoursesPersistenceServiceProtocol private let videosPersistenceService: VideosPersistenceServiceProtocol private let videoURLsPersistenceService: VideoURLsPersistenceServiceProtocol + private let wishlistEntriesPersistenceService: WishlistEntriesPersistenceServiceProtocol // Notifications private let notificationsRegistrationService: NotificationsRegistrationServiceProtocol private let notificationsService: NotificationsService @@ -53,7 +54,6 @@ final class LogoutDataClearService: LogoutDataClearServiceProtocol { private let spotlightIndexingService: SpotlightIndexingServiceProtocol private let analyticsUserProperties: AnalyticsUserProperties private let deviceDefaults: DeviceDefaults - private let wishlistService: WishlistServiceProtocol private let synchronizationQueue = DispatchQueue( label: "com.AlexKarpov.Stepic.LogoutDataClearQueue", @@ -101,13 +101,13 @@ final class LogoutDataClearService: LogoutDataClearServiceProtocol { userCoursesPersistenceService: UserCoursesPersistenceServiceProtocol = UserCoursesPersistenceService(), videosPersistenceService: VideosPersistenceServiceProtocol = VideosPersistenceService(), videoURLsPersistenceService: VideoURLsPersistenceServiceProtocol = VideoURLsPersistenceService(), + wishlistEntriesPersistenceService: WishlistEntriesPersistenceServiceProtocol = WishlistEntriesPersistenceService(), notificationsRegistrationService: NotificationsRegistrationServiceProtocol = NotificationsRegistrationService(), notificationsService: NotificationsService = NotificationsService(), spotlightIndexingService: SpotlightIndexingServiceProtocol = SpotlightIndexingService.shared, analyticsUserProperties: AnalyticsUserProperties = .shared, notificationsBadgesManager: NotificationsBadgesManager = .shared, - deviceDefaults: DeviceDefaults = .sharedDefaults, - wishlistService: WishlistServiceProtocol = WishlistService.default + deviceDefaults: DeviceDefaults = .sharedDefaults ) { self.downloadsDeletionService = downloadsDeletionService self.assignmentsPersistenceService = assignmentsPersistenceService @@ -147,13 +147,13 @@ final class LogoutDataClearService: LogoutDataClearServiceProtocol { self.userCoursesPersistenceService = userCoursesPersistenceService self.videosPersistenceService = videosPersistenceService self.videoURLsPersistenceService = videoURLsPersistenceService + self.wishlistEntriesPersistenceService = wishlistEntriesPersistenceService self.notificationsRegistrationService = notificationsRegistrationService self.notificationsService = notificationsService self.spotlightIndexingService = spotlightIndexingService self.analyticsUserProperties = analyticsUserProperties self.notificationsBadgesManager = notificationsBadgesManager self.deviceDefaults = deviceDefaults - self.wishlistService = wishlistService } func clearCurrentUserData() -> Guarantee { @@ -188,7 +188,6 @@ final class LogoutDataClearService: LogoutDataClearServiceProtocol { self.notificationsBadgesManager.set(number: 0) self.deviceDefaults.deviceId = nil - self.wishlistService.removeAll() self.notificationsService.removeAllLocalNotifications() self.spotlightIndexingService.deleteAllSearchableItems() @@ -272,6 +271,8 @@ final class LogoutDataClearService: LogoutDataClearServiceProtocol { Guarantee(self.videosPersistenceService.deleteAll(), fallback: nil) }.then { _ -> Guarantee in Guarantee(self.videoURLsPersistenceService.deleteAll(), fallback: nil) + }.then { _ -> Guarantee in + Guarantee(self.wishlistEntriesPersistenceService.deleteAll(), fallback: nil) }.done { _ in CoreDataHelper.shared.save() seal(()) diff --git a/Stepic/Sources/Services/Models/Network/MobileTiersNetworkService.swift b/Stepic/Sources/Services/Models/Network/MobileTiersNetworkService.swift new file mode 100644 index 0000000000..4d78286ea6 --- /dev/null +++ b/Stepic/Sources/Services/Models/Network/MobileTiersNetworkService.swift @@ -0,0 +1,38 @@ +import Foundation +import PromiseKit + +protocol MobileTiersNetworkServiceProtocol: AnyObject { + func calculateMobileTiers( + coursesIDsWithPromoCodesNames: [(Course.IdType, String?)] + ) -> Promise<[MobileTierPlainObject]> +} + +extension MobileTiersNetworkServiceProtocol { + func calculateMobileTier(courseID: Course.IdType, promoCodeName: String? = nil) -> Promise { + self.calculateMobileTiers(coursesIDsWithPromoCodesNames: [(courseID, promoCodeName)]).map(\.first) + } + + func checkPromoCode(name: String, courseID: Course.IdType) -> Promise { + self.calculateMobileTier(courseID: courseID, promoCodeName: name) + } +} + +final class MobileTiersNetworkService: MobileTiersNetworkServiceProtocol { + private let mobileTiersAPI: MobileTiersAPI + + init(mobileTiersAPI: MobileTiersAPI) { + self.mobileTiersAPI = mobileTiersAPI + } + + func calculateMobileTiers( + coursesIDsWithPromoCodesNames: [(Course.IdType, String?)] + ) -> Promise<[MobileTierPlainObject]> { + let dict = Dictionary(coursesIDsWithPromoCodesNames, uniquingKeysWith: { first, _ in first }) + let request = MobileTierCalculateRequest( + params: dict.map { courseID, promoCodeName in + MobileTierCalculateRequest.Param(courseID: courseID, promoCodeName: promoCodeName) + } + ) + return self.mobileTiersAPI.calculate(request: request).map(\.mobileTiers) + } +} diff --git a/Stepic/Sources/Services/Models/Network/WishListsNetworkService.swift b/Stepic/Sources/Services/Models/Network/WishListsNetworkService.swift new file mode 100644 index 0000000000..115499165a --- /dev/null +++ b/Stepic/Sources/Services/Models/Network/WishListsNetworkService.swift @@ -0,0 +1,35 @@ +import Foundation +import PromiseKit + +protocol WishListsNetworkServiceProtocol: AnyObject { + func fetchWishlistEntry(courseID: Course.IdType) -> Promise + func fetchWishlistEntries() -> Promise<[WishlistEntryPlainObject]> + + func createWishlistEntry(courseID: Course.IdType) -> Promise + + func deleteWishlistEntry(wishlistEntryID: WishlistEntryPlainObject.IdType) -> Promise +} + +final class WishListsNetworkService: WishListsNetworkServiceProtocol { + private let wishListsAPI: WishListsAPI + + init(wishListsAPI: WishListsAPI) { + self.wishListsAPI = wishListsAPI + } + + func fetchWishlistEntry(courseID: Course.IdType) -> Promise { + self.wishListsAPI.retrieveWishlistEntry(courseID: courseID) + } + + func fetchWishlistEntries() -> Promise<[WishlistEntryPlainObject]> { + self.wishListsAPI.retrieveAllWishlistPages() + } + + func createWishlistEntry(courseID: Course.IdType) -> Promise { + self.wishListsAPI.createWishlistEntry(courseID: courseID) + } + + func deleteWishlistEntry(wishlistEntryID: WishlistEntryPlainObject.IdType) -> Promise { + self.wishListsAPI.deleteWishlistEntry(wishlistEntryID: wishlistEntryID) + } +} diff --git a/Stepic/Sources/Services/Models/Persistence/CoursesPersistenceService.swift b/Stepic/Sources/Services/Models/Persistence/CoursesPersistenceService.swift index 3d174a0a67..8306624b88 100644 --- a/Stepic/Sources/Services/Models/Persistence/CoursesPersistenceService.swift +++ b/Stepic/Sources/Services/Models/Persistence/CoursesPersistenceService.swift @@ -7,6 +7,7 @@ protocol CoursesPersistenceServiceProtocol: AnyObject { func fetchEnrolled() -> Guarantee<[Course]> func fetchAll() -> Guarantee<[Course]> func unenrollAll() -> Promise + func batchUpdateIsInWishlist(id: Course.IdType, isInWishList: Bool) -> Promise } extension CoursesPersistenceServiceProtocol { @@ -53,6 +54,28 @@ final class CoursesPersistenceService: BasePersistenceService, CoursesPe } } + func batchUpdateIsInWishlist(id: Course.IdType, isInWishList: Bool) -> Promise { + Promise { seal in + let batchUpdateRequest = NSBatchUpdateRequest(entityName: Course.entityName) + batchUpdateRequest.predicate = NSPredicate( + format: "%K == %@", + #keyPath(Course.managedId), + NSNumber(value: id) + ) + batchUpdateRequest.propertiesToUpdate = ["managedIsInWishlist": NSNumber(value: isInWishList)] + + self.managedObjectContext.perform { + do { + try self.managedObjectContext.executeAndMergeChanges(using: batchUpdateRequest) + try self.managedObjectContext.save() + seal.fulfill(()) + } catch { + seal.reject(Error.batchUpdateFailed) + } + } + } + } + enum Error: Swift.Error { case batchUpdateFailed } diff --git a/Stepic/Sources/Services/Models/Persistence/MobileTiersPersistenceService.swift b/Stepic/Sources/Services/Models/Persistence/MobileTiersPersistenceService.swift new file mode 100644 index 0000000000..6ccc60d28e --- /dev/null +++ b/Stepic/Sources/Services/Models/Persistence/MobileTiersPersistenceService.swift @@ -0,0 +1,104 @@ +import CoreData +import Foundation +import PromiseKit + +protocol MobileTiersPersistenceServiceProtocol: AnyObject { + func fetchAll() -> Guarantee<[MobileTier]> + func fetch(id: MobileTier.IdType) -> Guarantee + func fetch(ids: [MobileTier.IdType]) -> Guarantee<[MobileTier]> + func fetch(courseID: Course.IdType) -> Guarantee<[MobileTier]> + func fetch(coursesIDsWithPromoCodesNames: [(Course.IdType, String?)]) -> Guarantee<[MobileTier]> + + func save(mobileTiers: [MobileTierPlainObject]) -> Guarantee<[MobileTier]> + + func deleteAll() -> Promise +} + +extension MobileTiersPersistenceServiceProtocol { + func fetch(courseID: Course.IdType, promoCodeName: String?) -> Guarantee { + self.fetch(coursesIDsWithPromoCodesNames: [(courseID, promoCodeName)]).map(\.first) + } +} + +final class MobileTiersPersistenceService: BasePersistenceService, MobileTiersPersistenceServiceProtocol { + func fetch(id: MobileTier.IdType) -> Guarantee { + self.fetch(ids: [id]).map(\.first) + } + + func fetch(ids: [MobileTier.IdType]) -> Guarantee<[MobileTier]> { + Guarantee { seal in + let request = MobileTier.sortedFetchRequest + + let idPredicates = ids.map { NSPredicate(format: "%K LIKE[c] %@", #keyPath(MobileTier.managedId), $0) } + request.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: idPredicates) + request.returnsObjectsAsFaults = false + + do { + let mobileTiers = try self.managedObjectContext.fetch(request) + seal(mobileTiers) + } catch { + print("MobileTiersPersistenceService :: \(#function) failed fetch with error = \(error)") + seal([]) + } + } + } + + func fetch(coursesIDsWithPromoCodesNames: [(Course.IdType, String?)]) -> Guarantee<[MobileTier]> { + let ids = coursesIDsWithPromoCodesNames.map(self.makeMobileTierID(courseID:promoCodeName:)) + return self.fetch(ids: ids) + } + + func fetch(courseID: Course.IdType) -> Guarantee<[MobileTier]> { + Guarantee { seal in + let request = MobileTier.sortedFetchRequest + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(MobileTier.managedCourseId), + NSNumber(value: courseID) + ) + request.returnsObjectsAsFaults = false + + do { + let mobileTiers = try self.managedObjectContext.fetch(request) + seal(mobileTiers) + } catch { + print("MobileTiersPersistenceService :: \(#function) failed fetch with error = \(error)") + seal([]) + } + } + } + + func save(mobileTiers: [MobileTierPlainObject]) -> Guarantee<[MobileTier]> { + Guarantee { seal in + firstly { + self.fetch(ids: mobileTiers.map(\.id)) + }.map { cachedMobileTiers in + Dictionary(cachedMobileTiers.map({ ($0.id, $0) }), uniquingKeysWith: { first, _ in first }) + }.done { cachedMobileTiersMap in + self.managedObjectContext.performChanges { + var result = [MobileTier]() + + for mobileTier in mobileTiers { + if let cachedMobileTier = cachedMobileTiersMap[mobileTier.id] { + cachedMobileTier.update(mobileTier: mobileTier) + result.append(cachedMobileTier) + } else { + let insertedMobileTier = MobileTier.insert( + into: self.managedObjectContext, + mobileTier: mobileTier + ) + result.append(insertedMobileTier) + } + } + + seal(result) + } + } + } + } + + private func makeMobileTierID(courseID: Course.IdType, promoCodeName: String?) -> String { + let promoCodeID = promoCodeName != nil ? promoCodeName.require() : "None" + return "\(courseID)-\(PaymentStore.appStore.intValue)-\(promoCodeID)".trimmed() + } +} diff --git a/Stepic/Sources/Services/Models/Persistence/WishlistEntriesPersistenceService.swift b/Stepic/Sources/Services/Models/Persistence/WishlistEntriesPersistenceService.swift new file mode 100644 index 0000000000..100b3d19db --- /dev/null +++ b/Stepic/Sources/Services/Models/Persistence/WishlistEntriesPersistenceService.swift @@ -0,0 +1,104 @@ +import Foundation +import PromiseKit + +protocol WishlistEntriesPersistenceServiceProtocol: AnyObject { + func fetchAll() -> Guarantee<[WishlistEntryEntity]> + func fetch(courseID: Course.IdType) -> Guarantee + + func save(wishlistEntries: [WishlistEntryPlainObject]) -> Guarantee + func saveNewWishlistEntries(_ wishlistEntries: [WishlistEntryPlainObject]) -> Guarantee + + func deleteAll() -> Promise + func deleteWishlistEntry(courseID: Course.IdType) -> Guarantee +} + +final class WishlistEntriesPersistenceService: BasePersistenceService, + WishlistEntriesPersistenceServiceProtocol { + func fetch(courseID: Course.IdType) -> Guarantee { + self.fetch(courseID: courseID).map(\.first) + } + + func save(wishlistEntries: [WishlistEntryPlainObject]) -> Guarantee { + if wishlistEntries.isEmpty { + return .value(()) + } + + return Guarantee { seal in + self.fetch(ids: wishlistEntries.map(\.id)).done { cachedWishlistEntities in + let cachedWishlistEntitiesMap = Dictionary( + uniqueKeysWithValues: cachedWishlistEntities.map({ ($0.id, $0) }) + ) + + self.managedObjectContext.performChanges { + for wishlistEntryToSave in wishlistEntries { + if let cachedWishlistEntity = cachedWishlistEntitiesMap[wishlistEntryToSave.id] { + cachedWishlistEntity.update(wishlistEntry: wishlistEntryToSave) + } else { + _ = WishlistEntryEntity.insert( + into: self.managedObjectContext, + wishlistEntry: wishlistEntryToSave + ) + } + } + + seal(()) + } + } + } + } + + func saveNewWishlistEntries(_ wishlistEntries: [WishlistEntryPlainObject]) -> Guarantee { + Guarantee { seal in + firstly { () -> Guarantee in + Guarantee(self.deleteAll(), fallback: nil) + }.done { _ in + self.managedObjectContext.performChanges { + for wishlistEntryToSave in wishlistEntries { + _ = WishlistEntryEntity.insert( + into: self.managedObjectContext, + wishlistEntry: wishlistEntryToSave + ) + } + seal(()) + } + } + } + } + + func deleteWishlistEntry(courseID: Course.IdType) -> Guarantee { + Guarantee { seal in + firstly { () -> Guarantee<[WishlistEntryEntity]> in + self.fetch(courseID: courseID) + }.done { wishlistEntries in + self.managedObjectContext.performChanges { + for wishlistEntry in wishlistEntries { + self.managedObjectContext.delete(wishlistEntry) + } + seal(()) + } + } + } + } + + // MARK: Private API + + private func fetch(courseID: Course.IdType) -> Guarantee<[WishlistEntryEntity]> { + Guarantee { seal in + let request = WishlistEntryEntity.sortedFetchRequest + request.predicate = NSPredicate( + format: "%K == %@", + #keyPath(WishlistEntryEntity.managedCourseId), + NSNumber(value: courseID) + ) + request.returnsObjectsAsFaults = false + + do { + let wishlistEntries = try self.managedObjectContext.fetch(request) + seal(wishlistEntries) + } catch { + print("WishlistEntriesPersistenceService :: \(#function) failed fetch with error = \(error)") + seal([]) + } + } + } +} diff --git a/Stepic/Sources/Services/Models/Repository/MobileTiersRepository.swift b/Stepic/Sources/Services/Models/Repository/MobileTiersRepository.swift new file mode 100644 index 0000000000..d82dc652f8 --- /dev/null +++ b/Stepic/Sources/Services/Models/Repository/MobileTiersRepository.swift @@ -0,0 +1,127 @@ +import Foundation +import PromiseKit + +protocol MobileTiersRepositoryProtocol: AnyObject { + func fetch( + courseID: Course.IdType, + promoCodeName: String?, + dataSourceType: DataSourceType + ) -> Promise + + func fetch( + coursesIDsWithPromoCodesNames: [(Course.IdType, String?)], + dataSourceType: DataSourceType + ) -> Promise<[MobileTierPlainObject]> + + func fetch(courseID: Course.IdType) -> Guarantee<[MobileTier]> + + func checkPromoCode(name: String, courseID: Course.IdType) -> Promise +} + +final class MobileTiersRepository: MobileTiersRepositoryProtocol { + private let mobileTiersNetworkService: MobileTiersNetworkServiceProtocol + private let mobileTiersPersistenceService: MobileTiersPersistenceServiceProtocol + + private let coursesPersistenceService: CoursesPersistenceServiceProtocol + + init( + mobileTiersNetworkService: MobileTiersNetworkServiceProtocol, + mobileTiersPersistenceService: MobileTiersPersistenceServiceProtocol, + coursesPersistenceService: CoursesPersistenceServiceProtocol + ) { + self.mobileTiersNetworkService = mobileTiersNetworkService + self.mobileTiersPersistenceService = mobileTiersPersistenceService + self.coursesPersistenceService = coursesPersistenceService + } + + func fetch( + courseID: Course.IdType, + promoCodeName: String?, + dataSourceType: DataSourceType + ) -> Promise { + switch dataSourceType { + case .cache: + return self.mobileTiersPersistenceService + .fetch(courseID: courseID, promoCodeName: promoCodeName) + .map(\.?.plainObject) + case .remote: + return self.mobileTiersNetworkService + .calculateMobileTier(courseID: courseID, promoCodeName: promoCodeName) + .then(self.saveMobileTierIfNeeded(_:)) + } + } + + func fetch( + coursesIDsWithPromoCodesNames: [(Course.IdType, String?)], + dataSourceType: DataSourceType + ) -> Promise<[MobileTierPlainObject]> { + switch dataSourceType { + case .cache: + return self.mobileTiersPersistenceService + .fetch(coursesIDsWithPromoCodesNames: coursesIDsWithPromoCodesNames) + .mapValues(\.plainObject) + case .remote: + return self.mobileTiersNetworkService + .calculateMobileTiers(coursesIDsWithPromoCodesNames: coursesIDsWithPromoCodesNames) + .then { remoteMobileTiers in + self.mobileTiersPersistenceService + .save(mobileTiers: remoteMobileTiers) + .then(self.establishRelationships(mobileTiers:)) + .map { _ in remoteMobileTiers } + } + } + } + + func fetch(courseID: Course.IdType) -> Guarantee<[MobileTier]> { + self.mobileTiersPersistenceService.fetch(courseID: courseID) + } + + func checkPromoCode(name: String, courseID: Course.IdType) -> Promise { + self.mobileTiersNetworkService + .checkPromoCode(name: name, courseID: courseID) + .then(self.saveMobileTierIfNeeded(_:)) + } + + // MARK: Private API + + private func saveMobileTierIfNeeded( + _ mobileTierOrNil: MobileTierPlainObject? + ) -> Promise { + guard let mobileTier = mobileTierOrNil else { + return .value(mobileTierOrNil) + } + + return self.mobileTiersPersistenceService + .save(mobileTiers: [mobileTier]) + .then(self.establishRelationships(mobileTiers:)) + .map { _ in mobileTierOrNil } + } + + private func establishRelationships(mobileTiers: [MobileTier]) -> Promise<[MobileTier]> { + if mobileTiers.isEmpty { + return .value([]) + } + + let coursesIDs = Set(mobileTiers.map(\.courseID)) + + return self.coursesPersistenceService.fetch(ids: Array(coursesIDs)).then { courses -> Promise<[MobileTier]> in + let coursesMap = Dictionary(courses.map({ ($0.id, $0) }), uniquingKeysWith: { first, _ in first }) + + for mobileTier in mobileTiers { + mobileTier.course = coursesMap[mobileTier.courseID] + } + + return .value(mobileTiers) + } + } +} + +extension MobileTiersRepository { + static var `default`: MobileTiersRepository { + MobileTiersRepository( + mobileTiersNetworkService: MobileTiersNetworkService(mobileTiersAPI: MobileTiersAPI()), + mobileTiersPersistenceService: MobileTiersPersistenceService(), + coursesPersistenceService: CoursesPersistenceService() + ) + } +} diff --git a/Stepic/Sources/Services/Models/Repository/WishlistRepository.swift b/Stepic/Sources/Services/Models/Repository/WishlistRepository.swift new file mode 100644 index 0000000000..1f9a2e0f63 --- /dev/null +++ b/Stepic/Sources/Services/Models/Repository/WishlistRepository.swift @@ -0,0 +1,106 @@ +import Foundation +import PromiseKit + +protocol WishlistRepositoryProtocol: AnyObject { + func fetchWishlistEntries(sourceType: DataSourceType) -> Promise<[WishlistEntryPlainObject]> + + func addCourseToWishlist(courseID: Course.IdType) -> Promise + func deleteCourseFromWishlist(courseID: Course.IdType, sourceType: DataSourceType) -> Promise + + func deleteAllWishlistEntries() -> Promise +} + +final class WishlistRepository: WishlistRepositoryProtocol { + private let wishlistEntriesPersistenceService: WishlistEntriesPersistenceServiceProtocol + private let wishListsNetworkService: WishListsNetworkServiceProtocol + + private let coursesPersistenceService: CoursesPersistenceServiceProtocol + + private let dataBackUpdateService: DataBackUpdateServiceProtocol + + init( + wishlistEntriesPersistenceService: WishlistEntriesPersistenceServiceProtocol, + wishListsNetworkService: WishListsNetworkServiceProtocol, + coursesPersistenceService: CoursesPersistenceServiceProtocol, + dataBackUpdateService: DataBackUpdateServiceProtocol + ) { + self.wishlistEntriesPersistenceService = wishlistEntriesPersistenceService + self.wishListsNetworkService = wishListsNetworkService + self.coursesPersistenceService = coursesPersistenceService + self.dataBackUpdateService = dataBackUpdateService + } + + func fetchWishlistEntries(sourceType: DataSourceType) -> Promise<[WishlistEntryPlainObject]> { + switch sourceType { + case .cache: + return self.wishlistEntriesPersistenceService + .fetchAll() + .mapValues(\.plainObject) + case .remote: + return self.wishListsNetworkService + .fetchWishlistEntries() + .map { wishlistEntries in + wishlistEntries.sorted { lhs, rhs in + (lhs.createDate ?? Date()) > (rhs.createDate ?? Date()) + } + } + .then { remoteWishlistEntries in + self.wishlistEntriesPersistenceService + .saveNewWishlistEntries(remoteWishlistEntries) + .map { remoteWishlistEntries } + } + } + } + + func addCourseToWishlist(courseID: Course.IdType) -> Promise { + self.wishListsNetworkService + .createWishlistEntry(courseID: courseID) + .then { self.wishlistEntriesPersistenceService.save(wishlistEntries: [$0]) } + .then { self.coursesPersistenceService.batchUpdateIsInWishlist(id: courseID, isInWishList: true) } + .then { self.triggerWishlistUpdate().asVoid() } + } + + func deleteCourseFromWishlist(courseID: Course.IdType, sourceType: DataSourceType) -> Promise { + firstly { () -> Promise in + if sourceType == .remote { + return self.wishlistEntriesPersistenceService + .fetch(courseID: courseID) + .then { cachedWishlistEntry -> Promise in + if let cachedWishlistEntry = cachedWishlistEntry { + return .value(cachedWishlistEntry.plainObject) + } + return self.wishListsNetworkService.fetchWishlistEntry(courseID: courseID) + } + .compactMap { $0 } + .then { self.wishListsNetworkService.deleteWishlistEntry(wishlistEntryID: $0.id) } + } + return .value(()) + } + .then { self.wishlistEntriesPersistenceService.deleteWishlistEntry(courseID: courseID).asVoid() } + .then { self.coursesPersistenceService.batchUpdateIsInWishlist(id: courseID, isInWishList: false) } + .then { self.triggerWishlistUpdate().asVoid() } + } + + func deleteAllWishlistEntries() -> Promise { + self.wishlistEntriesPersistenceService.deleteAll() + } + + // MARK: Private API + + private func triggerWishlistUpdate() -> Guarantee { + self.wishlistEntriesPersistenceService.fetchAll().done { wishlistEntries in + self.dataBackUpdateService.triggerWishlistUpdate(coursesIDs: wishlistEntries.map(\.courseID)) + } + } +} + +extension WishlistRepository { + static var `default`: WishlistRepository { + WishlistRepository( + wishlistEntriesPersistenceService: WishlistEntriesPersistenceService(), + wishListsNetworkService: WishListsNetworkService(wishListsAPI: WishListsAPI()), + coursesPersistenceService: CoursesPersistenceService(), + dataBackUpdateService: DataBackUpdateService.default + ) + } +} diff --git a/Stepic/Sources/Services/StorageManagers/WishlistStorageManager.swift b/Stepic/Sources/Services/StorageManagers/WishlistStorageManager.swift deleted file mode 100644 index 782c5fda53..0000000000 --- a/Stepic/Sources/Services/StorageManagers/WishlistStorageManager.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -protocol WishlistStorageManagerProtocol: AnyObject { - var coursesIDs: [Course.IdType] { get set } -} - -final class WishlistStorageManager: WishlistStorageManagerProtocol { - var coursesIDs: [Course.IdType] { - get { - UserDefaults.standard.array(forKey: Key.wishlistCoursesIDs.rawValue) as? [Course.IdType] ?? [] - } - set { - UserDefaults.standard.setValue(newValue, forKey: Key.wishlistCoursesIDs.rawValue) - } - } - - private enum Key: String { - case wishlistCoursesIDs - } -} diff --git a/Stepic/Sources/Services/WishlistService.swift b/Stepic/Sources/Services/WishlistService.swift deleted file mode 100644 index 4e5b3794db..0000000000 --- a/Stepic/Sources/Services/WishlistService.swift +++ /dev/null @@ -1,143 +0,0 @@ -import Foundation -import PromiseKit - -protocol WishlistServiceProtocol: AnyObject { - func canAdd(_ course: Course) -> Bool - func contains(_ courseID: Course.IdType) -> Bool - func getWishlist() -> [Course.IdType] - func removeAll() - - func fetchWishlist(userID: User.IdType) -> Promise - func add(_ courseID: Course.IdType, userID: User.IdType) -> Promise - func remove(_ courseID: Course.IdType, userID: User.IdType) -> Promise -} - -extension WishlistServiceProtocol { - func canAdd(_ course: Course) -> Bool { !course.enrolled } - - func contains(_ courseID: Course.IdType) -> Bool { self.getWishlist().contains(courseID) } - - func contains(_ course: Course) -> Bool { self.contains(course.id) } - - func add(_ course: Course, userID: User.IdType) -> Promise { self.add(course.id, userID: userID) } - - func remove(_ course: Course, userID: User.IdType) -> Promise { self.remove(course.id, userID: userID) } -} - -final class WishlistService: WishlistServiceProtocol { - private let wishlistStorageManager: WishlistStorageManagerProtocol - private let storageRecordsNetworkService: StorageRecordsNetworkServiceProtocol - - private let dataBackUpdateService: DataBackUpdateServiceProtocol - - init( - wishlistStorageManager: WishlistStorageManagerProtocol, - storageRecordsNetworkService: StorageRecordsNetworkServiceProtocol, - dataBackUpdateService: DataBackUpdateServiceProtocol - ) { - self.wishlistStorageManager = wishlistStorageManager - self.storageRecordsNetworkService = storageRecordsNetworkService - self.dataBackUpdateService = dataBackUpdateService - } - - func getWishlist() -> [Course.IdType] { - self.wishlistStorageManager.coursesIDs - } - - func removeAll() { - self.wishlistStorageManager.coursesIDs = [] - } - - func fetchWishlist(userID: User.IdType) -> Promise { - self.storageRecordsNetworkService.fetch( - userID: userID, - kind: .wishlist - ).done { storageRecords, _ in - if let storageRecord = storageRecords.first, - case .wishlist = storageRecord.kind, - let wishlistData = storageRecord.data as? WishlistStorageRecordData { - self.wishlistStorageManager.coursesIDs = wishlistData.coursesIDs - } else { - self.wishlistStorageManager.coursesIDs = [] - } - } - } - - func add(_ courseID: Course.IdType, userID: User.IdType) -> Promise { - if self.wishlistStorageManager.coursesIDs.contains(courseID) { - return .value(()) - } - - return self.storageRecordsNetworkService.fetch( - userID: userID, - kind: .wishlist - ).then { storageRecords, _ -> Promise in - if let storageRecord = storageRecords.first, - case .wishlist = storageRecord.kind, - let wishlistData = storageRecord.data as? WishlistStorageRecordData { - if !wishlistData.coursesIDs.contains(courseID) { - wishlistData.coursesIDs.append(courseID) - } - - return self.storageRecordsNetworkService.update(record: storageRecord) - } else { - let storageRecord = StorageRecord( - data: WishlistStorageRecordData(coursesIDs: [courseID]), - kind: .wishlist - ) - - return self.storageRecordsNetworkService.create(record: storageRecord) - } - }.then { storageRecord -> Promise in - if let wishlistData = storageRecord.data as? WishlistStorageRecordData { - self.wishlistStorageManager.coursesIDs = wishlistData.coursesIDs - } - - self.dataBackUpdateService.triggerWishlistUpdate(coursesIDs: self.wishlistStorageManager.coursesIDs) - - return .value(()) - } - } - - func remove(_ courseID: Course.IdType, userID: User.IdType) -> Promise { - if !self.wishlistStorageManager.coursesIDs.contains(courseID) { - return .value(()) - } - - return self.storageRecordsNetworkService.fetch( - userID: userID, - kind: .wishlist - ).then { storageRecords, _ -> Promise in - if let storageRecord = storageRecords.first, - case .wishlist = storageRecord.kind, - let wishlistData = storageRecord.data as? WishlistStorageRecordData { - wishlistData.coursesIDs = wishlistData.coursesIDs.filter { $0 != courseID } - return self.storageRecordsNetworkService.update(record: storageRecord).map { $0 } - } else { - return .value(nil) - } - }.then { storageRecordOrNil -> Promise in - if let storageRecord = storageRecordOrNil, - let wishlistData = storageRecord.data as? WishlistStorageRecordData { - self.wishlistStorageManager.coursesIDs = wishlistData.coursesIDs - } else { - self.wishlistStorageManager.coursesIDs = - self.wishlistStorageManager.coursesIDs.filter { $0 != courseID } - } - - self.dataBackUpdateService.triggerWishlistUpdate(coursesIDs: self.wishlistStorageManager.coursesIDs) - - return .value(()) - } - } -} - -extension WishlistService { - static var `default`: WishlistService { - WishlistService( - wishlistStorageManager: WishlistStorageManager(), - storageRecordsNetworkService: StorageRecordsNetworkService(storageRecordsAPI: StorageRecordsAPI()), - dataBackUpdateService: DataBackUpdateService.default - ) - } -} diff --git a/StepicTests/Info-Develop.plist b/StepicTests/Info-Develop.plist index dd92dffaec..7834611ead 100644 --- a/StepicTests/Info-Develop.plist +++ b/StepicTests/Info-Develop.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.196-develop + 1.201-develop CFBundleSignature ???? CFBundleVersion - 387 + 388 diff --git a/StepicTests/Info-Production.plist b/StepicTests/Info-Production.plist index 86f3fe6384..e84a14824f 100644 --- a/StepicTests/Info-Production.plist +++ b/StepicTests/Info-Production.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.196 + 1.201 CFBundleSignature ???? CFBundleVersion - 387 + 388 diff --git a/StepicTests/Info-Release.plist b/StepicTests/Info-Release.plist index 30ceecb734..b231a3feb0 100644 --- a/StepicTests/Info-Release.plist +++ b/StepicTests/Info-Release.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.196-release + 1.201-release CFBundleSignature ???? CFBundleVersion - 387 + 388 diff --git a/StepicUITests/Info-Develop.plist b/StepicUITests/Info-Develop.plist index a4e5306c82..ae6d89f9c0 100644 --- a/StepicUITests/Info-Develop.plist +++ b/StepicUITests/Info-Develop.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.196-develop + 1.201-develop CFBundleVersion - 387 + 388 diff --git a/StepicUITests/Info-Production.plist b/StepicUITests/Info-Production.plist index 552ddce002..5b6b6e961b 100644 --- a/StepicUITests/Info-Production.plist +++ b/StepicUITests/Info-Production.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.196 + 1.201 CFBundleVersion - 387 + 388 diff --git a/StepicUITests/Info-Release.plist b/StepicUITests/Info-Release.plist index c10802b71b..46d1f5acef 100644 --- a/StepicUITests/Info-Release.plist +++ b/StepicUITests/Info-Release.plist @@ -15,8 +15,8 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.196-release + 1.201-release CFBundleVersion - 387 + 388 diff --git a/StepicWidget/Info-Develop.plist b/StepicWidget/Info-Develop.plist index 5af3a3f9dc..42a9ddfa40 100644 --- a/StepicWidget/Info-Develop.plist +++ b/StepicWidget/Info-Develop.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.196-develop + 1.201-develop CFBundleVersion - 387 + 388 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Production.plist b/StepicWidget/Info-Production.plist index e573be530c..d352d33adb 100644 --- a/StepicWidget/Info-Production.plist +++ b/StepicWidget/Info-Production.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.196 + 1.201 CFBundleVersion - 387 + 388 NSExtension NSExtensionPointIdentifier diff --git a/StepicWidget/Info-Release.plist b/StepicWidget/Info-Release.plist index 6f9b3a0cf1..bfc99f98a0 100644 --- a/StepicWidget/Info-Release.plist +++ b/StepicWidget/Info-Release.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.196-release + 1.201-release CFBundleVersion - 387 + 388 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Develop.plist b/StickerPackExtension/Info-Develop.plist index 85e8dc1cf1..023b9076b6 100644 --- a/StickerPackExtension/Info-Develop.plist +++ b/StickerPackExtension/Info-Develop.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.196-develop + 1.201-develop CFBundleVersion - 387 + 388 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Production.plist b/StickerPackExtension/Info-Production.plist index 5dc7e42427..62a8945676 100644 --- a/StickerPackExtension/Info-Production.plist +++ b/StickerPackExtension/Info-Production.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.196 + 1.201 CFBundleVersion - 387 + 388 NSExtension NSExtensionPointIdentifier diff --git a/StickerPackExtension/Info-Release.plist b/StickerPackExtension/Info-Release.plist index e45e537005..5bf5e1394b 100644 --- a/StickerPackExtension/Info-Release.plist +++ b/StickerPackExtension/Info-Release.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.196-release + 1.201-release CFBundleVersion - 387 + 388 NSExtension NSExtensionPointIdentifier diff --git a/fastlane/release-notes.txt b/fastlane/release-notes.txt index 3762709d54..758525743f 100644 --- a/fastlane/release-notes.txt +++ b/fastlane/release-notes.txt @@ -1,2 +1,3 @@ Что тестировать: -- Debug menu / Управление remote и local флагами APPS-3482 \ No newline at end of file +- Новый flow покупки курса / Логика определения цены APPS-3445 +- Wishlist / Перейти на новое API APPS-3509 \ No newline at end of file