diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml new file mode 100644 index 000000000..d2d579ce3 --- /dev/null +++ b/.github/workflows/swiftlint.yml @@ -0,0 +1,37 @@ +name: SwiftLint + +on: + workflow_dispatch: + + pull_request: + +jobs: + lint: + name: Lint + runs-on: macos-latest + + concurrency: + # When running on develop, use the sha to allow all runs of this workflow to run concurrently. + # Otherwise only allow a single run of this workflow on each branch, automatically cancelling older runs. + group: ${{ github.ref == 'refs/heads/develop' && format('swiftlint-develop-{0}', github.sha) || format('swiftlint-{0}', github.ref) }} + cancel-in-progress: true + + steps: + - uses: nschloe/action-cached-lfs-checkout@v1.2.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: actions/cache@v3 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + + - name: Setup environment + run: + source ci_scripts/ci_prepare_env.sh && setup_github_actions_environment + + - name: SwiftLint + run: + bundle exec fastlane linting diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index fa683726e..8d72de40f 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -3,15 +3,12 @@ name: Unit Tests on: workflow_dispatch: - push: - branches: [ develop ] - pull_request: jobs: tests: name: Tests - runs-on: macos-14 + runs-on: macos-latest concurrency: # When running on develop, use the sha to allow all runs of this workflow to run concurrently. @@ -24,7 +21,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} @@ -35,15 +32,11 @@ jobs: run: source ci_scripts/ci_prepare_env.sh && setup_github_actions_environment - - name: SwiftLint - run: - bundle exec fastlane linting - - name: Run tests run: bundle exec fastlane unit_tests - name: Archive artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: name: test-output @@ -52,6 +45,6 @@ jobs: if-no-files-found: ignore - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: flags: unittests diff --git a/.github/workflows/validate-translations.yml b/.github/workflows/validate-translations.yml new file mode 100644 index 000000000..8bb49d670 --- /dev/null +++ b/.github/workflows/validate-translations.yml @@ -0,0 +1,49 @@ +name: Test Makefile + +on: + workflow_dispatch: + + push: + branches: [ develop ] + + pull_request: + +jobs: + translations: + name: "${{ matrix.case.name }}" + runs-on: macos-14 + + strategy: + matrix: + case: + - name: clean_translations + command: | + make clean_translations; + + - name: extract_translations + command: | + make extract_translations; + echo "Ensure combined localization file exists"; + test -f I18N/I18N/en.lproj/Localizable.strings; + + - name: pull_translations + command: + make pull_translations; + echo "Files are split properly"; + test -f Authorization/Authorization/uk.lproj/Localizable.strings; + + steps: + - uses: nschloe/action-cached-lfs-checkout@v1.2.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Use Python + uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - name: Install translations requirements + run: make translation_requirements + + - name: "${{ matrix.case.name }}" + run: "${{ matrix.case.command }}" diff --git a/.gitignore b/.gitignore index 8a80e27f8..75a5357e4 100644 --- a/.gitignore +++ b/.gitignore @@ -115,4 +115,11 @@ vendor/ venv/ Podfile.lock config_settings.yaml -default_config/ \ No newline at end of file +default_config/ + +# Translations ignored files +.venv/ +I18N/ +*.lproj/ +!en.lproj/ +/config_script/__pycache__ diff --git a/.swiftlint.yml b/.swiftlint.yml index f6b96b50b..4ed67e1a6 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -18,14 +18,24 @@ opt_in_rules: # some rules are only opt-in excluded: # paths to ignore during linting. Takes precedence over `included`. - Carthage - Pods + - Core/CoreTests - Authorization/AuthorizationTests - Course/CourseTests - Dashboard/DashboardTests - Discovery/DiscoveryTests - Discussion/DiscussionTests - Profile/ProfileTests + - WhatsNew/WhatsNewTests + - Theme/ThemeTests - vendor -# - Source/ExcludedFolder + - Core/Core/SwiftGen + - Authorization/Authorization/SwiftGen + - Course/Course/SwiftGen + - Discovery/Discovery/SwiftGen + - Dashboard/Dashboard/SwiftGen + - Profile/Profile/SwiftGen + - WhatsNew/WhatsNew/SwiftGen + - Theme/Theme/SwiftGen # - Source/ExcludedFile.swift # - Source/*/ExcludedFile.swift # Exclude files with a wildcard #analyzer_rules: # Rules run by `swiftlint analyze` (experimental) diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index acde4b3e3..013667223 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -26,8 +26,22 @@ 0770DE6B28D0C035006D8A5D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE6D28D0C035006D8A5D /* Localizable.strings */; }; 0770DE7128D0C0E7006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE7028D0C0E7006D8A5D /* Strings.swift */; }; 5FB79D2802949372CDAF08D6 /* Pods_App_Authorization_AuthorizationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4FAE9B7FD61FF88C9C4FE1E8 /* Pods_App_Authorization_AuthorizationTests.framework */; }; + 99C1654B2C0C4F0600DC384D /* ContainerWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C1654A2C0C4F0600DC384D /* ContainerWebView.swift */; }; + 99C1654D2C0C4F2F00DC384D /* SSOHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C1654C2C0C4F2F00DC384D /* SSOHelper.swift */; }; + 99C1654F2C0C4F5900DC384D /* SSOWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C1654E2C0C4F5900DC384D /* SSOWebView.swift */; }; + 99C165512C0C4F7B00DC384D /* SSOWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C165502C0C4F7B00DC384D /* SSOWebViewModel.swift */; }; BA8B3A322AD5487300D25EF5 /* SocialAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A312AD5487300D25EF5 /* SocialAuthView.swift */; }; BADB3F552AD6DFC3004D5CFA /* SocialAuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB3F542AD6DFC3004D5CFA /* SocialAuthViewModel.swift */; }; + CE7CAF2D2CC155BE00E0AC9D /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CE7CAF2C2CC155BE00E0AC9D /* OEXFoundation */; }; + CE7FB8772CC13C0B0088001A /* FacebookLogin in Frameworks */ = {isa = PBXBuildFile; productRef = CE7FB8762CC13C0B0088001A /* FacebookLogin */; }; + CE7FB87A2CC13C3C0088001A /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = CE7FB8792CC13C3C0088001A /* GoogleSignInSwift */; }; + CEB1E2642CC14E3100921517 /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CEB1E2632CC14E3100921517 /* OEXFoundation */; }; + CEB25A022CC13A36007FC792 /* AppleAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB259FC2CC13A36007FC792 /* AppleAuthProvider.swift */; }; + CEB25A032CC13A36007FC792 /* FacebookAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB259FE2CC13A36007FC792 /* FacebookAuthProvider.swift */; }; + CEB25A042CC13A36007FC792 /* GoogleAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB259FD2CC13A36007FC792 /* GoogleAuthProvider.swift */; }; + CEB25A052CC13A36007FC792 /* SocialAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB25A002CC13A36007FC792 /* SocialAuthResponse.swift */; }; + CEB25A062CC13A36007FC792 /* MicrosoftAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB259FF2CC13A36007FC792 /* MicrosoftAuthProvider.swift */; }; + CEB25A072CC13A36007FC792 /* SocialAuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB259FA2CC13A36007FC792 /* SocialAuthError.swift */; }; DE843D6BB1B9DDA398494890 /* Pods_App_Authorization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47BCFB7C19382EECF15131B6 /* Pods_App_Authorization.framework */; }; E03261642AE64676002CA7EB /* StartupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E03261632AE64676002CA7EB /* StartupViewModel.swift */; }; E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E03261652AE64AF4002CA7EB /* StartupView.swift */; }; @@ -43,6 +57,19 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + CE7CAF2F2CC155BE00E0AC9D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 02066B432906D72400F4307E /* SignUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpView.swift; sourceTree = ""; }; 02066B452906D72F00F4307E /* SignUpViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpViewModel.swift; sourceTree = ""; }; @@ -53,7 +80,6 @@ 025F40E129D360E20064C183 /* ResetPasswordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPasswordViewModel.swift; sourceTree = ""; }; 02A2ACDA2A4B016100FBBBBB /* AuthorizationAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationAnalytics.swift; sourceTree = ""; }; 02E0618329DC2373006E9024 /* ResetPasswordViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPasswordViewModelTests.swift; sourceTree = ""; }; - 02ED50CC29A64B90008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02F3BFE4292533720051930C /* AuthorizationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationRouter.swift; sourceTree = ""; }; 071009C628D1DA4F00344290 /* SignInViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewModel.swift; sourceTree = ""; }; 07169454296D913300E3DED6 /* AuthorizationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AuthorizationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -77,10 +103,20 @@ 7A84BB166492D4E46FBCF01C /* Pods-App-Authorization-AuthorizationTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.debugdev.xcconfig"; sourceTree = ""; }; 90DFBB75EF40580E180D71C8 /* Pods-App-Authorization.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.debugdev.xcconfig"; sourceTree = ""; }; 96C85172770225EB81A6D2DA /* Pods-App-Authorization.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.releasedev.xcconfig"; sourceTree = ""; }; + 99C1654A2C0C4F0600DC384D /* ContainerWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerWebView.swift; sourceTree = ""; }; + 99C1654C2C0C4F2F00DC384D /* SSOHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOHelper.swift; sourceTree = ""; }; + 99C1654E2C0C4F5900DC384D /* SSOWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOWebView.swift; sourceTree = ""; }; + 99C165502C0C4F7B00DC384D /* SSOWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSOWebViewModel.swift; sourceTree = ""; }; 9BF6A1004A955E24527FCF0F /* Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.releaseprod.xcconfig"; sourceTree = ""; }; A99D45203C981893C104053A /* Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Authorization-AuthorizationTests/Pods-App-Authorization-AuthorizationTests.releasestage.xcconfig"; sourceTree = ""; }; BA8B3A312AD5487300D25EF5 /* SocialAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthView.swift; sourceTree = ""; }; BADB3F542AD6DFC3004D5CFA /* SocialAuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthViewModel.swift; sourceTree = ""; }; + CEB259FA2CC13A36007FC792 /* SocialAuthError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthError.swift; sourceTree = ""; }; + CEB259FC2CC13A36007FC792 /* AppleAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAuthProvider.swift; sourceTree = ""; }; + CEB259FD2CC13A36007FC792 /* GoogleAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthProvider.swift; sourceTree = ""; }; + CEB259FE2CC13A36007FC792 /* FacebookAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthProvider.swift; sourceTree = ""; }; + CEB259FF2CC13A36007FC792 /* MicrosoftAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosoftAuthProvider.swift; sourceTree = ""; }; + CEB25A002CC13A36007FC792 /* SocialAuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthResponse.swift; sourceTree = ""; }; E03261632AE64676002CA7EB /* StartupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupViewModel.swift; sourceTree = ""; }; E03261652AE64AF4002CA7EB /* StartupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupView.swift; sourceTree = ""; }; E78971D8E6ED2116BBF9FD66 /* Pods-App-Authorization.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.release.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.release.xcconfig"; sourceTree = ""; }; @@ -94,6 +130,7 @@ buildActionMask = 2147483647; files = ( 07169458296D913400E3DED6 /* Authorization.framework in Frameworks */, + CE7CAF2D2CC155BE00E0AC9D /* OEXFoundation in Frameworks */, 5FB79D2802949372CDAF08D6 /* Pods_App_Authorization_AuthorizationTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -104,6 +141,9 @@ files = ( 0770DE4728D0A3DA006D8A5D /* Core.framework in Frameworks */, DE843D6BB1B9DDA398494890 /* Pods_App_Authorization.framework in Frameworks */, + CE7FB8772CC13C0B0088001A /* FacebookLogin in Frameworks */, + CEB1E2642CC14E3100921517 /* OEXFoundation in Frameworks */, + CE7FB87A2CC13C3C0088001A /* GoogleSignInSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -147,6 +187,7 @@ 071009CC28D1E24000344290 /* Presentation */ = { isa = PBXGroup; children = ( + 99C165492C0C4EF000DC384D /* SSO */, BA8B3A302AD5485100D25EF5 /* SocialAuth */, E03261622AE6464A002CA7EB /* Startup */, 020C31BD290AADA700D6DEA2 /* Base */, @@ -219,6 +260,7 @@ 0770DE3D28D0A319006D8A5D /* Authorization */ = { isa = PBXGroup; children = ( + CEB25A012CC13A36007FC792 /* SocialAuth */, 0770DE6F28D0C08E006D8A5D /* SwiftGen */, 071009CC28D1E24000344290 /* Presentation */, 0770DE6D28D0C035006D8A5D /* Localizable.strings */, @@ -268,6 +310,17 @@ path = ../Pods; sourceTree = ""; }; + 99C165492C0C4EF000DC384D /* SSO */ = { + isa = PBXGroup; + children = ( + 99C1654A2C0C4F0600DC384D /* ContainerWebView.swift */, + 99C1654C2C0C4F2F00DC384D /* SSOHelper.swift */, + 99C1654E2C0C4F5900DC384D /* SSOWebView.swift */, + 99C165502C0C4F7B00DC384D /* SSOWebViewModel.swift */, + ); + path = SSO; + sourceTree = ""; + }; BA8B3A302AD5485100D25EF5 /* SocialAuth */ = { isa = PBXGroup; children = ( @@ -277,6 +330,27 @@ path = SocialAuth; sourceTree = ""; }; + CEB259FB2CC13A36007FC792 /* Error */ = { + isa = PBXGroup; + children = ( + CEB259FA2CC13A36007FC792 /* SocialAuthError.swift */, + ); + path = Error; + sourceTree = ""; + }; + CEB25A012CC13A36007FC792 /* SocialAuth */ = { + isa = PBXGroup; + children = ( + CEB259FB2CC13A36007FC792 /* Error */, + CEB259FC2CC13A36007FC792 /* AppleAuthProvider.swift */, + CEB259FD2CC13A36007FC792 /* GoogleAuthProvider.swift */, + CEB259FE2CC13A36007FC792 /* FacebookAuthProvider.swift */, + CEB259FF2CC13A36007FC792 /* MicrosoftAuthProvider.swift */, + CEB25A002CC13A36007FC792 /* SocialAuthResponse.swift */, + ); + path = SocialAuth; + sourceTree = ""; + }; E03261622AE6464A002CA7EB /* Startup */ = { isa = PBXGroup; children = ( @@ -308,6 +382,7 @@ 07169451296D913300E3DED6 /* Frameworks */, 07169452296D913300E3DED6 /* Resources */, 95C8CAF0620ABBBAD7ED66D6 /* [CP] Copy Pods Resources */, + CE7CAF2F2CC155BE00E0AC9D /* Embed Frameworks */, ); buildRules = ( ); @@ -368,6 +443,11 @@ uk, ); mainGroup = 0770DE3128D0A318006D8A5D; + packageReferences = ( + CE7FB8752CC13C0B0088001A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */, + CE7FB8782CC13C3C0088001A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, + CEB1E2622CC14E3100921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */, + ); productRefGroup = 0770DE3C28D0A319006D8A5D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -502,13 +582,23 @@ 02066B462906D72F00F4307E /* SignUpViewModel.swift in Sources */, E03261642AE64676002CA7EB /* StartupViewModel.swift in Sources */, 02A2ACDB2A4B016100FBBBBB /* AuthorizationAnalytics.swift in Sources */, + 99C165512C0C4F7B00DC384D /* SSOWebViewModel.swift in Sources */, + 99C1654B2C0C4F0600DC384D /* ContainerWebView.swift in Sources */, 025F40E029D1E2FC0064C183 /* ResetPasswordView.swift in Sources */, 020C31CB290BF49900D6DEA2 /* FieldsView.swift in Sources */, 0770DE4E28D0A677006D8A5D /* SignInView.swift in Sources */, 02F3BFE5292533720051930C /* AuthorizationRouter.swift in Sources */, + 99C1654F2C0C4F5900DC384D /* SSOWebView.swift in Sources */, E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */, 071009C728D1DA4F00344290 /* SignInViewModel.swift in Sources */, BA8B3A322AD5487300D25EF5 /* SocialAuthView.swift in Sources */, + 99C1654D2C0C4F2F00DC384D /* SSOHelper.swift in Sources */, + CEB25A022CC13A36007FC792 /* AppleAuthProvider.swift in Sources */, + CEB25A032CC13A36007FC792 /* FacebookAuthProvider.swift in Sources */, + CEB25A042CC13A36007FC792 /* GoogleAuthProvider.swift in Sources */, + CEB25A052CC13A36007FC792 /* SocialAuthResponse.swift in Sources */, + CEB25A062CC13A36007FC792 /* MicrosoftAuthProvider.swift in Sources */, + CEB25A072CC13A36007FC792 /* SocialAuthError.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -528,7 +618,6 @@ isa = PBXVariantGroup; children = ( 0770DE6C28D0C035006D8A5D /* en */, - 02ED50CC29A64B90008341CD /* uk */, ); name = Localizable.strings; sourceTree = ""; @@ -608,14 +697,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -643,7 +732,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -719,14 +808,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -753,7 +842,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -771,7 +860,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -789,7 +878,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -807,7 +896,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -825,7 +914,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -843,7 +932,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -861,7 +950,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.AuthorizationTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -943,14 +1032,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1036,14 +1125,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1134,14 +1223,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1227,14 +1316,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1383,14 +1472,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1418,14 +1507,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1493,6 +1582,56 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + CE7FB8752CC13C0B0088001A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/facebook/facebook-ios-sdk"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 16.3.1; + }; + }; + CE7FB8782CC13C3C0088001A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/google/GoogleSignIn-iOS"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.0.0; + }; + }; + CEB1E2622CC14E3100921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; + requirement = { + kind = exactVersion; + version = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + CE7CAF2C2CC155BE00E0AC9D /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E2622CC14E3100921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; + CE7FB8762CC13C0B0088001A /* FacebookLogin */ = { + isa = XCSwiftPackageProductDependency; + package = CE7FB8752CC13C0B0088001A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */; + productName = FacebookLogin; + }; + CE7FB8792CC13C3C0088001A /* GoogleSignInSwift */ = { + isa = XCSwiftPackageProductDependency; + package = CE7FB8782CC13C3C0088001A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */; + productName = GoogleSignInSwift; + }; + CEB1E2632CC14E3100921517 /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E2622CC14E3100921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 0770DE3228D0A318006D8A5D /* Project object */; } diff --git a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift index b59ebd774..240f60fa0 100644 --- a/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift +++ b/Authorization/Authorization/Presentation/AuthorizationAnalytics.swift @@ -6,15 +6,20 @@ // import Foundation +import Core +import OEXFoundation public enum AuthMethod: Equatable { case password + case SSO case socailAuth(SocialAuthMethod) public var analyticsValue: String { switch self { case .password: "password" + case .SSO: + "SSO" case .socailAuth(let socialAuthMethod): socialAuthMethod.rawValue } @@ -22,10 +27,10 @@ public enum AuthMethod: Equatable { } public enum SocialAuthMethod: String { - case facebook = "facebook" - case google = "google" - case microsoft = "microsoft" - case apple = "apple" + case facebook + case google + case microsoft + case apple } //sourcery: AutoMockable @@ -40,6 +45,7 @@ public protocol AuthorizationAnalytics { func forgotPasswordClicked() func resetPasswordClicked() func resetPassword(success: Bool) + func authTrackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) } #if DEBUG @@ -54,5 +60,6 @@ class AuthorizationAnalyticsMock: AuthorizationAnalytics { public func forgotPasswordClicked() {} public func resetPasswordClicked() {} public func resetPassword(success: Bool) {} + public func authTrackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) {} } #endif diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index 209da1912..84d14adb0 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Theme import Swinject @@ -15,7 +16,7 @@ public struct SignInView: View { @State private var email: String = "" @State private var password: String = "" - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal @ObservedObject private var viewModel: SignInViewModel @@ -27,7 +28,7 @@ public struct SignInView: View { public var body: some View { ZStack(alignment: .top) { VStack { - ThemeAssets.authBackground.swiftUIImage + ThemeAssets.headerBackground.swiftUIImage .resizable() .edgesIgnoringSafeArea(.top) .accessibilityIdentifier("auth_bg_image") @@ -61,100 +62,167 @@ public struct SignInView: View { ScrollView { VStack { VStack(alignment: .leading) { - Text(AuthLocalization.SignIn.logInTitle) - .font(Theme.Fonts.displaySmall) - .foregroundColor(Theme.Colors.textPrimary) - .padding(.bottom, 4) - .accessibilityIdentifier("signin_text") - Text(AuthLocalization.SignIn.welcomeBack) - .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) - .padding(.bottom, 20) - .accessibilityIdentifier("welcome_back_text") - Text(AuthLocalization.SignIn.emailOrUsername) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) - .accessibilityIdentifier("username_text") - TextField("", text: $email) - .font(Theme.Fonts.bodyLarge) - .foregroundColor(Theme.Colors.textInputTextColor) - .keyboardType(.emailAddress) - .textContentType(.emailAddress) - .autocapitalization(.none) - .autocorrectionDisabled() - .padding(.all, 14) - .background( - Theme.InputFieldBackground( - placeHolder: AuthLocalization.SignIn.emailOrUsername, - text: email, - padding: 15 + if viewModel.config.uiComponents.loginRegistrationEnabled { + Text(AuthLocalization.SignIn.logInTitle) + .font(Theme.Fonts.displaySmall) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.bottom, 4) + .accessibilityIdentifier("signin_text") + Text(AuthLocalization.SignIn.welcomeBack) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.bottom, 20) + .accessibilityIdentifier("welcome_back_text") + Text(AuthLocalization.SignIn.emailOrUsername) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("username_text") + TextField("", text: $email) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textInputTextColor) + .keyboardType(.emailAddress) + .textContentType(.emailAddress) + .autocapitalization(.none) + .autocorrectionDisabled() + .padding(.all, 14) + .background( + Theme.InputFieldBackground( + placeHolder: AuthLocalization.SignIn.emailOrUsername, + text: email, + padding: 15 + ) ) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputStroke) - ) - .accessibilityIdentifier("username_textfield") - - Text(AuthLocalization.SignIn.password) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) - .padding(.top, 18) - .accessibilityIdentifier("password_text") - SecureField("", text: $password) - .font(Theme.Fonts.bodyLarge) - .foregroundColor(Theme.Colors.textInputTextColor) - .padding(.all, 14) - .background( - Theme.InputFieldBackground( - placeHolder: AuthLocalization.SignIn.password, - text: password, - padding: 15 + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill(Theme.Colors.textInputStroke) ) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputStroke) - ) - .accessibilityIdentifier("password_textfield") - HStack { - if !viewModel.config.features.startupScreenEnabled { - Button(CoreLocalization.register) { - viewModel.router.showRegisterScreen(sourceScreen: viewModel.sourceScreen) + .accessibilityIdentifier("username_textfield") + + Text(AuthLocalization.SignIn.password) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.top, 18) + .accessibilityIdentifier("password_text") + SecureField("", text: $password) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textInputTextColor) + .padding(.all, 14) + .background( + Theme.InputFieldBackground( + placeHolder: AuthLocalization.SignIn.password, + text: password, + padding: 15 + ) + ) + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill(Theme.Colors.textInputStroke) + ) + .accessibilityIdentifier("password_textfield") + HStack { + if !viewModel.config.features.startupScreenEnabled { + Button(CoreLocalization.SignIn.registerBtn) { + viewModel.router.showRegisterScreen(sourceScreen: viewModel.sourceScreen) + } + .foregroundColor(Theme.Colors.accentColor) + .accessibilityIdentifier("register_button") + + Spacer() } - .foregroundColor(Theme.Colors.accentColor) - .accessibilityIdentifier("register_button") - Spacer() + Button(AuthLocalization.SignIn.forgotPassBtn) { + viewModel.trackForgotPasswordClicked() + viewModel.router.showForgotPasswordScreen() + } + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.infoColor) + .padding(.top, 0) + .accessibilityIdentifier("forgot_password_button") } - Button(AuthLocalization.SignIn.forgotPassBtn) { - viewModel.trackForgotPasswordClicked() - viewModel.router.showForgotPasswordScreen() + if viewModel.isShowProgress { + HStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(20) + .accessibilityIdentifier("progress_bar") + }.frame(maxWidth: .infinity) + } else { + StyledButton(CoreLocalization.SignIn.logInBtn) { + Task { + await viewModel.login(username: email, password: password) + } + } + .frame(maxWidth: .infinity) + .padding(.top, 40) + .accessibilityIdentifier("signin_button") } - .font(Theme.Fonts.bodyLarge) - .foregroundColor(Theme.Colors.infoColor) - .padding(.top, 0) - .accessibilityIdentifier("forgot_password_button") } - - if viewModel.isShowProgress { - HStack(alignment: .center) { - ProgressBar(size: 40, lineWidth: 8) - .padding(20) - .accessibilityIdentifier("progressbar") - }.frame(maxWidth: .infinity) - } else { - StyledButton(CoreLocalization.SignIn.logInBtn) { - Task { - await viewModel.login(username: email, password: password) + if viewModel.config.uiComponents.samlSSOLoginEnabled { + if !viewModel.config.uiComponents.loginRegistrationEnabled{ + VStack(alignment: .center) { + Text(AuthLocalization.SignIn.ssoHeading) + .font(Theme.Fonts.headlineSmall) + .multilineTextAlignment(.center) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.bottom, 4) + .padding(.horizontal, 20) + .accessibilityIdentifier("signin_sso_heading") + } + + Divider() + + VStack(alignment: .center) { + Text(AuthLocalization.SignIn.ssoLogInTitle) + .font(Theme.Fonts.headlineSmall) + .multilineTextAlignment(.center) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.bottom, 10) + .padding(.horizontal, 20) + .accessibilityIdentifier("signin_sso_login_title") + + Text(AuthLocalization.SignIn.ssoLogInSubtitle) + .font(Theme.Fonts.titleMedium) + .multilineTextAlignment(.center) + .foregroundColor(Theme.Colors.textSecondaryLight) + .padding(.bottom, 10) + .padding(.horizontal, 20) + .accessibilityIdentifier("signin_sso_login_subtitle") + } + } + + VStack(alignment: .center) { + + if viewModel.isShowProgress { + HStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(20) + .accessibilityIdentifier("progressbar") + }.frame(maxWidth: .infinity) + } else { + let languageCode = Locale.current.language.languageCode?.identifier ?? "en" + if viewModel.config.uiComponents.samlSSODefaultLoginButton { + StyledButton(viewModel.config.ssoButtonTitle[languageCode] as! String, action: { + viewModel.router.showSSOWebBrowser(title: CoreLocalization.SignIn.logInBtn) + }) + .frame(maxWidth: .infinity) + .padding(.top, 20) + .accessibilityIdentifier("signin_SSO_button") + } else { + StyledButton(viewModel.config.ssoButtonTitle[languageCode] as! String, action: { + viewModel.router.showSSOWebBrowser(title: CoreLocalization.SignIn.logInBtn) + }, + color: .white, + textColor: Theme.Colors.accentColor, + borderColor: Theme.Colors.accentColor) + .frame(maxWidth: .infinity) + .padding(.top, 20) + .accessibilityIdentifier("signin_SSO_button") + } + } } - .frame(maxWidth: .infinity) - .padding(.top, 40) - .accessibilityIdentifier("signin_button") } } if viewModel.socialAuthEnabled { @@ -209,9 +277,12 @@ public struct SignInView: View { } } } - .hideNavigationBar() + .navigationBarHidden(true) .ignoresSafeArea(.all, edges: .horizontal) .background(Theme.Colors.background.ignoresSafeArea(.all)) + .onFirstAppear{ + viewModel.trackScreenEvent() + } } @ViewBuilder diff --git a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift index 041c98ca7..b719a9ede 100644 --- a/Authorization/Authorization/Presentation/Login/SignInViewModel.swift +++ b/Authorization/Authorization/Presentation/Login/SignInViewModel.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import SwiftUI import Alamofire import AuthenticationServices @@ -82,11 +83,26 @@ public class SignInViewModel: ObservableObject { analytics.identify(id: "\(user.id)", username: user.username, email: user.email) analytics.userLogin(method: .password) router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + NotificationCenter.default.post(name: .userAuthorized, object: nil) } catch let error { failure(error) } } + @MainActor + func ssoLogin(title: String) async { + analytics.userSignInClicked() + isShowProgress = true + do { + let user = try await interactor.login(ssoToken: "") + analytics.identify(id: "\(user.id)", username: user.username, email: user.email) + analytics.userLogin(method: .password) + router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + } catch let error { + failure(error) + } + } + @MainActor func login(with result: Result) async { switch result { @@ -113,6 +129,7 @@ public class SignInViewModel: ObservableObject { analytics.identify(id: "\(user.id)", username: user.username, email: user.email) analytics.userLogin(method: authMethod) router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + NotificationCenter.default.post(name: .userAuthorized, object: nil) } catch let error { failure(error, authMethod: authMethod) } @@ -145,5 +162,11 @@ public class SignInViewModel: ObservableObject { func trackForgotPasswordClicked() { analytics.forgotPasswordClicked() } - + + func trackScreenEvent() { + analytics.authTrackScreenEvent( + .logistrationSignIn, + biValue: .logistrationSignIn + ) + } } diff --git a/Authorization/Authorization/Presentation/Registration/SignUpView.swift b/Authorization/Authorization/Presentation/Registration/SignUpView.swift index 4c6e154c0..c4d46caf2 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpView.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Theme public struct SignUpView: View { @@ -14,7 +15,7 @@ public struct SignUpView: View { @State private var disclosureGroupOpen: Bool = false - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal @ObservedObject private var viewModel: SignUpViewModel @@ -29,7 +30,7 @@ public struct SignUpView: View { public var body: some View { ZStack(alignment: .top) { VStack { - ThemeAssets.authBackground.swiftUIImage + ThemeAssets.headerBackground.swiftUIImage .resizable() .edgesIgnoringSafeArea(.top) } @@ -40,7 +41,7 @@ public struct SignUpView: View { VStack(alignment: .center) { ZStack { HStack { - Text(CoreLocalization.register) + Text(CoreLocalization.SignIn.registerBtn) .titleSettings(color: Theme.Colors.loginNavigationText) .accessibilityIdentifier("register_text") } @@ -64,7 +65,7 @@ public struct SignUpView: View { ScrollView { VStack(alignment: .leading) { - Text(CoreLocalization.register) + Text(CoreLocalization.SignIn.registerBtn) .font(Theme.Fonts.displaySmall) .foregroundColor(Theme.Colors.textPrimary) .padding(.bottom, 4) @@ -195,7 +196,10 @@ public struct SignUpView: View { } .ignoresSafeArea(.all, edges: .horizontal) .background(Theme.Colors.background.ignoresSafeArea(.all)) - .hideNavigationBar() + .navigationBarHidden(true) + .onFirstAppear{ + viewModel.trackScreenEvent() + } } } diff --git a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift index 1f57b8c02..a53403f9d 100644 --- a/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift +++ b/Authorization/Authorization/Presentation/Registration/SignUpViewModel.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import SwiftUI import AuthenticationServices import FacebookLogin @@ -136,7 +137,7 @@ public class SignUpViewModel: ObservableObject { analytics.registrationSuccess(method: authMetod.analyticsValue) isShowProgress = false router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) - + NotificationCenter.default.post(name: .userAuthorized, object: nil) } catch let error { isShowProgress = false if case APIError.invalidGrant = error { @@ -193,6 +194,7 @@ public class SignUpViewModel: ObservableObject { analytics.userLogin(method: authMethod) isShowProgress = false router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + NotificationCenter.default.post(name: .userAuthorized, object: nil) } catch { update(fullName: response.name, email: response.email) self.externalToken = response.token @@ -212,4 +214,11 @@ public class SignUpViewModel: ObservableObject { func trackCreateAccountClicked() { analytics.createAccountClicked() } + + func trackScreenEvent() { + analytics.authTrackScreenEvent( + .logistrationRegister, + biValue: .logistrationRegister + ) + } } diff --git a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift index a66562028..e069b75e1 100644 --- a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift +++ b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Theme public struct ResetPasswordView: View { @@ -15,7 +16,7 @@ public struct ResetPasswordView: View { @State private var isRecovered: Bool = false - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal @ObservedObject private var viewModel: ResetPasswordViewModel @@ -28,7 +29,7 @@ public struct ResetPasswordView: View { GeometryReader { proxy in ZStack(alignment: .top) { VStack { - ThemeAssets.authBackground.swiftUIImage + ThemeAssets.headerBackground.swiftUIImage .resizable() .edgesIgnoringSafeArea(.top) } @@ -117,7 +118,7 @@ public struct ResetPasswordView: View { HStack(alignment: .center) { ProgressBar(size: 40, lineWidth: 8) .padding(20) - .accessibilityIdentifier("progressbar") + .accessibilityIdentifier("progress_bar") }.frame(maxWidth: .infinity) } else { StyledButton(AuthLocalization.Forgot.request) { @@ -173,10 +174,8 @@ public struct ResetPasswordView: View { } } .ignoresSafeArea(.all, edges: .horizontal) - .background(Theme.Colors.background.ignoresSafeArea(.all)) - - .hideNavigationBar() + .navigationBarHidden(true) } } } diff --git a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift index 10b2edc00..8310e2e64 100644 --- a/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift +++ b/Authorization/Authorization/Presentation/Reset Password/ResetPasswordViewModel.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation public class ResetPasswordViewModel: ObservableObject { diff --git a/Authorization/Authorization/Presentation/SSO/ContainerWebView.swift b/Authorization/Authorization/Presentation/SSO/ContainerWebView.swift new file mode 100644 index 000000000..476630cf6 --- /dev/null +++ b/Authorization/Authorization/Presentation/SSO/ContainerWebView.swift @@ -0,0 +1,46 @@ +// +// ContainerWebView.swift +// Authorization +// +// Created by Rawan Matar on 02/06/2024. +// + +import SwiftUI +import Core +import Swinject + +public struct ContainerWebView: View { + + // MARK: - Internal Properties + + let url: String + private var pageTitle: String + @Environment(\.presentationMode) var presentationMode + + // MARK: - Init + + public init(_ url: String, title: String) { + self.url = url + self.pageTitle = title + } + + // MARK: - UI + + public var body: some View { + VStack(alignment: .center) { + NavigationBar( + title: pageTitle, + leftButtonAction: { presentationMode.wrappedValue.dismiss() } + ) + + ZStack { + if !url.isEmpty { + SSOWebView(url: URL(string: url), viewModel: Container.shared.resolve(SSOWebViewModel.self)!) + } else { + EmptyView() + } + } + .accessibilityIdentifier("web_browser") + } + } +} diff --git a/Authorization/Authorization/Presentation/SSO/SSOHelper.swift b/Authorization/Authorization/Presentation/SSO/SSOHelper.swift new file mode 100644 index 000000000..1f195a153 --- /dev/null +++ b/Authorization/Authorization/Presentation/SSO/SSOHelper.swift @@ -0,0 +1,80 @@ +// +// SSOHelper.swift +// Authorization +// +// Created by Rawan Matar on 02/06/2024. +// + +import Foundation +import KeychainSwift + +// https://developer.apple.com/documentation/ios-ipados-release-notes/foundation-release-notes + +/** + A Helper for some of the SSO preferences. + Keeps data under the UserDefaults. + */ +public class SSOHelper: NSObject { + + private let keychain: KeychainSwift + public enum SSOHelperKeys: String, CaseIterable { + case cookiePayload + case cookieSignature + case userInfo + + var description: String { + switch self { + case .cookiePayload: + return "edx-jwt-cookie-header-payload" + case .cookieSignature: + return "edx-jwt-cookie-signature" + case .userInfo: + return "edx-user-info" + } + } + } + + public init(keychain: KeychainSwift) { + self.keychain = keychain + } + // MARK: - Public Properties + + /// Authentication + public var cookiePayload: String? { + get { + let defaults = UserDefaults.standard + return keychain.get(SSOHelperKeys.cookiePayload.rawValue) + } + set(newValue) { + if let newValue { + keychain.set(newValue, forKey: SSOHelperKeys.cookiePayload.rawValue) + } else { + keychain.delete(SSOHelperKeys.cookiePayload.rawValue) + } + } + } + + /// Authentication + public var cookieSignature: String? { + get { + let defaults = UserDefaults.standard + return keychain.get(SSOHelperKeys.cookieSignature.rawValue) + } + set(newValue) { + if let newValue { + keychain.set(newValue, forKey: SSOHelperKeys.cookieSignature.rawValue) + } else { + keychain.delete(SSOHelperKeys.cookieSignature.rawValue) + } + } + } + + // MARK: - Public Methods + + /// Checks if the user is login. + public func cleanAfterSuccesfulLogout() { + cookiePayload = nil + cookieSignature = nil + } +} + diff --git a/Authorization/Authorization/Presentation/SSO/SSOWebView.swift b/Authorization/Authorization/Presentation/SSO/SSOWebView.swift new file mode 100644 index 000000000..d7a89cf7b --- /dev/null +++ b/Authorization/Authorization/Presentation/SSO/SSOWebView.swift @@ -0,0 +1,91 @@ +// +// SSOWebView.swift +// Authorization +// +// Created by Rawan Matar on 02/06/2024. +// +import SwiftUI +@preconcurrency import WebKit +import Core + +public struct SSOWebView: UIViewRepresentable { + + let url: URL? + + var viewModel: SSOWebViewModel + + public init(url: URL?, viewModel: SSOWebViewModel) { + self.url = url + self.viewModel = viewModel + } + + public func makeUIView(context: Context) -> WKWebView { + let coordinator = makeCoordinator() + let userContentController = WKUserContentController() + userContentController.add(coordinator, name: "bridge") + + let prefs = WKWebpagePreferences() + let config = WKWebViewConfiguration() + prefs.allowsContentJavaScript = true + + config.userContentController = userContentController + config.defaultWebpagePreferences = prefs + config.websiteDataStore = WKWebsiteDataStore.nonPersistent() + + let wkWebView = WKWebView(frame: .zero, configuration: config) + wkWebView.navigationDelegate = coordinator + + guard let currentURL = url else { + return wkWebView + } + let request = URLRequest(url: currentURL) + wkWebView.load(request) + + return wkWebView + } + + public func updateUIView(_ uiView: WKWebView, context: Context) { + + } + + public func makeCoordinator() -> Coordinator { + Coordinator(viewModel: self.viewModel) + } + + public class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate { + var viewModel: SSOWebViewModel + + init(viewModel: SSOWebViewModel) { + self.viewModel = viewModel + super.init() + } + + // WKScriptMessageHandler + public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + } + + // WKNavigationDelegate + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + if webView.url?.absoluteString == nil { + return + } + } + + public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = webView.url?.absoluteString else { + decisionHandler(.allow) + return + } + + if url.contains(viewModel.config.ssoFinishedURL.absoluteString) { + webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in + Task { + await self.viewModel.SSOLogin(cookies: cookies) + } + } + } + + decisionHandler(.allow) + } + } +} diff --git a/Authorization/Authorization/Presentation/SSO/SSOWebViewModel.swift b/Authorization/Authorization/Presentation/SSO/SSOWebViewModel.swift new file mode 100644 index 000000000..07e0faa55 --- /dev/null +++ b/Authorization/Authorization/Presentation/SSO/SSOWebViewModel.swift @@ -0,0 +1,120 @@ +// +// SSOWebViewModel.swift +// Authorization +// +// Created by Rawan Matar on 02/06/2024. +// + +import Foundation +import SwiftUI +import OEXFoundation +import Core +import Alamofire +import AuthenticationServices +import FacebookLogin +import GoogleSignIn +import MSAL + +public class SSOWebViewModel: ObservableObject { + + @Published private(set) var isShowProgress = false + @Published private(set) var showError: Bool = false + @Published private(set) var showAlert: Bool = false + let sourceScreen: LogistrationSourceScreen = .default + + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + var alertMessage: String? { + didSet { + withAnimation { + showAlert = alertMessage != nil + } + } + } + + let router: AuthorizationRouter + let config: ConfigProtocol + private let interactor: AuthInteractorProtocol + private let analytics: AuthorizationAnalytics + let ssoHelper: SSOHelper + + public init( + interactor: AuthInteractorProtocol, + router: AuthorizationRouter, + config: ConfigProtocol, + analytics: AuthorizationAnalytics, + ssoHelper: SSOHelper + ) { + self.interactor = interactor + self.router = router + self.config = config + self.analytics = analytics + self.ssoHelper = ssoHelper + } + + @MainActor + func SSOLogin(cookies: [HTTPCookie]) async { + guard !cookies.isEmpty else { + errorMessage = "COOKIES EMPTY" + return + } + + isShowProgress = true + for cookie in cookies { + /// Store cookies in UserDefaults + if cookie.name == SSOHelper.SSOHelperKeys.cookiePayload.description { + self.ssoHelper.cookiePayload = cookie.value + } + + if cookie.name == SSOHelper.SSOHelperKeys.cookieSignature.description { + self.ssoHelper.cookieSignature = cookie.value + } + if let signature = self.ssoHelper.cookieSignature, + let payload = self.ssoHelper.cookiePayload { + isShowProgress = true + do { + let user = try await interactor.login(ssoToken: "\(payload).\(signature)") + analytics.identify(id: "\(user.id)", username: user.username, email: user.email) + analytics.userLogin(method: .SSO) + router.showMainOrWhatsNewScreen(sourceScreen: sourceScreen) + } catch let error { + failure(error, authMethod: .SSO) + } + } + } + } + + @MainActor + private func failure(_ error: Error, authMethod: AuthMethod? = nil) { + isShowProgress = false + if let validationError = error.validationError, + let value = validationError.data?["error_description"] as? String { + if authMethod != .password, validationError.statusCode == 400, let authMethod = authMethod { + errorMessage = AuthLocalization.Error.accountNotRegistered( + authMethod.analyticsValue, + config.platformName + ) + } else if validationError.statusCode == 403 { + errorMessage = AuthLocalization.Error.disabledAccount + } else { + errorMessage = value + } + } else if case APIError.invalidGrant = error { + errorMessage = CoreLocalization.Error.invalidCredentials + } else if error.isInternetError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + + func trackForgotPasswordClicked() { + analytics.forgotPasswordClicked() + } + +} diff --git a/Authorization/Authorization/Presentation/Startup/StartupView.swift b/Authorization/Authorization/Presentation/Startup/StartupView.swift index a13c3e3c8..5d762f98b 100644 --- a/Authorization/Authorization/Presentation/Startup/StartupView.swift +++ b/Authorization/Authorization/Presentation/Startup/StartupView.swift @@ -105,6 +105,8 @@ public struct StartupView: View { switch buttonAction { case .signIn: viewModel.router.showLoginScreen(sourceScreen: .startup) + case .signInWithSSO: + viewModel.router.showLoginScreen(sourceScreen: .startup) case .register: viewModel.router.showRegisterScreen(sourceScreen: .startup) } @@ -119,13 +121,16 @@ public struct StartupView: View { .frameLimit() } .navigationTitle(AuthLocalization.Startup.title) - .hideNavigationBar() + .navigationBarHidden(true) .padding(.all, isHorizontal ? 1 : 0) .background(Theme.Colors.background.ignoresSafeArea(.all)) .ignoresSafeArea(.keyboard, edges: .bottom) .onTapGesture { UIApplication.shared.endEditing() } + .onFirstAppear { + viewModel.trackScreenEvent() + } } } diff --git a/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift b/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift index 650ae5f7f..b4cf50091 100644 --- a/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift +++ b/Authorization/Authorization/Presentation/Startup/StartupViewModel.swift @@ -33,4 +33,8 @@ public class StartupViewModel: ObservableObject { analytics.trackEvent(.logistrationExploreAllCourses, biValue: .logistrationExploreAllCourses) } } + + func trackScreenEvent() { + analytics.trackScreenEvent(.logistration, biValue: .logistration) + } } diff --git a/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift b/Authorization/Authorization/SocialAuth/AppleAuthProvider.swift similarity index 98% rename from Core/Core/Providers/SocialAuth/AppleAuthProvider.swift rename to Authorization/Authorization/SocialAuth/AppleAuthProvider.swift index 1ed3d4c06..27afa234a 100644 --- a/Core/Core/Providers/SocialAuth/AppleAuthProvider.swift +++ b/Authorization/Authorization/SocialAuth/AppleAuthProvider.swift @@ -8,6 +8,8 @@ import Foundation import AuthenticationServices import Swinject +import OEXFoundation +import Core public final class AppleAuthProvider: NSObject, ASAuthorizationControllerDelegate { diff --git a/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift b/Authorization/Authorization/SocialAuth/Error/SocialAuthError.swift similarity index 97% rename from Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift rename to Authorization/Authorization/SocialAuth/Error/SocialAuthError.swift index a9167451e..27071bab7 100644 --- a/Core/Core/Providers/SocialAuth/Error/SocialAuthError.swift +++ b/Authorization/Authorization/SocialAuth/Error/SocialAuthError.swift @@ -6,6 +6,7 @@ // import Foundation +import Core public enum SocialAuthError: Error { case error(text: String) diff --git a/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift b/Authorization/Authorization/SocialAuth/FacebookAuthProvider.swift similarity index 99% rename from Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift rename to Authorization/Authorization/SocialAuth/FacebookAuthProvider.swift index 66ae46b6d..b5913bb25 100644 --- a/Core/Core/Providers/SocialAuth/FacebookAuthProvider.swift +++ b/Authorization/Authorization/SocialAuth/FacebookAuthProvider.swift @@ -7,6 +7,7 @@ import Foundation import FacebookLogin +import Core public final class FacebookAuthProvider { diff --git a/Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift b/Authorization/Authorization/SocialAuth/GoogleAuthProvider.swift similarity index 99% rename from Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift rename to Authorization/Authorization/SocialAuth/GoogleAuthProvider.swift index c600c1735..fb4336af2 100644 --- a/Core/Core/Providers/SocialAuth/GoogleAuthProvider.swift +++ b/Authorization/Authorization/SocialAuth/GoogleAuthProvider.swift @@ -7,6 +7,7 @@ import GoogleSignIn import Foundation +import Core public final class GoogleAuthProvider { diff --git a/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift b/Authorization/Authorization/SocialAuth/MicrosoftAuthProvider.swift similarity index 98% rename from Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift rename to Authorization/Authorization/SocialAuth/MicrosoftAuthProvider.swift index 16178b17c..813c755f1 100644 --- a/Core/Core/Providers/SocialAuth/MicrosoftAuthProvider.swift +++ b/Authorization/Authorization/SocialAuth/MicrosoftAuthProvider.swift @@ -8,6 +8,8 @@ import Foundation import MSAL import Swinject +import OEXFoundation +import Core public typealias MSLoginCompletionHandler = (account: MSALAccount, token: String) @@ -49,7 +51,7 @@ public final class MicrosoftAuthProvider { continuation.resume( returning: .success( SocialAuthResponse( - name: account.accountClaims?["name"] as? String ?? "" , + name: account.accountClaims?["name"] as? String ?? "", email: account.accountClaims?["email"] as? String ?? "", token: result.accessToken ) diff --git a/Core/Core/Providers/SocialAuth/SocialAuthResponse.swift b/Authorization/Authorization/SocialAuth/SocialAuthResponse.swift similarity index 100% rename from Core/Core/Providers/SocialAuth/SocialAuthResponse.swift rename to Authorization/Authorization/SocialAuth/SocialAuthResponse.swift diff --git a/Authorization/Authorization/SwiftGen/Strings.swift b/Authorization/Authorization/SwiftGen/Strings.swift index d139b4a0e..11eeb9a91 100644 --- a/Authorization/Authorization/SwiftGen/Strings.swift +++ b/Authorization/Authorization/SwiftGen/Strings.swift @@ -69,6 +69,14 @@ public enum AuthLocalization { public static let logInTitle = AuthLocalization.tr("Localizable", "SIGN_IN.LOG_IN_TITLE", fallback: "Sign in") /// Password public static let password = AuthLocalization.tr("Localizable", "SIGN_IN.PASSWORD", fallback: "Password") + /// Start today to build your career with confidence + public static let ssoHeading = AuthLocalization.tr("Localizable", "SIGN_IN.SSO_HEADING", fallback: "Start today to build your career with confidence") + /// Log in through the national unified sign-on service + public static let ssoLogInSubtitle = AuthLocalization.tr("Localizable", "SIGN_IN.SSO_LOG_IN_SUBTITLE", fallback: "Log in through the national unified sign-on service") + /// Sign in + public static let ssoLogInTitle = AuthLocalization.tr("Localizable", "SIGN_IN.SSO_LOG_IN_TITLE", fallback: "Sign in") + /// An integrated set of knowledge and empowerment programs to develop the components of the endowment sector and its workers + public static let ssoSupportingText = AuthLocalization.tr("Localizable", "SIGN_IN.SSO_SUPPORTING_TEXT", fallback: "An integrated set of knowledge and empowerment programs to develop the components of the endowment sector and its workers") /// Welcome back! Sign in to access your courses. public static let welcomeBack = AuthLocalization.tr("Localizable", "SIGN_IN.WELCOME_BACK", fallback: "Welcome back! Sign in to access your courses.") } diff --git a/Authorization/Authorization/en.lproj/Localizable.strings b/Authorization/Authorization/en.lproj/Localizable.strings index f15da07ce..5285b05e6 100644 --- a/Authorization/Authorization/en.lproj/Localizable.strings +++ b/Authorization/Authorization/en.lproj/Localizable.strings @@ -14,7 +14,10 @@ "SIGN_IN.FORGOT_PASS_BTN" = "Forgot password?"; "SIGN_IN.AGREEMENT" = "By signing in to this app, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data in accordance with the [Privacy Policy.](%@)"; - +"SIGN_IN.SSO_HEADING" = "Start today to build your career with confidence"; +"SIGN_IN.SSO_SUPPORTING_TEXT" = "An integrated set of knowledge and empowerment programs to develop the components of the endowment sector and its workers"; +"SIGN_IN.SSO_LOG_IN_TITLE" = "Sign in"; +"SIGN_IN.SSO_LOG_IN_SUBTITLE" = "Log in through the national unified sign-on service"; "ERROR.INVALID_EMAIL_ADDRESS" = "Invalid email address"; "ERROR.INVALID_PASSWORD_LENGHT" = "Invalid password lenght"; diff --git a/Authorization/Authorization/uk.lproj/Localizable.strings b/Authorization/Authorization/uk.lproj/Localizable.strings deleted file mode 100644 index 00ab874ce..000000000 --- a/Authorization/Authorization/uk.lproj/Localizable.strings +++ /dev/null @@ -1,49 +0,0 @@ -/* - Localizable.strings - Authorization - - Created by Vladimir Chekyrta on 13.09.2022. - -*/ - -"SIGN_IN.LOG_IN_TITLE" = "Увійти"; -"SIGN_IN.WELCOME_BACK" = "Welcome back! Sign in to access your courses."; -"SIGN_IN.EMAIL" = "Пошта"; -"SIGN_IN.PASSWORD" = "Пароль"; -"SIGN_IN.FORGOT_PASS_BTN" = "Забули пароль?"; -"SIGN_IN.AGREEMENT" = "By signing in to this app, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data in -accordance with the [Privacy Policy.](%@)"; - -"ERROR.INVALID_EMAIL_ADDRESS" = "невірна адреса електронної пошти"; -"ERROR.INVALID_PASSWORD_LENGHT" = "Пароль занадто короткий або занадто довгий"; -"ERROR.ACCOUNT_NOT_REGISTERED" = "This %@ account is not linked with any %@ account. Please register."; -"ERROR.DISABLED_ACCOUNT" = "Your account is disabled. Please contact customer support for assistance."; - -"SIGN_UP.SUBTITLE" = "Create an account to start learning today!"; -"SIGN_UP.CREATE_ACCOUNT_BTN" = "Створити акаунт"; -"SIGN_UP.HIDE_FIELDS" = "Приховати необовʼязкові поля"; -"SIGN_UP.SHOW_FIELDS" = "Показати необовʼязкові поля"; -"SIGN_UP.SUCCESS_SIGNIN_LABEL" = "You've successfully signed in."; -"SIGN_UP.SUCCESS_SIGNIN_SUBLABEL" = "We just need a little more information before you start learning."; -"SIGN_UP.AGREEMENT" = "By creating an account, you agree to the [%@ End User License Agreement](%@) and [%@ Terms of Service and Honor Code](%@) and you acknowledge that %@ and each Member process your personal data inaccordance with the [Privacy Policy.](%@)"; -"SIGN_UP.MARKETING_EMAIL_TITLE" = "I agree that %@ may send me marketing messages."; - -"FORGOT.TITLE"= "Відновлення паролю"; -"FORGOT.DESCRIPTION" = "Будь ласка, введіть свою адресу електронної пошти для входу або відновлення нижче, і ми надішлемо вам електронний лист з інструкціями."; -"FORGOT.REQUEST" = "Відновити пароль"; -"FORGOT.CHECK_TITLE" = "Перевірте свою електронну пошту"; -"FORGOT.CHECK_Description" = "Ми надіслали інструкції щодо відновлення пароля на вашу електронну пошту "; - -"SIGN_IN_WITH" = "Sign in with"; -"REGISTER_WITH" = "Register with"; -"APPLE" = "Apple"; -"GOOGLE" = "Google"; -"FACEBOOK" = "Facebook"; -"MICROSOFT" = "Microsoft"; -"OR" = "Or"; - -"STARTUP.INFO_MESSAGE" = "Courses and programs from the world's best universities in your pocket."; -"STARTUP.SEARCH_TITLE" = "What do you want to learn?"; -"STARTUP.SEARCH_PLACEHOLDER" = "Search our 3000+ courses"; -"STARTUP.EXPLORE_ALL_COURSES" = "Explore all courses"; -"STARTUP.TITLE" = "Start"; diff --git a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift index e1c3b12b5..31285ffcb 100644 --- a/Authorization/AuthorizationTests/AuthorizationMock.generated.swift +++ b/Authorization/AuthorizationTests/AuthorizationMock.generated.swift @@ -13,6 +13,7 @@ import Authorization import Foundation import SwiftUI import Combine +import OEXFoundation // MARK: - AuthInteractorProtocol @@ -93,6 +94,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + open func login(ssoToken: String) throws -> User { + addInvocation(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) + let perform = methodPerformValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) as? (String) -> Void + perform?(`ssoToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(ssoToken: String). Use given") + Failure("Stub return value not specified for login(ssoToken: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -174,6 +191,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) + case m_login__ssoToken_ssoToken(Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields @@ -194,6 +212,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) return Matcher.ComparisonResult(results) + case (.m_login__ssoToken_ssoToken(let lhsSsotoken), .m_login__ssoToken_ssoToken(let rhsSsotoken)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSsotoken, rhs: rhsSsotoken, with: matcher), lhsSsotoken, rhsSsotoken, "ssoToken")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -224,6 +247,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue + case let .m_login__ssoToken_ssoToken(p0): return p0.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 @@ -235,6 +259,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" + case .m_login__ssoToken_ssoToken: return ".login(ssoToken:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" @@ -261,6 +286,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func login(ssoToken: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -297,6 +325,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + public static func login(ssoToken: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func login(ssoToken: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -356,6 +394,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} @discardableResult public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} + public static func login(ssoToken: Parameter) -> Verify { return Verify(method: .m_login__ssoToken_ssoToken(`ssoToken`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} @@ -375,6 +414,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) } + public static func login(ssoToken: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_login__ssoToken_ssoToken(`ssoToken`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -569,6 +611,12 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { perform?(`success`) } + open func authTrackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_authTrackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_authTrackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + fileprivate enum MethodType { case m_identify__id_idusername_usernameemail_email(Parameter, Parameter, Parameter) @@ -581,6 +629,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { case m_forgotPasswordClicked case m_resetPasswordClicked case m_resetPassword__success_success(Parameter) + case m_authTrackScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -617,6 +666,12 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSuccess, rhs: rhsSuccess, with: matcher), lhsSuccess, rhsSuccess, "success")) return Matcher.ComparisonResult(results) + + case (.m_authTrackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_authTrackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -633,6 +688,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { case .m_forgotPasswordClicked: return 0 case .m_resetPasswordClicked: return 0 case let .m_resetPassword__success_success(p0): return p0.intValue + case let .m_authTrackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { @@ -647,6 +703,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { case .m_forgotPasswordClicked: return ".forgotPasswordClicked()" case .m_resetPasswordClicked: return ".resetPasswordClicked()" case .m_resetPassword__success_success: return ".resetPassword(success:)" + case .m_authTrackScreenEvent__eventbiValue_biValue: return ".authTrackScreenEvent(_:biValue:)" } } } @@ -675,6 +732,7 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { public static func forgotPasswordClicked() -> Verify { return Verify(method: .m_forgotPasswordClicked)} public static func resetPasswordClicked() -> Verify { return Verify(method: .m_resetPasswordClicked)} public static func resetPassword(success: Parameter) -> Verify { return Verify(method: .m_resetPassword__success_success(`success`))} + public static func authTrackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_authTrackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -711,6 +769,9 @@ open class AuthorizationAnalyticsMock: AuthorizationAnalytics, Mock { public static func resetPassword(success: Parameter, perform: @escaping (Bool) -> Void) -> Perform { return Perform(method: .m_resetPassword__success_success(`success`), performs: perform) } + public static func authTrackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_authTrackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } } public func given(_ method: Given) { @@ -908,6 +969,12 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -947,6 +1014,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showSSOWebBrowser__title_title(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -1012,6 +1080,11 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) return Matcher.ComparisonResult(results) + case (.m_showSSOWebBrowser__title_title(let lhsTitle), .m_showSSOWebBrowser__title_title(let rhsTitle)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -1066,6 +1139,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showSSOWebBrowser__title_title(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -1087,6 +1161,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showSSOWebBrowser__title_title: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -1122,6 +1197,7 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} + public static func showSSOWebBrowser(title: Parameter) -> Verify { return Verify(method: .m_showSSOWebBrowser__title_title(`title`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`))} @@ -1171,6 +1247,9 @@ open class AuthorizationRouterMock: AuthorizationRouter, Mock { public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showSSOWebBrowser__title_title(`title`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -1374,6 +1453,12 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -1412,6 +1497,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showSSOWebBrowser__title_title(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -1472,6 +1558,11 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) return Matcher.ComparisonResult(results) + case (.m_showSSOWebBrowser__title_title(let lhsTitle), .m_showSSOWebBrowser__title_title(let rhsTitle)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -1525,6 +1616,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showSSOWebBrowser__title_title(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -1545,6 +1637,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showSSOWebBrowser__title_title: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -1579,6 +1672,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} + public static func showSSOWebBrowser(title: Parameter) -> Verify { return Verify(method: .m_showSSOWebBrowser__title_title(`title`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`))} @@ -1625,6 +1719,9 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showSSOWebBrowser__title_title(`title`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -1712,9 +1809,9 @@ open class BaseRouterMock: BaseRouter, Mock { } } -// MARK: - ConnectivityProtocol +// MARK: - CalendarManagerProtocol -open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { +open class CalendarManagerProtocolMock: CalendarManagerProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -1752,51 +1849,176 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } - public var isInternetAvaliable: Bool { - get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } - } - private var __p_isInternetAvaliable: (Bool)? - public var isMobileData: Bool { - get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } - } - private var __p_isMobileData: (Bool)? - public var internetReachableSubject: CurrentValueSubject { - get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } - } - private var __p_internetReachableSubject: (CurrentValueSubject)? + open func createCalendarIfNeeded() { + addInvocation(.m_createCalendarIfNeeded) + let perform = methodPerformValue(.m_createCalendarIfNeeded) as? () -> Void + perform?() + } + + open func filterCoursesBySelected(fetchedCourses: [CourseForSync]) -> [CourseForSync] { + addInvocation(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) + let perform = methodPerformValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) as? ([CourseForSync]) -> Void + perform?(`fetchedCourses`) + var __value: [CourseForSync] + do { + __value = try methodReturnValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))).casted() + } catch { + onFatalFailure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + Failure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + } + return __value + } + + open func removeOldCalendar() { + addInvocation(.m_removeOldCalendar) + let perform = methodPerformValue(.m_removeOldCalendar) as? () -> Void + perform?() + } + + open func removeOutdatedEvents(courseID: String) { + addInvocation(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + } + + open func syncCourse(courseID: String, courseName: String, dates: CourseDates) { + addInvocation(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) + let perform = methodPerformValue(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) as? (String, String, CourseDates) -> Void + perform?(`courseID`, `courseName`, `dates`) + } + + open func requestAccess() -> Bool { + addInvocation(.m_requestAccess) + let perform = methodPerformValue(.m_requestAccess) as? () -> Void + perform?() + var __value: Bool + do { + __value = try methodReturnValue(.m_requestAccess).casted() + } catch { + onFatalFailure("Stub return value not specified for requestAccess(). Use given") + Failure("Stub return value not specified for requestAccess(). Use given") + } + return __value + } + + open func courseStatus(courseID: String) -> SyncStatus { + addInvocation(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: SyncStatus + do { + __value = try methodReturnValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch { + onFatalFailure("Stub return value not specified for courseStatus(courseID: String). Use given") + Failure("Stub return value not specified for courseStatus(courseID: String). Use given") + } + return __value + } + open func clearAllData(removeCalendar: Bool) { + addInvocation(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) + let perform = methodPerformValue(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) as? (Bool) -> Void + perform?(`removeCalendar`) + } + open func isDatesChanged(courseID: String, checksum: String) -> Bool { + addInvocation(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) + let perform = methodPerformValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) as? (String, String) -> Void + perform?(`courseID`, `checksum`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + Failure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + } + return __value + } fileprivate enum MethodType { - case p_isInternetAvaliable_get - case p_isMobileData_get - case p_internetReachableSubject_get + case m_createCalendarIfNeeded + case m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>) + case m_removeOldCalendar + case m_removeOutdatedEvents__courseID_courseID(Parameter) + case m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter, Parameter, Parameter) + case m_requestAccess + case m_courseStatus__courseID_courseID(Parameter) + case m_clearAllData__removeCalendar_removeCalendar(Parameter) + case m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match - case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match - case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + switch (lhs, rhs) { + case (.m_createCalendarIfNeeded, .m_createCalendarIfNeeded): return .match + + case (.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let lhsFetchedcourses), .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let rhsFetchedcourses)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFetchedcourses, rhs: rhsFetchedcourses, with: matcher), lhsFetchedcourses, rhsFetchedcourses, "fetchedCourses")) + return Matcher.ComparisonResult(results) + + case (.m_removeOldCalendar, .m_removeOldCalendar): return .match + + case (.m_removeOutdatedEvents__courseID_courseID(let lhsCourseid), .m_removeOutdatedEvents__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let lhsCourseid, let lhsCoursename, let lhsDates), .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let rhsCourseid, let rhsCoursename, let rhsDates)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDates, rhs: rhsDates, with: matcher), lhsDates, rhsDates, "dates")) + return Matcher.ComparisonResult(results) + + case (.m_requestAccess, .m_requestAccess): return .match + + case (.m_courseStatus__courseID_courseID(let lhsCourseid), .m_courseStatus__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_clearAllData__removeCalendar_removeCalendar(let lhsRemovecalendar), .m_clearAllData__removeCalendar_removeCalendar(let rhsRemovecalendar)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRemovecalendar, rhs: rhsRemovecalendar, with: matcher), lhsRemovecalendar, rhsRemovecalendar, "removeCalendar")) + return Matcher.ComparisonResult(results) + + case (.m_isDatesChanged__courseID_courseIDchecksum_checksum(let lhsCourseid, let lhsChecksum), .m_isDatesChanged__courseID_courseIDchecksum_checksum(let rhsCourseid, let rhsChecksum)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsChecksum, rhs: rhsChecksum, with: matcher), lhsChecksum, rhsChecksum, "checksum")) + return Matcher.ComparisonResult(results) default: return .none } } func intValue() -> Int { switch self { - case .p_isInternetAvaliable_get: return 0 - case .p_isMobileData_get: return 0 - case .p_internetReachableSubject_get: return 0 + case .m_createCalendarIfNeeded: return 0 + case let .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(p0): return p0.intValue + case .m_removeOldCalendar: return 0 + case let .m_removeOutdatedEvents__courseID_courseID(p0): return p0.intValue + case let .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case .m_requestAccess: return 0 + case let .m_courseStatus__courseID_courseID(p0): return p0.intValue + case let .m_clearAllData__removeCalendar_removeCalendar(p0): return p0.intValue + case let .m_isDatesChanged__courseID_courseIDchecksum_checksum(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { - case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" - case .p_isMobileData_get: return "[get] .isMobileData" - case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + case .m_createCalendarIfNeeded: return ".createCalendarIfNeeded()" + case .m_filterCoursesBySelected__fetchedCourses_fetchedCourses: return ".filterCoursesBySelected(fetchedCourses:)" + case .m_removeOldCalendar: return ".removeOldCalendar()" + case .m_removeOutdatedEvents__courseID_courseID: return ".removeOutdatedEvents(courseID:)" + case .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates: return ".syncCourse(courseID:courseName:dates:)" + case .m_requestAccess: return ".requestAccess()" + case .m_courseStatus__courseID_courseID: return ".courseStatus(courseID:)" + case .m_clearAllData__removeCalendar_removeCalendar: return ".clearAllData(removeCalendar:)" + case .m_isDatesChanged__courseID_courseIDchecksum_checksum: return ".isDatesChanged(courseID:checksum:)" } } } @@ -1809,30 +2031,94 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { super.init(products) } - public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willReturn: [CourseForSync]...) -> MethodStub { + return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func requestAccess(willReturn: Bool...) -> MethodStub { + return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { - return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func courseStatus(courseID: Parameter, willReturn: SyncStatus...) -> MethodStub { + return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willProduce: (Stubber<[CourseForSync]>) -> Void) -> MethodStub { + let willReturn: [[CourseForSync]] = [] + let given: Given = { return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseForSync]).self) + willProduce(stubber) + return given + } + public static func requestAccess(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given + } + public static func courseStatus(courseID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [SyncStatus] = [] + let given: Given = { return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (SyncStatus).self) + willProduce(stubber) + return given + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given } - } public struct Verify { fileprivate var method: MethodType - public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } - public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } - public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + public static func createCalendarIfNeeded() -> Verify { return Verify(method: .m_createCalendarIfNeeded)} + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>) -> Verify { return Verify(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`))} + public static func removeOldCalendar() -> Verify { return Verify(method: .m_removeOldCalendar)} + public static func removeOutdatedEvents(courseID: Parameter) -> Verify { return Verify(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`))} + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter) -> Verify { return Verify(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`))} + public static func requestAccess() -> Verify { return Verify(method: .m_requestAccess)} + public static func courseStatus(courseID: Parameter) -> Verify { return Verify(method: .m_courseStatus__courseID_courseID(`courseID`))} + public static func clearAllData(removeCalendar: Parameter) -> Verify { return Verify(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`))} + public static func isDatesChanged(courseID: Parameter, checksum: Parameter) -> Verify { return Verify(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`))} } public struct Perform { fileprivate var method: MethodType var performs: Any + public static func createCalendarIfNeeded(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_createCalendarIfNeeded, performs: perform) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, perform: @escaping ([CourseForSync]) -> Void) -> Perform { + return Perform(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), performs: perform) + } + public static func removeOldCalendar(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeOldCalendar, performs: perform) + } + public static func removeOutdatedEvents(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`), performs: perform) + } + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter, perform: @escaping (String, String, CourseDates) -> Void) -> Perform { + return Perform(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`), performs: perform) + } + public static func requestAccess(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_requestAccess, performs: perform) + } + public static func courseStatus(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_courseStatus__courseID_courseID(`courseID`), performs: perform) + } + public static func clearAllData(removeCalendar: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`), performs: perform) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), performs: perform) + } } public func given(_ method: Given) { @@ -1908,9 +2194,9 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } -// MARK: - CoreAnalytics +// MARK: - ConnectivityProtocol -open class CoreAnalyticsMock: CoreAnalytics, Mock { +open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -1948,72 +2234,309 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var isInternetAvaliable: Bool { + get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } + } + private var __p_isInternetAvaliable: (Bool)? + public var isMobileData: Bool { + get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } + } + private var __p_isMobileData: (Bool)? + public var internetReachableSubject: CurrentValueSubject { + get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } + } + private var __p_internetReachableSubject: (CurrentValueSubject)? - open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void - perform?(`event`, `parameters`) - } - - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void - perform?(`event`, `biValue`, `parameters`) - } - - open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { - addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) - let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void - perform?(`event`, `biValue`, `action`, `rating`) - } - - open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { - addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) - let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void - perform?(`event`, `bivalue`, `value`, `oldValue`) - } - open func trackEvent(_ event: AnalyticsEvent) { - addInvocation(.m_trackEvent__event(Parameter.value(`event`))) - let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void - perform?(`event`) - } - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void - perform?(`event`, `biValue`) - } fileprivate enum MethodType { - case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) - case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) - case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) - case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) - case m_trackEvent__event(Parameter) - case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case p_isInternetAvaliable_get + case p_isMobileData_get + case p_internetReachableSubject_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { - case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match + case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match + case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + default: return .none + } + } - case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + func intValue() -> Int { + switch self { + case .p_isInternetAvaliable_get: return 0 + case .p_isMobileData_get: return 0 + case .p_internetReachableSubject_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" + case .p_isMobileData_get: return "[get] .isMobileData" + case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + } + } + } - case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): - var results: [Matcher.ParameterComparisonResult] = [] + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { + return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } + public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } + public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreAnalytics + +open class CoreAnalyticsMock: CoreAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { + addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) + let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void + perform?(`event`, `biValue`, `action`, `rating`) + } + + open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { + addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) + let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void + perform?(`event`, `bivalue`, `value`, `oldValue`) + } + + open func trackEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + + fileprivate enum MethodType { + case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) + case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) + case m_trackEvent__event(Parameter) + case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): + var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) @@ -2038,6 +2561,17 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) default: return .none } } @@ -2046,67 +2580,696 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { switch self { case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_trackEvent__event(p0): return p0.intValue case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" + case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" + case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" + case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" + case .m_trackEvent__event: return ".trackEvent(_:)" + case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" } } - func assertionName() -> String { - switch self { - case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" - case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" - case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" - case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" - case .m_trackEvent__event: return ".trackEvent(_:)" - case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" - } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} + public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { + return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) + } + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { + return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) + } + public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackEvent__event(`event`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CorePersistenceProtocol + +open class CorePersistenceProtocolMock: CorePersistenceProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func set(userId: Int) { + addInvocation(.m_set__userId_userId(Parameter.value(`userId`))) + let perform = methodPerformValue(.m_set__userId_userId(Parameter.value(`userId`))) as? (Int) -> Void + perform?(`userId`) + } + + open func getUserID() -> Int? { + addInvocation(.m_getUserID) + let perform = methodPerformValue(.m_getUserID) as? () -> Void + perform?() + var __value: Int? = nil + do { + __value = try methodReturnValue(.m_getUserID).casted() + } catch { + // do nothing + } + return __value + } + + open func publisher() -> AnyPublisher { + addInvocation(.m_publisher) + let perform = methodPerformValue(.m_publisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_publisher).casted() + } catch { + onFatalFailure("Stub return value not specified for publisher(). Use given") + Failure("Stub return value not specified for publisher(). Use given") + } + return __value + } + + open func addToDownloadQueue(tasks: [DownloadDataTask]) { + addInvocation(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) as? ([DownloadDataTask]) -> Void + perform?(`tasks`) + } + + open func saveOfflineProgress(progress: OfflineProgress) { + addInvocation(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) + let perform = methodPerformValue(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) as? (OfflineProgress) -> Void + perform?(`progress`) + } + + open func loadProgress(for blockID: String) -> OfflineProgress? { + addInvocation(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + var __value: OfflineProgress? = nil + do { + __value = try methodReturnValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))).casted() + } catch { + // do nothing + } + return __value + } + + open func loadAllOfflineProgress() -> [OfflineProgress] { + addInvocation(.m_loadAllOfflineProgress) + let perform = methodPerformValue(.m_loadAllOfflineProgress) as? () -> Void + perform?() + var __value: [OfflineProgress] + do { + __value = try methodReturnValue(.m_loadAllOfflineProgress).casted() + } catch { + onFatalFailure("Stub return value not specified for loadAllOfflineProgress(). Use given") + Failure("Stub return value not specified for loadAllOfflineProgress(). Use given") + } + return __value + } + + open func deleteProgress(for blockID: String) { + addInvocation(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + } + + open func deleteAllProgress() { + addInvocation(.m_deleteAllProgress) + let perform = methodPerformValue(.m_deleteAllProgress) as? () -> Void + perform?() + } + + open func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) { + addInvocation(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) as? ([CourseBlock], DownloadQuality) -> Void + perform?(`blocks`, `downloadQuality`) + } + + open func nextBlockForDownloading() -> DownloadDataTask? { + addInvocation(.m_nextBlockForDownloading) + let perform = methodPerformValue(.m_nextBlockForDownloading) as? () -> Void + perform?() + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_nextBlockForDownloading).casted() + } catch { + // do nothing + } + return __value + } + + open func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) { + addInvocation(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) + let perform = methodPerformValue(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) as? (String, DownloadState, Data?) -> Void + perform?(`id`, `state`, `resumeData`) + } + + open func deleteDownloadDataTask(id: String) throws { + addInvocation(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) + let perform = methodPerformValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) as? (String) -> Void + perform?(`id`) + do { + _ = try methodReturnValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func saveDownloadDataTask(_ task: DownloadDataTask) { + addInvocation(.m_saveDownloadDataTask__task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_saveDownloadDataTask__task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) + } + + open func downloadDataTask(for blockId: String) -> DownloadDataTask? { + addInvocation(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) + let perform = methodPerformValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void + perform?(`blockId`) + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))).casted() + } catch { + // do nothing + } + return __value + } + + open func getDownloadDataTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasks) + let perform = methodPerformValue(.m_getDownloadDataTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasks(). Use given") + Failure("Stub return value not specified for getDownloadDataTasks(). Use given") + } + return __value + } + + open func getDownloadDataTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + } + return __value + } + + + fileprivate enum MethodType { + case m_set__userId_userId(Parameter) + case m_getUserID + case m_publisher + case m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>) + case m_saveOfflineProgress__progress_progress(Parameter) + case m_loadProgress__for_blockID(Parameter) + case m_loadAllOfflineProgress + case m_deleteProgress__for_blockID(Parameter) + case m_deleteAllProgress + case m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>, Parameter) + case m_nextBlockForDownloading + case m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter, Parameter, Parameter) + case m_deleteDownloadDataTask__id_id(Parameter) + case m_saveDownloadDataTask__task(Parameter) + case m_downloadDataTask__for_blockId(Parameter) + case m_getDownloadDataTasks + case m_getDownloadDataTasksForCourse__courseId(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_set__userId_userId(let lhsUserid), .m_set__userId_userId(let rhsUserid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUserid, rhs: rhsUserid, with: matcher), lhsUserid, rhsUserid, "userId")) + return Matcher.ComparisonResult(results) + + case (.m_getUserID, .m_getUserID): return .match + + case (.m_publisher, .m_publisher): return .match + + case (.m_addToDownloadQueue__tasks_tasks(let lhsTasks), .m_addToDownloadQueue__tasks_tasks(let rhsTasks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTasks, rhs: rhsTasks, with: matcher), lhsTasks, rhsTasks, "tasks")) + return Matcher.ComparisonResult(results) + + case (.m_saveOfflineProgress__progress_progress(let lhsProgress), .m_saveOfflineProgress__progress_progress(let rhsProgress)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsProgress, rhs: rhsProgress, with: matcher), lhsProgress, rhsProgress, "progress")) + return Matcher.ComparisonResult(results) + + case (.m_loadProgress__for_blockID(let lhsBlockid), .m_loadProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_loadAllOfflineProgress, .m_loadAllOfflineProgress): return .match + + case (.m_deleteProgress__for_blockID(let lhsBlockid), .m_deleteProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_deleteAllProgress, .m_deleteAllProgress): return .match + + case (.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let lhsBlocks, let lhsDownloadquality), .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let rhsBlocks, let rhsDownloadquality)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDownloadquality, rhs: rhsDownloadquality, with: matcher), lhsDownloadquality, rhsDownloadquality, "downloadQuality")) + return Matcher.ComparisonResult(results) + + case (.m_nextBlockForDownloading, .m_nextBlockForDownloading): return .match + + case (.m_updateDownloadState__id_idstate_stateresumeData_resumeData(let lhsId, let lhsState, let lhsResumedata), .m_updateDownloadState__id_idstate_stateresumeData_resumeData(let rhsId, let rhsState, let rhsResumedata)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsState, rhs: rhsState, with: matcher), lhsState, rhsState, "state")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResumedata, rhs: rhsResumedata, with: matcher), lhsResumedata, rhsResumedata, "resumeData")) + return Matcher.ComparisonResult(results) + + case (.m_deleteDownloadDataTask__id_id(let lhsId), .m_deleteDownloadDataTask__id_id(let rhsId)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + return Matcher.ComparisonResult(results) + + case (.m_saveDownloadDataTask__task(let lhsTask), .m_saveDownloadDataTask__task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "_ task")) + return Matcher.ComparisonResult(results) + + case (.m_downloadDataTask__for_blockId(let lhsBlockid), .m_downloadDataTask__for_blockId(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) + return Matcher.ComparisonResult(results) + + case (.m_getDownloadDataTasks, .m_getDownloadDataTasks): return .match + + case (.m_getDownloadDataTasksForCourse__courseId(let lhsCourseid), .m_getDownloadDataTasksForCourse__courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_set__userId_userId(p0): return p0.intValue + case .m_getUserID: return 0 + case .m_publisher: return 0 + case let .m_addToDownloadQueue__tasks_tasks(p0): return p0.intValue + case let .m_saveOfflineProgress__progress_progress(p0): return p0.intValue + case let .m_loadProgress__for_blockID(p0): return p0.intValue + case .m_loadAllOfflineProgress: return 0 + case let .m_deleteProgress__for_blockID(p0): return p0.intValue + case .m_deleteAllProgress: return 0 + case let .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(p0, p1): return p0.intValue + p1.intValue + case .m_nextBlockForDownloading: return 0 + case let .m_updateDownloadState__id_idstate_stateresumeData_resumeData(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_deleteDownloadDataTask__id_id(p0): return p0.intValue + case let .m_saveDownloadDataTask__task(p0): return p0.intValue + case let .m_downloadDataTask__for_blockId(p0): return p0.intValue + case .m_getDownloadDataTasks: return 0 + case let .m_getDownloadDataTasksForCourse__courseId(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_set__userId_userId: return ".set(userId:)" + case .m_getUserID: return ".getUserID()" + case .m_publisher: return ".publisher()" + case .m_addToDownloadQueue__tasks_tasks: return ".addToDownloadQueue(tasks:)" + case .m_saveOfflineProgress__progress_progress: return ".saveOfflineProgress(progress:)" + case .m_loadProgress__for_blockID: return ".loadProgress(for:)" + case .m_loadAllOfflineProgress: return ".loadAllOfflineProgress()" + case .m_deleteProgress__for_blockID: return ".deleteProgress(for:)" + case .m_deleteAllProgress: return ".deleteAllProgress()" + case .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality: return ".addToDownloadQueue(blocks:downloadQuality:)" + case .m_nextBlockForDownloading: return ".nextBlockForDownloading()" + case .m_updateDownloadState__id_idstate_stateresumeData_resumeData: return ".updateDownloadState(id:state:resumeData:)" + case .m_deleteDownloadDataTask__id_id: return ".deleteDownloadDataTask(id:)" + case .m_saveDownloadDataTask__task: return ".saveDownloadDataTask(_:)" + case .m_downloadDataTask__for_blockId: return ".downloadDataTask(for:)" + case .m_getDownloadDataTasks: return ".getDownloadDataTasks()" + case .m_getDownloadDataTasksForCourse__courseId: return ".getDownloadDataTasksForCourse(_:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func getUserID(willReturn: Int?...) -> MethodStub { + return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func publisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadProgress(for blockID: Parameter, willReturn: OfflineProgress?...) -> MethodStub { + return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadAllOfflineProgress(willReturn: [OfflineProgress]...) -> MethodStub { + return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func nextBlockForDownloading(willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func downloadDataTask(for blockId: Parameter, willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getUserID(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Int?] = [] + let given: Given = { return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Int?).self) + willProduce(stubber) + return given + } + public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func loadProgress(for blockID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [OfflineProgress?] = [] + let given: Given = { return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (OfflineProgress?).self) + willProduce(stubber) + return given + } + public static func loadAllOfflineProgress(willProduce: (Stubber<[OfflineProgress]>) -> Void) -> MethodStub { + let willReturn: [[OfflineProgress]] = [] + let given: Given = { return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([OfflineProgress]).self) + willProduce(stubber) + return given + } + public static func nextBlockForDownloading(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func downloadDataTask(for blockId: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given } - } - - open class Given: StubbedMethod { - fileprivate var method: MethodType - - private init(method: MethodType, products: [StubProduct]) { - self.method = method - super.init(products) + public static func deleteDownloadDataTask(id: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func deleteDownloadDataTask(id: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given } - - } public struct Verify { fileprivate var method: MethodType - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} - public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func set(userId: Parameter) -> Verify { return Verify(method: .m_set__userId_userId(`userId`))} + public static func getUserID() -> Verify { return Verify(method: .m_getUserID)} + public static func publisher() -> Verify { return Verify(method: .m_publisher)} + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>) -> Verify { return Verify(method: .m_addToDownloadQueue__tasks_tasks(`tasks`))} + public static func saveOfflineProgress(progress: Parameter) -> Verify { return Verify(method: .m_saveOfflineProgress__progress_progress(`progress`))} + public static func loadProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_loadProgress__for_blockID(`blockID`))} + public static func loadAllOfflineProgress() -> Verify { return Verify(method: .m_loadAllOfflineProgress)} + public static func deleteProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_deleteProgress__for_blockID(`blockID`))} + public static func deleteAllProgress() -> Verify { return Verify(method: .m_deleteAllProgress)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`))} + public static func nextBlockForDownloading() -> Verify { return Verify(method: .m_nextBlockForDownloading)} + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter) -> Verify { return Verify(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`))} + public static func deleteDownloadDataTask(id: Parameter) -> Verify { return Verify(method: .m_deleteDownloadDataTask__id_id(`id`))} + public static func saveDownloadDataTask(_ task: Parameter) -> Verify { return Verify(method: .m_saveDownloadDataTask__task(`task`))} + public static func downloadDataTask(for blockId: Parameter) -> Verify { return Verify(method: .m_downloadDataTask__for_blockId(`blockId`))} + public static func getDownloadDataTasks() -> Verify { return Verify(method: .m_getDownloadDataTasks)} + public static func getDownloadDataTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`))} } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + public static func set(userId: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_set__userId_userId(`userId`), performs: perform) } - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + public static func getUserID(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getUserID, performs: perform) } - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { - return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) + public static func publisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_publisher, performs: perform) } - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { - return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>, perform: @escaping ([DownloadDataTask]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__tasks_tasks(`tasks`), performs: perform) } - public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { - return Perform(method: .m_trackEvent__event(`event`), performs: perform) + public static func saveOfflineProgress(progress: Parameter, perform: @escaping (OfflineProgress) -> Void) -> Perform { + return Perform(method: .m_saveOfflineProgress__progress_progress(`progress`), performs: perform) } - public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + public static func loadProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_loadProgress__for_blockID(`blockID`), performs: perform) + } + public static func loadAllOfflineProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_loadAllOfflineProgress, performs: perform) + } + public static func deleteProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteProgress__for_blockID(`blockID`), performs: perform) + } + public static func deleteAllProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteAllProgress, performs: perform) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter, perform: @escaping ([CourseBlock], DownloadQuality) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`), performs: perform) + } + public static func nextBlockForDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_nextBlockForDownloading, performs: perform) + } + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter, perform: @escaping (String, DownloadState, Data?) -> Void) -> Perform { + return Perform(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`), performs: perform) + } + public static func deleteDownloadDataTask(id: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteDownloadDataTask__id_id(`id`), performs: perform) + } + public static func saveDownloadDataTask(_ task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_saveDownloadDataTask__task(`task`), performs: perform) + } + public static func downloadDataTask(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_downloadDataTask__for_blockId(`blockId`), performs: perform) + } + public static func getDownloadDataTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasks, performs: perform) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), performs: perform) } } @@ -2378,6 +3541,20 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func updateUnzippedFileSize(for sequentials: [CourseSequential]) -> [CourseSequential] { + addInvocation(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) + let perform = methodPerformValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) as? ([CourseSequential]) -> Void + perform?(`sequentials`) + var __value: [CourseSequential] + do { + __value = try methodReturnValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))).casted() + } catch { + onFatalFailure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + Failure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + } + return __value + } + open func resumeDownloading() throws { addInvocation(.m_resumeDownloading) let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void @@ -2405,6 +3582,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func removeAppSupportDirectoryUnusedContent() { + addInvocation(.m_removeAppSupportDirectoryUnusedContent) + let perform = methodPerformValue(.m_removeAppSupportDirectoryUnusedContent) as? () -> Void + perform?() + } + fileprivate enum MethodType { case m_publisher @@ -2419,8 +3602,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>) case m_resumeDownloading case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_removeAppSupportDirectoryUnusedContent case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2471,12 +3656,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + case (.m_updateUnzippedFileSize__for_sequentials(let lhsSequentials), .m_updateUnzippedFileSize__for_sequentials(let rhsSequentials)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSequentials, rhs: rhsSequentials, with: matcher), lhsSequentials, rhsSequentials, "for sequentials")) + return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) + + case (.m_removeAppSupportDirectoryUnusedContent, .m_removeAppSupportDirectoryUnusedContent): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -2496,8 +3688,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_updateUnzippedFileSize__for_sequentials(p0): return p0.intValue case .m_resumeDownloading: return 0 case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_removeAppSupportDirectoryUnusedContent: return 0 case .p_currentDownloadTask_get: return 0 } } @@ -2515,8 +3709,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_updateUnzippedFileSize__for_sequentials: return ".updateUnzippedFileSize(for:)" case .m_resumeDownloading: return ".resumeDownloading()" case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_removeAppSupportDirectoryUnusedContent: return ".removeAppSupportDirectoryUnusedContent()" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -2549,6 +3745,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willReturn: [CourseSequential]...) -> MethodStub { + return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -2587,6 +3786,13 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willProduce: (Stubber<[CourseSequential]>) -> Void) -> MethodStub { + let willReturn: [[CourseSequential]] = [] + let given: Given = { return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseSequential]).self) + willProduce(stubber) + return given + } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { let willReturn: [Bool] = [] let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -2671,8 +3877,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>) -> Verify { return Verify(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func removeAppSupportDirectoryUnusedContent() -> Verify { return Verify(method: .m_removeAppSupportDirectoryUnusedContent)} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -2716,12 +3924,217 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, perform: @escaping ([CourseSequential]) -> Void) -> Perform { + return Perform(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), performs: perform) + } public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } + public static func removeAppSupportDirectoryUnusedContent(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeAppSupportDirectoryUnusedContent, performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - OfflineSyncInteractorProtocol + +open class OfflineSyncInteractorProtocolMock: OfflineSyncInteractorProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func submitOfflineProgress(courseID: String, blockID: String, data: String) throws -> Bool { + addInvocation(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) + let perform = methodPerformValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) as? (String, String, String) -> Void + perform?(`courseID`, `blockID`, `data`) + var __value: Bool + do { + __value = try methodReturnValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + Failure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + } catch { + throw error + } + return __value + } + + + fileprivate enum MethodType { + case m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter, Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let lhsCourseid, let lhsBlockid, let lhsData), .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let rhsCourseid, let rhsBlockid, let rhsData)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsData, rhs: rhsData, with: matcher), lhsData, rhsData, "data")) + return Matcher.ComparisonResult(results) + } + } + + func intValue() -> Int { + switch self { + case let .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + } + } + func assertionName() -> String { + switch self { + case .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data: return ".submitOfflineProgress(courseID:blockID:data:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Bool).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter) -> Verify { return Verify(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { + return Perform(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), performs: perform) + } } public func given(_ method: Given) { diff --git a/Authorization/AuthorizationTests/Presentation/Login/ResetPasswordViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Login/ResetPasswordViewModelTests.swift index 6b09633f2..e5ffa55e7 100644 --- a/Authorization/AuthorizationTests/Presentation/Login/ResetPasswordViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Login/ResetPasswordViewModelTests.swift @@ -9,6 +9,7 @@ import SwiftyMocky import XCTest @testable import Core @testable import Authorization +import OEXFoundation import Alamofire import SwiftUI diff --git a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift index e824ad975..c91d48adc 100644 --- a/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Login/SignInViewModelTests.swift @@ -9,6 +9,7 @@ import SwiftyMocky import XCTest @testable import Core @testable import Authorization +import OEXFoundation import Alamofire import SwiftUI @@ -93,7 +94,33 @@ final class SignInViewModelTests: XCTestCase { XCTAssertEqual(viewModel.errorMessage, nil) XCTAssertEqual(viewModel.isShowProgress, true) } - + + func testSSOLoginSuccess() async throws { + let interactor = AuthInteractorProtocolMock() + let router = AuthorizationRouterMock() + let validator = Validator() + let analytics = AuthorizationAnalyticsMock() + let viewModel = SignInViewModel( + interactor: interactor, + router: router, + config: ConfigMock(), + analytics: analytics, + validator: validator, + sourceScreen: .default + ) + let user = User(id: 1, username: "username", email: "edxUser@edx.com", name: "Name", userAvatar: "") + + Given(interactor, .login(ssoToken: .any, willReturn: user)) + + await viewModel.ssoLogin(title: "Riyadah") + + Verify(interactor, 1, .login(ssoToken: .any)) + Verify(router, 1, .showMainOrWhatsNewScreen(sourceScreen: .any)) + + XCTAssertEqual(viewModel.errorMessage, nil) + XCTAssertEqual(viewModel.isShowProgress, true) + } + func testSocialLoginSuccess() async throws { let interactor = AuthInteractorProtocolMock() let router = AuthorizationRouterMock() diff --git a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift index ad180a925..7e8236d27 100644 --- a/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift +++ b/Authorization/AuthorizationTests/Presentation/Register/SignUpViewModelTests.swift @@ -9,6 +9,7 @@ import SwiftyMocky import XCTest @testable import Core @testable import Authorization +import OEXFoundation import Alamofire import SwiftUI diff --git a/Authorization/Mockfile b/Authorization/Mockfile index 5e0805e28..a3ae4b4f7 100644 --- a/Authorization/Mockfile +++ b/Authorization/Mockfile @@ -14,4 +14,5 @@ unit.tests.mock: - Authorization - Foundation - SwiftUI - - Combine \ No newline at end of file + - Combine + - OEXFoundation \ No newline at end of file diff --git a/Core/Core.xcodeproj/project.pbxproj b/Core/Core.xcodeproj/project.pbxproj index f718ae857..d1a063a0e 100644 --- a/Core/Core.xcodeproj/project.pbxproj +++ b/Core/Core.xcodeproj/project.pbxproj @@ -14,8 +14,9 @@ 021D924828DC860C00ACC565 /* Data_UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924728DC860C00ACC565 /* Data_UserProfile.swift */; }; 021D925028DC89D100ACC565 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924F28DC89D100ACC565 /* UserProfile.swift */; }; 021D925728DCF12900ACC565 /* AlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D925628DCF12900ACC565 /* AlertView.swift */; }; + 022020462C11BB2200D15795 /* Data_CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022020452C11BB2200D15795 /* Data_CourseDates.swift */; }; 02280F5B294B4E6F0032823A /* Connectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5A294B4E6F0032823A /* Connectivity.swift */; }; - 02284C182A3B1AE00007117F /* UIApplicationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */; }; + 02286D162C106393005EEC8D /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02286D152C106393005EEC8D /* CourseDates.swift */; }; 022C64E429AE0191000F532B /* TextWithUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64E329AE0191000F532B /* TextWithUrls.swift */; }; 0231CDBE2922422D00032416 /* CSSInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0231CDBD2922422D00032416 /* CSSInjector.swift */; }; 0233D56F2AF13EB200BAC8BD /* StarRatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0233D56E2AF13EB200BAC8BD /* StarRatingView.swift */; }; @@ -31,16 +32,15 @@ 023A4DD4299E66BD006C0E48 /* OfflineSnackBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023A4DD3299E66BD006C0E48 /* OfflineSnackBarView.swift */; }; 0241666B28F5A78B00082765 /* HTMLFormattedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0241666A28F5A78B00082765 /* HTMLFormattedText.swift */; }; 0248C92329C075EF00DC8402 /* CourseBlockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248C92229C075EF00DC8402 /* CourseBlockModel.swift */; }; - 024BE3DF29B2615500BCDEE2 /* CGColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024BE3DE29B2615500BCDEE2 /* CGColorExtension.swift */; }; 024D723529C8BB1A006D36ED /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024D723429C8BB1A006D36ED /* NavigationBar.swift */; }; 024FCD0028EF1CD300232339 /* WebBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024FCCFF28EF1CD300232339 /* WebBrowser.swift */; }; 02512FF0299533DF0024D438 /* CoreDataHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02512FEF299533DE0024D438 /* CoreDataHandlerProtocol.swift */; }; 0254D1912BCD699F000CDE89 /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0254D1902BCD699F000CDE89 /* RefreshProgressView.swift */; }; - 0255D5582936283A004DBC1A /* UploadBodyEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0255D55729362839004DBC1A /* UploadBodyEncoding.swift */; }; 0259104A29C4A5B6004B5A55 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0259104929C4A5B6004B5A55 /* UserSettings.swift */; }; + 025A20502C071EB0003EA08D /* ErrorAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025A204F2C071EB0003EA08D /* ErrorAlertView.swift */; }; 025B36752A13B7D5001A640E /* UnitButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025B36742A13B7D5001A640E /* UnitButtonView.swift */; }; - 025EF2F62971740000B838AB /* YouTubePlayerKit in Frameworks */ = {isa = PBXBuildFile; productRef = 025EF2F52971740000B838AB /* YouTubePlayerKit */; }; 0260E58028FD792800BBBE18 /* WebUnitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0260E57F28FD792800BBBE18 /* WebUnitViewModel.swift */; }; + 0267F8512C3C256F0089D810 /* FileWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0267F8502C3C256F0089D810 /* FileWebView.swift */; }; 027BD3922907D88F00392132 /* Data_RegistrationFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3912907D88F00392132 /* Data_RegistrationFields.swift */; }; 027BD39C2908810C00392132 /* RegisterUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD39B2908810C00392132 /* RegisterUser.swift */; }; 027BD3A82909474200392132 /* KeyboardAvoidingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3A62909474100392132 /* KeyboardAvoidingViewController.swift */; }; @@ -53,9 +53,8 @@ 027BD3B52909475900392132 /* KeyboardStateObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3B22909475900392132 /* KeyboardStateObserver.swift */; }; 027BD3B82909476200392132 /* DismissKeyboardTapViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3B62909476200392132 /* DismissKeyboardTapViewModifier.swift */; }; 027BD3B92909476200392132 /* KeyboardAvoidingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3B72909476200392132 /* KeyboardAvoidingModifier.swift */; }; - 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3BA2909478B00392132 /* UIView+EnclosingScrollView.swift */; }; - 027BD3BE2909478B00392132 /* UIResponder+CurrentResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3BB2909478B00392132 /* UIResponder+CurrentResponder.swift */; }; 027BD3C52909707700392132 /* Shake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027BD3C42909707700392132 /* Shake.swift */; }; + 027F1BF72C071C820001A24C /* NavigationTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027F1BF62C071C820001A24C /* NavigationTitle.swift */; }; 0282DA7328F98CC9003C3F07 /* WebUnitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0282DA7228F98CC9003C3F07 /* WebUnitView.swift */; }; 0283347D28D4D3DE00C828FC /* Data_Discovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283347C28D4D3DE00C828FC /* Data_Discovery.swift */; }; 0283348028D4DCD200C828FC /* ViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0283347F28D4DCD200C828FC /* ViewExtension.swift */; }; @@ -63,31 +62,34 @@ 028CE96929858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */; }; 028F9F37293A44C700DE65D0 /* Data_ResetPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028F9F36293A44C700DE65D0 /* Data_ResetPassword.swift */; }; 028F9F39293A452B00DE65D0 /* ResetPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028F9F38293A452B00DE65D0 /* ResetPassword.swift */; }; - 0295C885299B99DD00ABE571 /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */; }; + 02935B732BCECAD000B22F66 /* Data_PrimaryEnrollment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B722BCECAD000B22F66 /* Data_PrimaryEnrollment.swift */; }; + 02935B752BCEE6D600B22F66 /* PrimaryEnrollment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B742BCEE6D600B22F66 /* PrimaryEnrollment.swift */; }; + 029A13262C2457D9005FB830 /* OfflineProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029A13252C2457D9005FB830 /* OfflineProgress.swift */; }; + 029A13282C246AE6005FB830 /* OfflineSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029A13272C246AE6005FB830 /* OfflineSyncManager.swift */; }; + 029A132A2C2471DF005FB830 /* OfflineSyncRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029A13292C2471DF005FB830 /* OfflineSyncRepository.swift */; }; + 029A132C2C2471F8005FB830 /* OfflineSyncEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029A132B2C2471F8005FB830 /* OfflineSyncEndpoint.swift */; }; + 029A13302C2479E7005FB830 /* OfflineSyncInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029A132F2C2479E7005FB830 /* OfflineSyncInteractor.swift */; }; 02A463112AEA966C00331037 /* AppReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A463102AEA966C00331037 /* AppReviewView.swift */; }; 02A4833529B8A73400D33F33 /* CorePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.swift */; }; 02A4833829B8A8F900D33F33 /* CoreDataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833629B8A8F800D33F33 /* CoreDataModel.xcdatamodeld */; }; 02A4833A29B8A9AB00D33F33 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833929B8A9AB00D33F33 /* DownloadManager.swift */; }; 02A4833C29B8C57800D33F33 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833B29B8C57800D33F33 /* DownloadView.swift */; }; + 02A8C5812C05DBB4004B91FF /* Data_EnrollmentsStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A8C5802C05DBB4004B91FF /* Data_EnrollmentsStatus.swift */; }; + 02AA27942C2C1B88006F5B6A /* ZipArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 02AA27932C2C1B88006F5B6A /* ZipArchive */; }; 02AFCC182AEFDB24000360F0 /* ThirdPartyMailClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AFCC172AEFDB24000360F0 /* ThirdPartyMailClient.swift */; }; 02AFCC1A2AEFDC18000360F0 /* ThirdPartyMailer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02AFCC192AEFDC18000360F0 /* ThirdPartyMailer.swift */; }; - 02B2B594295C5C7A00914876 /* Thread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B2B593295C5C7A00914876 /* Thread.swift */; }; - 02B3E3B32930198600A50475 /* AVPlayerViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */; }; - 02B3F16E2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */; }; - 02C917F029CDA99E00DBB8BD /* Data_Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */; }; + 02C917F029CDA99E00DBB8BD /* Data_Enrollments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C917EF29CDA99E00DBB8BD /* Data_Enrollments.swift */; }; + 02CA59842BD7DDBE00D517AA /* DashboardConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CA59832BD7DDBE00D517AA /* DashboardConfig.swift */; }; 02CF46C829546AA200A698EE /* NoCachedDataError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CF46C729546AA200A698EE /* NoCachedDataError.swift */; }; - 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */; }; 02D800CC29348F460099CF16 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D800CB29348F460099CF16 /* ImagePicker.swift */; }; 02E224DB2BB76B3E00EF1ADB /* DynamicOffsetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E224DA2BB76B3E00EF1ADB /* DynamicOffsetView.swift */; }; - 02E225B0291D29EB0067769A /* UrlExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E225AF291D29EB0067769A /* UrlExtension.swift */; }; 02E93F852AEBAEBC006C4750 /* AppReviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E93F842AEBAEBC006C4750 /* AppReviewViewModel.swift */; }; - 02F164372902A9EB0090DDEF /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F164362902A9EB0090DDEF /* StringExtension.swift */; }; + 02EBC7572C19DCDB00BE182C /* SyncStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EBC7562C19DCDB00BE182C /* SyncStatus.swift */; }; + 02EBC7592C19DE1100BE182C /* CalendarManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EBC7582C19DE1100BE182C /* CalendarManagerProtocol.swift */; }; + 02EBC75B2C19DE3D00BE182C /* CourseForSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EBC75A2C19DE3D00BE182C /* CourseForSync.swift */; }; 02F6EF3B28D9B8EC00835477 /* CourseCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F6EF3A28D9B8EC00835477 /* CourseCellView.swift */; }; 02F6EF4A28D9F0A700835477 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F6EF4928D9F0A700835477 /* DateExtension.swift */; }; - 02F98A7F28F81EE900DE94C0 /* Container+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F98A7E28F81EE900DE94C0 /* Container+App.swift */; }; 0604C9AA2B22FACF00AD5DBF /* UIComponentsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0604C9A92B22FACF00AD5DBF /* UIComponentsConfig.swift */; }; - 06078B702BA49C3100576798 /* Dictionary+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06078B6E2BA49C3100576798 /* Dictionary+JSON.swift */; }; - 06078B712BA49C3100576798 /* String+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06078B6F2BA49C3100576798 /* String+JSON.swift */; }; 064987932B4D69FF0071642A /* DragAndDropCssInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0649878A2B4D69FE0071642A /* DragAndDropCssInjection.swift */; }; 064987942B4D69FF0071642A /* WebviewInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0649878B2B4D69FE0071642A /* WebviewInjection.swift */; }; 064987952B4D69FF0071642A /* SurveyCssInjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0649878C2B4D69FE0071642A /* SurveyCssInjection.swift */; }; @@ -111,63 +113,46 @@ 0727877028D23411002E9142 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727876F28D23411002E9142 /* Config.swift */; }; 0727877728D23847002E9142 /* DataLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727877628D23847002E9142 /* DataLayer.swift */; }; 0727877928D23BE0002E9142 /* RequestInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727877828D23BE0002E9142 /* RequestInterceptor.swift */; }; - 0727877B28D24A1D002E9142 /* HeadersRedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727877A28D24A1D002E9142 /* HeadersRedirectHandler.swift */; }; 0727877D28D25212002E9142 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727877C28D25212002E9142 /* ProgressBar.swift */; }; - 0727877F28D25B24002E9142 /* Alamofire+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727877E28D25B24002E9142 /* Alamofire+Error.swift */; }; 0727878128D25EFD002E9142 /* SnackBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727878028D25EFD002E9142 /* SnackBarView.swift */; }; - 0727878328D31287002E9142 /* DispatchQueue+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727878228D31287002E9142 /* DispatchQueue+App.swift */; }; 0727878528D31657002E9142 /* Data_User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727878428D31657002E9142 /* Data_User.swift */; }; 0727878928D31734002E9142 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0727878828D31734002E9142 /* User.swift */; }; 072787B628D37A0E002E9142 /* Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072787B528D37A0E002E9142 /* Validator.swift */; }; - 07460FE1294B706200F70538 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07460FE0294B706200F70538 /* CollectionExtension.swift */; }; 07460FE3294B72D700F70538 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07460FE2294B72D700F70538 /* Notification.swift */; }; 076F297F2A1F80C800967E7D /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 076F297E2A1F80C800967E7D /* Pagination.swift */; }; 0770DE1928D0847D006D8A5D /* BaseRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE1828D0847D006D8A5D /* BaseRouter.swift */; }; 0770DE2528D08FBA006D8A5D /* CoreStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2428D08FBA006D8A5D /* CoreStorage.swift */; }; - 0770DE2A28D0929E006D8A5D /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2928D0929E006D8A5D /* HTTPTask.swift */; }; - 0770DE2C28D092B3006D8A5D /* NetworkLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2B28D092B3006D8A5D /* NetworkLogger.swift */; }; - 0770DE2E28D09743006D8A5D /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2D28D09743006D8A5D /* API.swift */; }; - 0770DE3028D09793006D8A5D /* EndPointType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE2F28D09793006D8A5D /* EndPointType.swift */; }; 0770DE5228D0ADFF006D8A5D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE5128D0ADFF006D8A5D /* Assets.xcassets */; }; 0770DE5428D0B00C006D8A5D /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE5328D0B00C006D8A5D /* swiftgen.yml */; }; 0770DE5B28D0B209006D8A5D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE5D28D0B209006D8A5D /* Localizable.strings */; }; 0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE5E28D0B22C006D8A5D /* Strings.swift */; }; 0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE6028D0B2CB006D8A5D /* Assets.swift */; }; - 07DDFCBD29A780BB00572595 /* UINavigationController+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */; }; 07E0939F2B308D2800F1E4B2 /* Data_Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E0939E2B308D2800F1E4B2 /* Data_Certificate.swift */; }; 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141F1D2F2B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift */; }; - 142EDD6C2B831D1400F9F320 /* BranchSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 142EDD6B2B831D1400F9F320 /* BranchSDK */; }; 14769D3C2B9822EE00AB36D4 /* CoreAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14769D3B2B9822EE00AB36D4 /* CoreAnalytics.swift */; }; - A51CDBE72B6D21F2009B6D4E /* SegmentConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */; }; + 5E58740A2AA9DF20F4644191 /* Pods_App_Core_CoreTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33FA09A20AAE2B2A0BA89190 /* Pods_App_Core_CoreTests.framework */; }; + 9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */; }; A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A32342B233DEC005FE38A /* ThemeConfig.swift */; }; A595689B2B6173DF00ED4F90 /* BranchConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A595689A2B6173DF00ED4F90 /* BranchConfig.swift */; }; A5F4E7B52B61544A00ACD166 /* BrazeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F4E7B42B61544A00ACD166 /* BrazeConfig.swift */; }; - BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA30427D2B20B299009B64B7 /* SocialAuthError.swift */; }; BA4AFB422B5A7A0900A21367 /* VideoDownloadQualityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */; }; BA4AFB442B6A5AF100A21367 /* CheckBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA4AFB432B6A5AF100A21367 /* CheckBoxView.swift */; }; BA593F1C2AF8E498009ADB51 /* ScrollSlidingTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA593F1B2AF8E498009ADB51 /* ScrollSlidingTabBar.swift */; }; BA593F1E2AF8E4A0009ADB51 /* FrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA593F1D2AF8E4A0009ADB51 /* FrameReader.swift */; }; - BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */; }; - BA8B3A2F2AD546A700D25EF5 /* DebugLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */; }; - BA8FA6612AD5974300EA029A /* AppleAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA6602AD5974300EA029A /* AppleAuthProvider.swift */; }; BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */; }; - BA8FA66A2AD59B5500EA029A /* GoogleAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA6692AD59B5500EA029A /* GoogleAuthProvider.swift */; }; - BA8FA66C2AD59BBC00EA029A /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = BA8FA66B2AD59BBC00EA029A /* GoogleSignIn */; }; - BA8FA66E2AD59E7D00EA029A /* FacebookAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */; }; - BA8FA6702AD59EA300EA029A /* MicrosoftAuthProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */; }; - BA981BCE2B8F5C49005707C2 /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA981BCD2B8F5C49005707C2 /* Sequence+Extensions.swift */; }; BA981BD02B91ED50005707C2 /* FullScreenProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA981BCF2B91ED50005707C2 /* FullScreenProgressView.swift */; }; - BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */; }; BAD9CA2F2B289B3500DE790A /* ajaxHandler.js in Resources */ = {isa = PBXBuildFile; fileRef = BAD9CA2E2B289B3500DE790A /* ajaxHandler.js */; }; BAD9CA332B28A8F300DE790A /* AjaxProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA322B28A8F300DE790A /* AjaxProvider.swift */; }; BAD9CA422B2B140100DE790A /* AgreementConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */; }; - BADB3F5B2AD6EC56004D5CFA /* ResultExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */; }; - BAF0D4CB2AD6AE14007AC334 /* FacebookLogin in Frameworks */ = {isa = PBXBuildFile; productRef = BAF0D4CA2AD6AE14007AC334 /* FacebookLogin */; }; BAFB99822B0E2354007D09F9 /* FacebookConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */; }; BAFB99842B0E282E007D09F9 /* MicrosoftConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */; }; BAFB99902B14B377007D09F9 /* GoogleConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */; }; BAFB99922B14E23D007D09F9 /* AppleSignInConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */; }; C8C446EF233F81B9FABB77D2 /* Pods_App_Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */; }; + CE54C2D22CC80D8500E529F9 /* DownloadManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE54C2D12CC80D8500E529F9 /* DownloadManagerTests.swift */; }; + CE57127C2CD109DB00D4AB17 /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CE57127B2CD109DB00D4AB17 /* OEXFoundation */; }; + CE7CAF392CC1561E00E0AC9D /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CE7CAF382CC1561E00E0AC9D /* OEXFoundation */; }; + CE953A3B2CD0DA940023D667 /* CoreMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE953A3A2CD0DA940023D667 /* CoreMock.generated.swift */; }; CFC84952299F8B890055E497 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC84951299F8B890055E497 /* Debounce.swift */; }; DBF6F2412B014ADA0098414B /* FirebaseConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF6F2402B014ADA0098414B /* FirebaseConfig.swift */; }; DBF6F2462B01DAFE0098414B /* AgreementConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF6F2452B01DAFE0098414B /* AgreementConfig.swift */; }; @@ -175,7 +160,6 @@ E055A5392B18DC95008D9E5E /* Theme.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E055A5382B18DC95008D9E5E /* Theme.framework */; }; E09179FD2B0F204E002AB695 /* ConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09179FC2B0F204D002AB695 /* ConfigTests.swift */; }; E0D5861A2B2FF74C009B4BA7 /* DiscoveryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */; }; - E0D5861C2B2FF85B009B4BA7 /* RawStringExtactable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */; }; E0D586362B314CD3009B4BA7 /* LogistrationBottomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */; }; /* End PBXBuildFile section */ @@ -189,6 +173,29 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + CE57127E2CD109DB00D4AB17 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + CE7CAF3B2CC1561E00E0AC9D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 020306CB2932C0C4000949EA /* PickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerView.swift; sourceTree = ""; }; 02066B472906F73400F4307E /* PickerMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerMenu.swift; sourceTree = ""; }; @@ -197,8 +204,9 @@ 021D924728DC860C00ACC565 /* Data_UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_UserProfile.swift; sourceTree = ""; }; 021D924F28DC89D100ACC565 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 021D925628DCF12900ACC565 /* AlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertView.swift; sourceTree = ""; }; + 022020452C11BB2200D15795 /* Data_CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseDates.swift; sourceTree = ""; }; 02280F5A294B4E6F0032823A /* Connectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.swift; sourceTree = ""; }; - 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplicationExtension.swift; sourceTree = ""; }; + 02286D152C106393005EEC8D /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; 022C64E329AE0191000F532B /* TextWithUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithUrls.swift; sourceTree = ""; }; 0231CDBD2922422D00032416 /* CSSInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSSInjector.swift; sourceTree = ""; }; 0233D56E2AF13EB200BAC8BD /* StarRatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StarRatingView.swift; sourceTree = ""; }; @@ -214,15 +222,15 @@ 023A4DD3299E66BD006C0E48 /* OfflineSnackBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineSnackBarView.swift; sourceTree = ""; }; 0241666A28F5A78B00082765 /* HTMLFormattedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLFormattedText.swift; sourceTree = ""; }; 0248C92229C075EF00DC8402 /* CourseBlockModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseBlockModel.swift; sourceTree = ""; }; - 024BE3DE29B2615500BCDEE2 /* CGColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGColorExtension.swift; sourceTree = ""; }; 024D723429C8BB1A006D36ED /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; 024FCCFF28EF1CD300232339 /* WebBrowser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebBrowser.swift; sourceTree = ""; }; 02512FEF299533DE0024D438 /* CoreDataHandlerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandlerProtocol.swift; sourceTree = ""; }; 0254D1902BCD699F000CDE89 /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; - 0255D55729362839004DBC1A /* UploadBodyEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadBodyEncoding.swift; sourceTree = ""; }; 0259104929C4A5B6004B5A55 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; + 025A204F2C071EB0003EA08D /* ErrorAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlertView.swift; sourceTree = ""; }; 025B36742A13B7D5001A640E /* UnitButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitButtonView.swift; sourceTree = ""; }; 0260E57F28FD792800BBBE18 /* WebUnitViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebUnitViewModel.swift; sourceTree = ""; }; + 0267F8502C3C256F0089D810 /* FileWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileWebView.swift; sourceTree = ""; }; 027BD3912907D88F00392132 /* Data_RegistrationFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_RegistrationFields.swift; sourceTree = ""; }; 027BD39B2908810C00392132 /* RegisterUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterUser.swift; sourceTree = ""; }; 027BD3A62909474100392132 /* KeyboardAvoidingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardAvoidingViewController.swift; sourceTree = ""; }; @@ -235,9 +243,8 @@ 027BD3B22909475900392132 /* KeyboardStateObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardStateObserver.swift; sourceTree = ""; }; 027BD3B62909476200392132 /* DismissKeyboardTapViewModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissKeyboardTapViewModifier.swift; sourceTree = ""; }; 027BD3B72909476200392132 /* KeyboardAvoidingModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardAvoidingModifier.swift; sourceTree = ""; }; - 027BD3BA2909478B00392132 /* UIView+EnclosingScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+EnclosingScrollView.swift"; sourceTree = ""; }; - 027BD3BB2909478B00392132 /* UIResponder+CurrentResponder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIResponder+CurrentResponder.swift"; sourceTree = ""; }; 027BD3C42909707700392132 /* Shake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shake.swift; sourceTree = ""; }; + 027F1BF62C071C820001A24C /* NavigationTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTitle.swift; sourceTree = ""; }; 0282DA7228F98CC9003C3F07 /* WebUnitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebUnitView.swift; sourceTree = ""; }; 0283347C28D4D3DE00C828FC /* Data_Discovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Discovery.swift; sourceTree = ""; }; 0283347F28D4DCD200C828FC /* ViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtension.swift; sourceTree = ""; }; @@ -245,32 +252,34 @@ 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleKeyboardInputView.swift; sourceTree = ""; }; 028F9F36293A44C700DE65D0 /* Data_ResetPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_ResetPassword.swift; sourceTree = ""; }; 028F9F38293A452B00DE65D0 /* ResetPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetPassword.swift; sourceTree = ""; }; - 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; + 02935B722BCECAD000B22F66 /* Data_PrimaryEnrollment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_PrimaryEnrollment.swift; sourceTree = ""; }; + 02935B742BCEE6D600B22F66 /* PrimaryEnrollment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryEnrollment.swift; sourceTree = ""; }; + 029A13252C2457D9005FB830 /* OfflineProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineProgress.swift; sourceTree = ""; }; + 029A13272C246AE6005FB830 /* OfflineSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineSyncManager.swift; sourceTree = ""; }; + 029A13292C2471DF005FB830 /* OfflineSyncRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineSyncRepository.swift; sourceTree = ""; }; + 029A132B2C2471F8005FB830 /* OfflineSyncEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineSyncEndpoint.swift; sourceTree = ""; }; + 029A132F2C2479E7005FB830 /* OfflineSyncInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineSyncInteractor.swift; sourceTree = ""; }; 02A463102AEA966C00331037 /* AppReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewView.swift; sourceTree = ""; }; 02A4833429B8A73400D33F33 /* CorePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePersistenceProtocol.swift; sourceTree = ""; }; 02A4833729B8A8F800D33F33 /* CoreDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreDataModel.xcdatamodel; sourceTree = ""; }; 02A4833929B8A9AB00D33F33 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; 02A4833B29B8C57800D33F33 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; }; + 02A8C5802C05DBB4004B91FF /* Data_EnrollmentsStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_EnrollmentsStatus.swift; sourceTree = ""; }; 02AFCC172AEFDB24000360F0 /* ThirdPartyMailClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyMailClient.swift; sourceTree = ""; }; 02AFCC192AEFDC18000360F0 /* ThirdPartyMailer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyMailer.swift; sourceTree = ""; }; - 02B2B593295C5C7A00914876 /* Thread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thread.swift; sourceTree = ""; }; - 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayerViewControllerExtension.swift; sourceTree = ""; }; - 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollViewCompat.swift; sourceTree = ""; }; - 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Dashboard.swift; sourceTree = ""; }; + 02C917EF29CDA99E00DBB8BD /* Data_Enrollments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Enrollments.swift; sourceTree = ""; }; + 02CA59832BD7DDBE00D517AA /* DashboardConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardConfig.swift; sourceTree = ""; }; 02CF46C729546AA200A698EE /* NoCachedDataError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCachedDataError.swift; sourceTree = ""; }; - 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SKStoreReviewControllerExtension.swift; sourceTree = ""; }; 02D800CB29348F460099CF16 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; 02E224DA2BB76B3E00EF1ADB /* DynamicOffsetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicOffsetView.swift; sourceTree = ""; }; - 02E225AF291D29EB0067769A /* UrlExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlExtension.swift; sourceTree = ""; }; 02E93F842AEBAEBC006C4750 /* AppReviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewViewModel.swift; sourceTree = ""; }; - 02ED50CB29A64B84008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; - 02F164362902A9EB0090DDEF /* StringExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; + 02EBC7562C19DCDB00BE182C /* SyncStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatus.swift; sourceTree = ""; }; + 02EBC7582C19DE1100BE182C /* CalendarManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManagerProtocol.swift; sourceTree = ""; }; + 02EBC75A2C19DE3D00BE182C /* CourseForSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseForSync.swift; sourceTree = ""; }; 02F6EF3A28D9B8EC00835477 /* CourseCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseCellView.swift; sourceTree = ""; }; 02F6EF4928D9F0A700835477 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; - 02F98A7E28F81EE900DE94C0 /* Container+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Container+App.swift"; sourceTree = ""; }; + 043DD0B526F919DFA1C5E600 /* Pods-App-Core-CoreTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core-CoreTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests.releaseprod.xcconfig"; sourceTree = ""; }; 0604C9A92B22FACF00AD5DBF /* UIComponentsConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIComponentsConfig.swift; sourceTree = ""; }; - 06078B6E2BA49C3100576798 /* Dictionary+JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+JSON.swift"; sourceTree = ""; }; - 06078B6F2BA49C3100576798 /* String+JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+JSON.swift"; sourceTree = ""; }; 0649878A2B4D69FE0071642A /* DragAndDropCssInjection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragAndDropCssInjection.swift; sourceTree = ""; }; 0649878B2B4D69FE0071642A /* WebviewInjection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebviewInjection.swift; sourceTree = ""; }; 0649878C2B4D69FE0071642A /* SurveyCssInjection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SurveyCssInjection.swift; sourceTree = ""; }; @@ -294,78 +303,76 @@ 0727876F28D23411002E9142 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 0727877628D23847002E9142 /* DataLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataLayer.swift; sourceTree = ""; }; 0727877828D23BE0002E9142 /* RequestInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestInterceptor.swift; sourceTree = ""; }; - 0727877A28D24A1D002E9142 /* HeadersRedirectHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadersRedirectHandler.swift; sourceTree = ""; }; 0727877C28D25212002E9142 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = ""; }; - 0727877E28D25B24002E9142 /* Alamofire+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Alamofire+Error.swift"; sourceTree = ""; }; 0727878028D25EFD002E9142 /* SnackBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnackBarView.swift; sourceTree = ""; }; - 0727878228D31287002E9142 /* DispatchQueue+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+App.swift"; sourceTree = ""; }; 0727878428D31657002E9142 /* Data_User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_User.swift; sourceTree = ""; }; 0727878828D31734002E9142 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 072787B528D37A0E002E9142 /* Validator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Validator.swift; sourceTree = ""; }; - 07460FE0294B706200F70538 /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; 07460FE2294B72D700F70538 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 0754BB7841E3C0F8D6464951 /* Pods-App-Core.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releasestage.xcconfig"; sourceTree = ""; }; 076F297E2A1F80C800967E7D /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = ""; }; 0770DE0828D07831006D8A5D /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0770DE1828D0847D006D8A5D /* BaseRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseRouter.swift; sourceTree = ""; }; 0770DE2428D08FBA006D8A5D /* CoreStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreStorage.swift; sourceTree = ""; }; - 0770DE2928D0929E006D8A5D /* HTTPTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTask.swift; sourceTree = ""; }; - 0770DE2B28D092B3006D8A5D /* NetworkLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkLogger.swift; sourceTree = ""; }; - 0770DE2D28D09743006D8A5D /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; - 0770DE2F28D09793006D8A5D /* EndPointType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndPointType.swift; sourceTree = ""; }; 0770DE5128D0ADFF006D8A5D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 0770DE5328D0B00C006D8A5D /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; 0770DE5C28D0B209006D8A5D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 0770DE5E28D0B22C006D8A5D /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 0770DE6028D0B2CB006D8A5D /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; - 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Animation.swift"; sourceTree = ""; }; 07E0939E2B308D2800F1E4B2 /* Data_Certificate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_Certificate.swift; sourceTree = ""; }; + 0CA4A65A3AECED83CC425A00 /* Pods-CoreTests.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreTests.debugstage.xcconfig"; path = "Target Support Files/Pods-CoreTests/Pods-CoreTests.debugstage.xcconfig"; sourceTree = ""; }; 0E13E9173C9C4CFC19F8B6F2 /* Pods-App-Core.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugstage.xcconfig"; sourceTree = ""; }; 141F1D2F2B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebviewCookiesUpdateProtocol.swift; sourceTree = ""; }; 14769D3B2B9822EE00AB36D4 /* CoreAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreAnalytics.swift; sourceTree = ""; }; 1A154A95AF4EE85A4A1C083B /* Pods-App-Core.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releasedev.xcconfig"; sourceTree = ""; }; 2B7E6FE7843FC4CF2BFA712D /* Pods-App-Core.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debug.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debug.xcconfig"; sourceTree = ""; }; + 33FA09A20AAE2B2A0BA89190 /* Pods_App_Core_CoreTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Core_CoreTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releaseprod.xcconfig"; sourceTree = ""; }; + 3C63D5D2247C793C259341B8 /* Pods-App-Core-CoreTests.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core-CoreTests.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests.releasedev.xcconfig"; sourceTree = ""; }; + 5CEFA8766C44C519B86C681D /* Pods-App-Core-CoreTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core-CoreTests.debug.xcconfig"; path = "Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests.debug.xcconfig"; sourceTree = ""; }; 60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.release.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.release.xcconfig"; sourceTree = ""; }; + 8F3B171E9FA5E6F40B4890A8 /* Pods-App-Core-CoreTests.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core-CoreTests.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests.debugstage.xcconfig"; sourceTree = ""; }; + 90D63D7E70B8F5027A211EA3 /* Pods-CoreTests.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreTests.debugprod.xcconfig"; path = "Target Support Files/Pods-CoreTests/Pods-CoreTests.debugprod.xcconfig"; sourceTree = ""; }; + 951751177FD4703992DC1491 /* Pods-CoreTests.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreTests.releasestage.xcconfig"; path = "Target Support Files/Pods-CoreTests/Pods-CoreTests.releasestage.xcconfig"; sourceTree = ""; }; + 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullScreenErrorView.swift; sourceTree = ""; }; 9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = ""; }; - A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentConfig.swift; sourceTree = ""; }; + 9E0B33614CBD791307FFDEAE /* Pods-App-Core-CoreTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core-CoreTests.release.xcconfig"; path = "Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests.release.xcconfig"; sourceTree = ""; }; A53A32342B233DEC005FE38A /* ThemeConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeConfig.swift; sourceTree = ""; }; A595689A2B6173DF00ED4F90 /* BranchConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BranchConfig.swift; sourceTree = ""; }; A5F4E7B42B61544A00ACD166 /* BrazeConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrazeConfig.swift; sourceTree = ""; }; - BA30427D2B20B299009B64B7 /* SocialAuthError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocialAuthError.swift; sourceTree = ""; }; + B2556B4A2D4F84F402A7A7D9 /* Pods-CoreTests.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreTests.releaseprod.xcconfig"; path = "Target Support Files/Pods-CoreTests/Pods-CoreTests.releaseprod.xcconfig"; sourceTree = ""; }; BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadQualityView.swift; sourceTree = ""; }; BA4AFB432B6A5AF100A21367 /* CheckBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxView.swift; sourceTree = ""; }; BA593F1B2AF8E498009ADB51 /* ScrollSlidingTabBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollSlidingTabBar.swift; sourceTree = ""; }; BA593F1D2AF8E4A0009ADB51 /* FrameReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FrameReader.swift; sourceTree = ""; }; - BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthResponse.swift; sourceTree = ""; }; - BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLog.swift; sourceTree = ""; }; - BA8FA6602AD5974300EA029A /* AppleAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleAuthProvider.swift; sourceTree = ""; }; BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthButton.swift; sourceTree = ""; }; - BA8FA6692AD59B5500EA029A /* GoogleAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthProvider.swift; sourceTree = ""; }; - BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookAuthProvider.swift; sourceTree = ""; }; - BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosoftAuthProvider.swift; sourceTree = ""; }; - BA981BCD2B8F5C49005707C2 /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = ""; }; BA981BCF2B91ED50005707C2 /* FullScreenProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenProgressView.swift; sourceTree = ""; }; - BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDisclosureGroup.swift; sourceTree = ""; }; BAD9CA2E2B289B3500DE790A /* ajaxHandler.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = ajaxHandler.js; sourceTree = ""; }; BAD9CA322B28A8F300DE790A /* AjaxProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AjaxProvider.swift; sourceTree = ""; }; BAD9CA412B2B140100DE790A /* AgreementConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreementConfigTests.swift; sourceTree = ""; }; - BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultExtension.swift; sourceTree = ""; }; BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FacebookConfig.swift; sourceTree = ""; }; BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrosoftConfig.swift; sourceTree = ""; }; BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleConfig.swift; sourceTree = ""; }; BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleSignInConfig.swift; sourceTree = ""; }; + C28D4872BAB1276B9AD24A33 /* Pods-CoreTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreTests.debugdev.xcconfig"; path = "Target Support Files/Pods-CoreTests/Pods-CoreTests.debugdev.xcconfig"; sourceTree = ""; }; C7E5BCE79CE297B20777B27A /* Pods-App-Core.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugprod.xcconfig"; sourceTree = ""; }; + CE54C2D12CC80D8500E529F9 /* DownloadManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManagerTests.swift; sourceTree = ""; }; + CE953A3A2CD0DA940023D667 /* CoreMock.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreMock.generated.swift; sourceTree = ""; }; CFC84951299F8B890055E497 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; DBF6F2402B014ADA0098414B /* FirebaseConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseConfig.swift; sourceTree = ""; }; DBF6F2452B01DAFE0098414B /* AgreementConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreementConfig.swift; sourceTree = ""; }; DBF6F2492B0380E00098414B /* FeaturesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturesConfig.swift; sourceTree = ""; }; + DD27D6BF5EB15C6C8B66969A /* Pods-App-Core-CoreTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core-CoreTests.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests.debugdev.xcconfig"; sourceTree = ""; }; E055A5382B18DC95008D9E5E /* Theme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Theme.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E09179FC2B0F204D002AB695 /* ConfigTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigTests.swift; sourceTree = ""; }; E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryConfig.swift; sourceTree = ""; }; - E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawStringExtactable.swift; sourceTree = ""; }; E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogistrationBottomView.swift; sourceTree = ""; }; + E8D9725130C85DA55AD474A4 /* Pods-CoreTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreTests.debug.xcconfig"; path = "Target Support Files/Pods-CoreTests/Pods-CoreTests.debug.xcconfig"; sourceTree = ""; }; + F4E50CE1DB6AA77E9B5D09EF /* Pods-App-Core-CoreTests.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core-CoreTests.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests.debugprod.xcconfig"; sourceTree = ""; }; + F7ED6F0C276DBD2F1BA38987 /* Pods-CoreTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreTests.release.xcconfig"; path = "Target Support Files/Pods-CoreTests/Pods-CoreTests.release.xcconfig"; sourceTree = ""; }; + FB6C49AC95A27A1222AD0F06 /* Pods-App-Core-CoreTests.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core-CoreTests.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests.releasestage.xcconfig"; sourceTree = ""; }; + FD97820E148E423964AC0CAB /* Pods-CoreTests.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CoreTests.releasedev.xcconfig"; path = "Target Support Files/Pods-CoreTests/Pods-CoreTests.releasedev.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -373,7 +380,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CE7CAF392CC1561E00E0AC9D /* OEXFoundation in Frameworks */, 0716946D296D996900E3DED6 /* Core.framework in Frameworks */, + 5E58740A2AA9DF20F4644191 /* Pods_App_Core_CoreTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -381,11 +390,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - BAF0D4CB2AD6AE14007AC334 /* FacebookLogin in Frameworks */, - 025EF2F62971740000B838AB /* YouTubePlayerKit in Frameworks */, + CE57127C2CD109DB00D4AB17 /* OEXFoundation in Frameworks */, C8C446EF233F81B9FABB77D2 /* Pods_App_Core.framework in Frameworks */, - 142EDD6C2B831D1400F9F320 /* BranchSDK in Frameworks */, - BA8FA66C2AD59BBC00EA029A /* GoogleSignIn in Frameworks */, + 02AA27942C2C1B88006F5B6A /* ZipArchive in Frameworks */, E055A5392B18DC95008D9E5E /* Theme.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -407,6 +414,7 @@ isa = PBXGroup; children = ( 0236961828F9A26900EEF206 /* AuthRepository.swift */, + 029A13292C2471DF005FB830 /* OfflineSyncRepository.swift */, ); path = Repository; sourceTree = ""; @@ -464,28 +472,9 @@ 0283347E28D4DCC100C828FC /* Extensions */ = { isa = PBXGroup; children = ( - 06078B6E2BA49C3100576798 /* Dictionary+JSON.swift */, - 06078B6F2BA49C3100576798 /* String+JSON.swift */, - 02F164362902A9EB0090DDEF /* StringExtension.swift */, - 024BE3DE29B2615500BCDEE2 /* CGColorExtension.swift */, 0283347F28D4DCD200C828FC /* ViewExtension.swift */, 02F6EF4928D9F0A700835477 /* DateExtension.swift */, - 02F98A7E28F81EE900DE94C0 /* Container+App.swift */, - 027BD3BB2909478B00392132 /* UIResponder+CurrentResponder.swift */, - 027BD3BA2909478B00392132 /* UIView+EnclosingScrollView.swift */, - 02E225AF291D29EB0067769A /* UrlExtension.swift */, - 02B3E3B22930198600A50475 /* AVPlayerViewControllerExtension.swift */, - 07460FE0294B706200F70538 /* CollectionExtension.swift */, 07460FE2294B72D700F70538 /* Notification.swift */, - 02B2B593295C5C7A00914876 /* Thread.swift */, - 0727878228D31287002E9142 /* DispatchQueue+App.swift */, - 07DDFCBC29A780BB00572595 /* UINavigationController+Animation.swift */, - 02284C172A3B1AE00007117F /* UIApplicationExtension.swift */, - BA8B3A2E2AD546A700D25EF5 /* DebugLog.swift */, - BADB3F5A2AD6EC56004D5CFA /* ResultExtension.swift */, - 02D400602B0678190029D168 /* SKStoreReviewControllerExtension.swift */, - E0D5861B2B2FF85B009B4BA7 /* RawStringExtactable.swift */, - BA981BCD2B8F5C49005707C2 /* Sequence+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -585,7 +574,8 @@ 0727877628D23847002E9142 /* DataLayer.swift */, 0727878428D31657002E9142 /* Data_User.swift */, 0283347C28D4D3DE00C828FC /* Data_Discovery.swift */, - 02C917EF29CDA99E00DBB8BD /* Data_Dashboard.swift */, + 02C917EF29CDA99E00DBB8BD /* Data_Enrollments.swift */, + 02935B722BCECAD000B22F66 /* Data_PrimaryEnrollment.swift */, 021D924728DC860C00ACC565 /* Data_UserProfile.swift */, 0259104929C4A5B6004B5A55 /* UserSettings.swift */, 070019A428F6F17900D5FC78 /* Data_Media.swift */, @@ -593,6 +583,8 @@ 027BD3912907D88F00392132 /* Data_RegistrationFields.swift */, 028F9F36293A44C700DE65D0 /* Data_ResetPassword.swift */, 07E0939E2B308D2800F1E4B2 /* Data_Certificate.swift */, + 02A8C5802C05DBB4004B91FF /* Data_EnrollmentsStatus.swift */, + 022020452C11BB2200D15795 /* Data_CourseDates.swift */, ); path = Model; sourceTree = ""; @@ -602,6 +594,7 @@ children = ( 0727878728D3172D002E9142 /* Model */, 0236961A28F9A28B00EEF206 /* AuthInteractor.swift */, + 029A132F2C2479E7005FB830 /* OfflineSyncInteractor.swift */, ); path = Domain; sourceTree = ""; @@ -611,6 +604,7 @@ children = ( 0727878828D31734002E9142 /* User.swift */, 0284DBFD28D48C5300830893 /* CourseItem.swift */, + 02935B742BCEE6D600B22F66 /* PrimaryEnrollment.swift */, 021D924F28DC89D100ACC565 /* UserProfile.swift */, 070019AB28F6FD0100D5FC78 /* CourseDetailBlock.swift */, 0248C92229C075EF00DC8402 /* CourseBlockModel.swift */, @@ -618,7 +612,11 @@ 027BD39B2908810C00392132 /* RegisterUser.swift */, 028F9F38293A452B00DE65D0 /* ResetPassword.swift */, 020C31C8290AC3F700D6DEA2 /* PickerFields.swift */, + 02EBC75A2C19DE3D00BE182C /* CourseForSync.swift */, 076F297E2A1F80C800967E7D /* Pagination.swift */, + 029A13252C2457D9005FB830 /* OfflineProgress.swift */, + 02286D152C106393005EEC8D /* CourseDates.swift */, + 02EBC7562C19DCDB00BE182C /* SyncStatus.swift */, ); path = Model; sourceTree = ""; @@ -667,16 +665,11 @@ 0770DE2828D0928B006D8A5D /* Network */ = { isa = PBXGroup; children = ( - 0770DE2928D0929E006D8A5D /* HTTPTask.swift */, - 0770DE2B28D092B3006D8A5D /* NetworkLogger.swift */, - 0770DE2D28D09743006D8A5D /* API.swift */, - 0770DE2F28D09793006D8A5D /* EndPointType.swift */, 0727877828D23BE0002E9142 /* RequestInterceptor.swift */, - 0727877A28D24A1D002E9142 /* HeadersRedirectHandler.swift */, - 0727877E28D25B24002E9142 /* Alamofire+Error.swift */, 0236961E28F9A2F600EEF206 /* AuthEndpoint.swift */, - 0255D55729362839004DBC1A /* UploadBodyEncoding.swift */, + 029A132B2C2471F8005FB830 /* OfflineSyncEndpoint.swift */, 02A4833929B8A9AB00D33F33 /* DownloadManager.swift */, + 029A13272C246AE6005FB830 /* OfflineSyncManager.swift */, ); path = Network; sourceTree = ""; @@ -702,6 +695,7 @@ 0770DE7728D0C49E006D8A5D /* Base */ = { isa = PBXGroup; children = ( + 9784D47C2BF7761F00AFEFFF /* FullScreenErrorView */, 064987882B4D69FE0071642A /* Webview */, E0D586352B314CD3009B4BA7 /* LogistrationBottomView.swift */, 02A4833B29B8C57800D33F33 /* DownloadView.swift */, @@ -711,14 +705,15 @@ 025B36742A13B7D5001A640E /* UnitButtonView.swift */, 0727877C28D25212002E9142 /* ProgressBar.swift */, 022C64E329AE0191000F532B /* TextWithUrls.swift */, + 027F1BF62C071C820001A24C /* NavigationTitle.swift */, + 025A204F2C071EB0003EA08D /* ErrorAlertView.swift */, 0727878028D25EFD002E9142 /* SnackBarView.swift */, 02F6EF3A28D9B8EC00835477 /* CourseCellView.swift */, + 02EBC7582C19DE1100BE182C /* CalendarManagerProtocol.swift */, 024FCCFF28EF1CD300232339 /* WebBrowser.swift */, 028CE96829858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift */, 023A4DD3299E66BD006C0E48 /* OfflineSnackBarView.swift */, 021D925628DCF12900ACC565 /* AlertView.swift */, - 0295C884299B99DD00ABE571 /* RefreshableScrollView.swift */, - 02B3F16D2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift */, 0236F3B628F4351E0050F09B /* CourseButton.swift */, 0241666A28F5A78B00082765 /* HTMLFormattedText.swift */, 0282DA7228F98CC9003C3F07 /* WebUnitView.swift */, @@ -731,7 +726,6 @@ 023A1137291432FD00D0D354 /* FieldConfiguration.swift */, BA4AFB412B5A7A0900A21367 /* VideoDownloadQualityView.swift */, BA593F1A2AF8E487009ADB51 /* ScrollSlidingTabBar */, - BAAD62C52AFCF00B000E6103 /* CustomDisclosureGroup.swift */, BA8FA6672AD59A5700EA029A /* SocialAuthButton.swift */, 02E93F862AEBAED4006C4750 /* AppReview */, BA981BCF2B91ED50005707C2 /* FullScreenProgressView.swift */, @@ -740,6 +734,7 @@ 020D72F32BB76DFE00773319 /* VisualEffectView.swift */, 06DEA4A22BBD66A700110D20 /* BackNavigationButton.swift */, 06DEA4A42BBD66D700110D20 /* BackNavigationButtonViewModel.swift */, + 0267F8502C3C256F0089D810 /* FileWebView.swift */, ); path = Base; sourceTree = ""; @@ -752,25 +747,12 @@ path = Analytics; sourceTree = ""; }; - BA30427C2B20B235009B64B7 /* SocialAuth */ = { + 9784D47C2BF7761F00AFEFFF /* FullScreenErrorView */ = { isa = PBXGroup; children = ( - BA30427E2B20B299009B64B7 /* Error */, - BA8FA6602AD5974300EA029A /* AppleAuthProvider.swift */, - BA8FA6692AD59B5500EA029A /* GoogleAuthProvider.swift */, - BA8FA66D2AD59E7D00EA029A /* FacebookAuthProvider.swift */, - BA8FA66F2AD59EA300EA029A /* MicrosoftAuthProvider.swift */, - BA76135B2B21BC7300B599B7 /* SocialAuthResponse.swift */, - ); - path = SocialAuth; - sourceTree = ""; - }; - BA30427E2B20B299009B64B7 /* Error */ = { - isa = PBXGroup; - children = ( - BA30427D2B20B299009B64B7 /* SocialAuthError.swift */, + 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */, ); - path = Error; + path = FullScreenErrorView; sourceTree = ""; }; BA593F1A2AF8E487009ADB51 /* ScrollSlidingTabBar */ = { @@ -785,7 +767,6 @@ BA8FA65F2AD5973500EA029A /* Providers */ = { isa = PBXGroup; children = ( - BA30427C2B20B235009B64B7 /* SocialAuth */, BAD9CA3D2B29BB1A00DE790A /* Ajax */, ); path = Providers; @@ -811,11 +792,35 @@ 1A154A95AF4EE85A4A1C083B /* Pods-App-Core.releasedev.xcconfig */, 0E13E9173C9C4CFC19F8B6F2 /* Pods-App-Core.debugstage.xcconfig */, 0754BB7841E3C0F8D6464951 /* Pods-App-Core.releasestage.xcconfig */, + 5CEFA8766C44C519B86C681D /* Pods-App-Core-CoreTests.debug.xcconfig */, + F4E50CE1DB6AA77E9B5D09EF /* Pods-App-Core-CoreTests.debugprod.xcconfig */, + 8F3B171E9FA5E6F40B4890A8 /* Pods-App-Core-CoreTests.debugstage.xcconfig */, + DD27D6BF5EB15C6C8B66969A /* Pods-App-Core-CoreTests.debugdev.xcconfig */, + 9E0B33614CBD791307FFDEAE /* Pods-App-Core-CoreTests.release.xcconfig */, + 043DD0B526F919DFA1C5E600 /* Pods-App-Core-CoreTests.releaseprod.xcconfig */, + FB6C49AC95A27A1222AD0F06 /* Pods-App-Core-CoreTests.releasestage.xcconfig */, + 3C63D5D2247C793C259341B8 /* Pods-App-Core-CoreTests.releasedev.xcconfig */, + E8D9725130C85DA55AD474A4 /* Pods-CoreTests.debug.xcconfig */, + 90D63D7E70B8F5027A211EA3 /* Pods-CoreTests.debugprod.xcconfig */, + 0CA4A65A3AECED83CC425A00 /* Pods-CoreTests.debugstage.xcconfig */, + C28D4872BAB1276B9AD24A33 /* Pods-CoreTests.debugdev.xcconfig */, + F7ED6F0C276DBD2F1BA38987 /* Pods-CoreTests.release.xcconfig */, + B2556B4A2D4F84F402A7A7D9 /* Pods-CoreTests.releaseprod.xcconfig */, + 951751177FD4703992DC1491 /* Pods-CoreTests.releasestage.xcconfig */, + FD97820E148E423964AC0CAB /* Pods-CoreTests.releasedev.xcconfig */, ); name = Pods; path = ../Pods; sourceTree = ""; }; + CE54C2CE2CC80B4A00E529F9 /* DownloadManager */ = { + isa = PBXGroup; + children = ( + CE54C2D12CC80D8500E529F9 /* DownloadManagerTests.swift */, + ); + path = DownloadManager; + sourceTree = ""; + }; CFC84955299FAC4D0055E497 /* Combine */ = { isa = PBXGroup; children = ( @@ -832,13 +837,13 @@ DBF6F2402B014ADA0098414B /* FirebaseConfig.swift */, A5F4E7B42B61544A00ACD166 /* BrazeConfig.swift */, A595689A2B6173DF00ED4F90 /* BranchConfig.swift */, - A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */, DBF6F2492B0380E00098414B /* FeaturesConfig.swift */, DBF6F2452B01DAFE0098414B /* AgreementConfig.swift */, BAFB99812B0E2354007D09F9 /* FacebookConfig.swift */, BAFB99832B0E282E007D09F9 /* MicrosoftConfig.swift */, BAFB998F2B14B377007D09F9 /* GoogleConfig.swift */, BAFB99912B14E23D007D09F9 /* AppleSignInConfig.swift */, + 02CA59832BD7DDBE00D517AA /* DashboardConfig.swift */, A53A32342B233DEC005FE38A /* ThemeConfig.swift */, E0D586192B2FF74C009B4BA7 /* DiscoveryConfig.swift */, ); @@ -848,6 +853,8 @@ E09179FA2B0F204D002AB695 /* CoreTests */ = { isa = PBXGroup; children = ( + CE953A3A2CD0DA940023D667 /* CoreMock.generated.swift */, + CE54C2CE2CC80B4A00E529F9 /* DownloadManager */, E09179FB2B0F204D002AB695 /* Configuration */, ); path = CoreTests; @@ -867,6 +874,7 @@ children = ( E055A5382B18DC95008D9E5E /* Theme.framework */, 349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */, + 33FA09A20AAE2B2A0BA89190 /* Pods_App_Core_CoreTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -888,9 +896,12 @@ isa = PBXNativeTarget; buildConfigurationList = 07169476296D996900E3DED6 /* Build configuration list for PBXNativeTarget "CoreTests" */; buildPhases = ( + F87EB93C339DD81527F250AE /* [CP] Check Pods Manifest.lock */, 07169465296D996800E3DED6 /* Sources */, 07169466296D996800E3DED6 /* Frameworks */, 07169467296D996800E3DED6 /* Resources */, + C9AA9371F83D4B112F310DB8 /* [CP] Copy Pods Resources */, + CE7CAF3B2CC1561E00E0AC9D /* Embed Frameworks */, ); buildRules = ( ); @@ -912,6 +923,8 @@ 0770DE0428D07831006D8A5D /* Sources */, 0770DE0528D07831006D8A5D /* Frameworks */, 0770DE0628D07831006D8A5D /* Resources */, + 49BAD0663C27D73B9115401F /* [CP] Copy Pods Resources */, + CE57127E2CD109DB00D4AB17 /* Embed Frameworks */, ); buildRules = ( ); @@ -919,10 +932,8 @@ ); name = Core; packageProductDependencies = ( - 025EF2F52971740000B838AB /* YouTubePlayerKit */, - BA8FA66B2AD59BBC00EA029A /* GoogleSignIn */, - BAF0D4CA2AD6AE14007AC334 /* FacebookLogin */, - 142EDD6B2B831D1400F9F320 /* BranchSDK */, + 02AA27932C2C1B88006F5B6A /* ZipArchive */, + CE57127B2CD109DB00D4AB17 /* OEXFoundation */, ); productName = Core; productReference = 0770DE0828D07831006D8A5D /* Core.framework */; @@ -959,10 +970,8 @@ ); mainGroup = 0770DDFE28D07831006D8A5D; packageReferences = ( - 025EF2F42971740000B838AB /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */, - BA8FA65E2AD574D700EA029A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */, - BA8FA6712AD6ABA300EA029A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */, - 142EDD6A2B831D1400F9F320 /* XCRemoteSwiftPackageReference "ios-branch-sdk-spm" */, + 02AA27922C2C1B61006F5B6A /* XCRemoteSwiftPackageReference "ZipArchive" */, + CE924BE42CD8F8E4000137CA /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */, ); productRefGroup = 0770DE0928D07831006D8A5D /* Products */; projectDirPath = ""; @@ -1015,6 +1024,40 @@ shellPath = /bin/sh; shellScript = "if [[ -f \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" ]]; then\n \"${PODS_ROOT}/SwiftGen/bin/swiftgen\"\nelse\n echo \"warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; }; + 49BAD0663C27D73B9115401F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-App-Core/Pods-App-Core-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-App-Core/Pods-App-Core-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-Core/Pods-App-Core-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + C9AA9371F83D4B112F310DB8 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App-Core-CoreTests/Pods-App-Core-CoreTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; ED83AD5255805030E042D62A /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1037,6 +1080,28 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + F87EB93C339DD81527F250AE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-App-Core-CoreTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1045,7 +1110,9 @@ buildActionMask = 2147483647; files = ( BAD9CA422B2B140100DE790A /* AgreementConfigTests.swift in Sources */, + CE953A3B2CD0DA940023D667 /* CoreMock.generated.swift in Sources */, E09179FD2B0F204E002AB695 /* ConfigTests.swift in Sources */, + CE54C2D22CC80D8500E529F9 /* DownloadManagerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1056,35 +1123,33 @@ 0727878528D31657002E9142 /* Data_User.swift in Sources */, 064987942B4D69FF0071642A /* WebviewInjection.swift in Sources */, 02F6EF4A28D9F0A700835477 /* DateExtension.swift in Sources */, + 02EBC7572C19DCDB00BE182C /* SyncStatus.swift in Sources */, DBF6F2462B01DAFE0098414B /* AgreementConfig.swift in Sources */, 027BD3AF2909475000392132 /* DismissKeyboardTapHandler.swift in Sources */, + 027F1BF72C071C820001A24C /* NavigationTitle.swift in Sources */, 06619EAA2B8F2936001FAADE /* ReadabilityModifier.swift in Sources */, BAFB99902B14B377007D09F9 /* GoogleConfig.swift in Sources */, - 02E225B0291D29EB0067769A /* UrlExtension.swift in Sources */, + 029A132C2C2471F8005FB830 /* OfflineSyncEndpoint.swift in Sources */, 02CF46C829546AA200A698EE /* NoCachedDataError.swift in Sources */, 0727877728D23847002E9142 /* DataLayer.swift in Sources */, 0241666B28F5A78B00082765 /* HTMLFormattedText.swift in Sources */, 02D800CC29348F460099CF16 /* ImagePicker.swift in Sources */, 027BD3B92909476200392132 /* KeyboardAvoidingModifier.swift in Sources */, BA4AFB422B5A7A0900A21367 /* VideoDownloadQualityView.swift in Sources */, - 0770DE2C28D092B3006D8A5D /* NetworkLogger.swift in Sources */, + 02A8C5812C05DBB4004B91FF /* Data_EnrollmentsStatus.swift in Sources */, 06BEEA0E2B6A55C500D25A97 /* ColorInversionInjection.swift in Sources */, 064987972B4D69FF0071642A /* WebView.swift in Sources */, - 0770DE2A28D0929E006D8A5D /* HTTPTask.swift in Sources */, 06619EAD2B90918B001FAADE /* ReadabilityInjection.swift in Sources */, - 02284C182A3B1AE00007117F /* UIApplicationExtension.swift in Sources */, - 0255D5582936283A004DBC1A /* UploadBodyEncoding.swift in Sources */, + 029A13262C2457D9005FB830 /* OfflineProgress.swift in Sources */, 06619EAF2B973B25001FAADE /* AccessibilityInjection.swift in Sources */, BAFB99822B0E2354007D09F9 /* FacebookConfig.swift in Sources */, + 02935B732BCECAD000B22F66 /* Data_PrimaryEnrollment.swift in Sources */, 027BD3B32909475900392132 /* Publishers+KeyboardState.swift in Sources */, 06DEA4A32BBD66A700110D20 /* BackNavigationButton.swift in Sources */, 0727877D28D25212002E9142 /* ProgressBar.swift in Sources */, BA981BD02B91ED50005707C2 /* FullScreenProgressView.swift in Sources */, 0236961F28F9A2F600EEF206 /* AuthEndpoint.swift in Sources */, - 02B3E3B32930198600A50475 /* AVPlayerViewControllerExtension.swift in Sources */, BAD9CA332B28A8F300DE790A /* AjaxProvider.swift in Sources */, - 0295C885299B99DD00ABE571 /* RefreshableScrollView.swift in Sources */, - A51CDBE72B6D21F2009B6D4E /* SegmentConfig.swift in Sources */, 0282DA7328F98CC9003C3F07 /* WebUnitView.swift in Sources */, E0D586362B314CD3009B4BA7 /* LogistrationBottomView.swift in Sources */, 0727878128D25EFD002E9142 /* SnackBarView.swift in Sources */, @@ -1095,55 +1160,48 @@ 0727877028D23411002E9142 /* Config.swift in Sources */, CFC84952299F8B890055E497 /* Debounce.swift in Sources */, 0236F3B728F4351E0050F09B /* CourseButton.swift in Sources */, - 0727878328D31287002E9142 /* DispatchQueue+App.swift in Sources */, 028F9F37293A44C700DE65D0 /* Data_ResetPassword.swift in Sources */, 022C64E429AE0191000F532B /* TextWithUrls.swift in Sources */, 0283348028D4DCD200C828FC /* ViewExtension.swift in Sources */, - BA8FA66A2AD59B5500EA029A /* GoogleAuthProvider.swift in Sources */, 02A4833529B8A73400D33F33 /* CorePersistenceProtocol.swift in Sources */, - 06078B712BA49C3100576798 /* String+JSON.swift in Sources */, 0233D5732AF13EEE00BAC8BD /* AppReviewButton.swift in Sources */, 02512FF0299533DF0024D438 /* CoreDataHandlerProtocol.swift in Sources */, 0260E58028FD792800BBBE18 /* WebUnitViewModel.swift in Sources */, 02A4833A29B8A9AB00D33F33 /* DownloadManager.swift in Sources */, - 06078B702BA49C3100576798 /* Dictionary+JSON.swift in Sources */, + 9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */, 027BD3AE2909475000392132 /* KeyboardScrollerOptions.swift in Sources */, BAFB99922B14E23D007D09F9 /* AppleSignInConfig.swift in Sources */, 141F1D302B7328D4009E81EB /* WebviewCookiesUpdateProtocol.swift in Sources */, 064987992B4D69FF0071642A /* WebViewScriptInjectionProtocol.swift in Sources */, - 027BD3BE2909478B00392132 /* UIResponder+CurrentResponder.swift in Sources */, 070019AE28F701B200D5FC78 /* Certificate.swift in Sources */, BA4AFB442B6A5AF100A21367 /* CheckBoxView.swift in Sources */, 076F297F2A1F80C800967E7D /* Pagination.swift in Sources */, 14769D3C2B9822EE00AB36D4 /* CoreAnalytics.swift in Sources */, 02AFCC1A2AEFDC18000360F0 /* ThirdPartyMailer.swift in Sources */, 0770DE5F28D0B22C006D8A5D /* Strings.swift in Sources */, - BA981BCE2B8F5C49005707C2 /* Sequence+Extensions.swift in Sources */, - 02C917F029CDA99E00DBB8BD /* Data_Dashboard.swift in Sources */, + 02C917F029CDA99E00DBB8BD /* Data_Enrollments.swift in Sources */, 024FCD0028EF1CD300232339 /* WebBrowser.swift in Sources */, 027BD3B52909475900392132 /* KeyboardStateObserver.swift in Sources */, + 022020462C11BB2200D15795 /* Data_CourseDates.swift in Sources */, 0283347D28D4D3DE00C828FC /* Data_Discovery.swift in Sources */, 064987962B4D69FF0071642A /* WebviewMessage.swift in Sources */, - 07DDFCBD29A780BB00572595 /* UINavigationController+Animation.swift in Sources */, + 02EBC75B2C19DE3D00BE182C /* CourseForSync.swift in Sources */, 023A4DD4299E66BD006C0E48 /* OfflineSnackBarView.swift in Sources */, 021D925728DCF12900ACC565 /* AlertView.swift in Sources */, 027BD3A82909474200392132 /* KeyboardAvoidingViewController.swift in Sources */, + 025A20502C071EB0003EA08D /* ErrorAlertView.swift in Sources */, + 029A13282C246AE6005FB830 /* OfflineSyncManager.swift in Sources */, 02E93F852AEBAEBC006C4750 /* AppReviewViewModel.swift in Sources */, A595689B2B6173DF00ED4F90 /* BranchConfig.swift in Sources */, 0770DE2528D08FBA006D8A5D /* CoreStorage.swift in Sources */, - BA8FA6612AD5974300EA029A /* AppleAuthProvider.swift in Sources */, - BA8FA6702AD59EA300EA029A /* MicrosoftAuthProvider.swift in Sources */, - BA76135C2B21BC7300B599B7 /* SocialAuthResponse.swift in Sources */, 020306CC2932C0C4000949EA /* PickerView.swift in Sources */, 027BD3C52909707700392132 /* Shake.swift in Sources */, 027BD39C2908810C00392132 /* RegisterUser.swift in Sources */, 071009C428D1C9D000344290 /* StyledButton.swift in Sources */, - 07460FE1294B706200F70538 /* CollectionExtension.swift in Sources */, 064987932B4D69FF0071642A /* DragAndDropCssInjection.swift in Sources */, 027BD3B42909475900392132 /* KeyboardState.swift in Sources */, 027BD3922907D88F00392132 /* Data_RegistrationFields.swift in Sources */, 07460FE3294B72D700F70538 /* Notification.swift in Sources */, - 0727877F28D25B24002E9142 /* Alamofire+Error.swift in Sources */, 02A4833829B8A8F900D33F33 /* CoreDataModel.xcdatamodeld in Sources */, 064987952B4D69FF0071642A /* SurveyCssInjection.swift in Sources */, 02E224DB2BB76B3E00EF1ADB /* DynamicOffsetView.swift in Sources */, @@ -1151,57 +1209,48 @@ 021D925028DC89D100ACC565 /* UserProfile.swift in Sources */, 071009D028D1E3A600344290 /* Constants.swift in Sources */, 0770DE1928D0847D006D8A5D /* BaseRouter.swift in Sources */, - BA30427F2B20B320009B64B7 /* SocialAuthError.swift in Sources */, + 02286D162C106393005EEC8D /* CourseDates.swift in Sources */, 0284DBFE28D48C5300830893 /* CourseItem.swift in Sources */, 0248C92329C075EF00DC8402 /* CourseBlockModel.swift in Sources */, E0D5861A2B2FF74C009B4BA7 /* DiscoveryConfig.swift in Sources */, DBF6F2412B014ADA0098414B /* FirebaseConfig.swift in Sources */, 072787B628D37A0E002E9142 /* Validator.swift in Sources */, 0236961D28F9A2D200EEF206 /* Data_AuthResponse.swift in Sources */, + 0267F8512C3C256F0089D810 /* FileWebView.swift in Sources */, A5F4E7B52B61544A00ACD166 /* BrazeConfig.swift in Sources */, 06DEA4A52BBD66D700110D20 /* BackNavigationButtonViewModel.swift in Sources */, 02AFCC182AEFDB24000360F0 /* ThirdPartyMailClient.swift in Sources */, 0233D5712AF13EC800BAC8BD /* SelectMailClientView.swift in Sources */, BAFB99842B0E282E007D09F9 /* MicrosoftConfig.swift in Sources */, - 02B2B594295C5C7A00914876 /* Thread.swift in Sources */, - E0D5861C2B2FF85B009B4BA7 /* RawStringExtactable.swift in Sources */, - 027BD3BD2909478B00392132 /* UIView+EnclosingScrollView.swift in Sources */, + 02EBC7592C19DE1100BE182C /* CalendarManagerProtocol.swift in Sources */, BA8FA6682AD59A5700EA029A /* SocialAuthButton.swift in Sources */, - 02D400612B0678190029D168 /* SKStoreReviewControllerExtension.swift in Sources */, 02A4833C29B8C57800D33F33 /* DownloadView.swift in Sources */, 027BD3AD2909475000392132 /* KeyboardScroller.swift in Sources */, 070019A528F6F17900D5FC78 /* Data_Media.swift in Sources */, 024D723529C8BB1A006D36ED /* NavigationBar.swift in Sources */, - BA8B3A2F2AD546A700D25EF5 /* DebugLog.swift in Sources */, 0727877928D23BE0002E9142 /* RequestInterceptor.swift in Sources */, - BA8FA66E2AD59E7D00EA029A /* FacebookAuthProvider.swift in Sources */, - 0770DE2E28D09743006D8A5D /* API.swift in Sources */, 028CE96929858ECC00B6B1C3 /* FlexibleKeyboardInputView.swift in Sources */, 027BD3A92909474200392132 /* KeyboardAvoidingViewControllerRepr.swift in Sources */, - 02F98A7F28F81EE900DE94C0 /* Container+App.swift in Sources */, - 02B3F16E2AB489A400DDDD4E /* RefreshableScrollViewCompat.swift in Sources */, 0649879A2B4D69FF0071642A /* WebViewHTML.swift in Sources */, - 0727877B28D24A1D002E9142 /* HeadersRedirectHandler.swift in Sources */, + 02CA59842BD7DDBE00D517AA /* DashboardConfig.swift in Sources */, 0236961B28F9A28B00EEF206 /* AuthInteractor.swift in Sources */, - 0770DE3028D09793006D8A5D /* EndPointType.swift in Sources */, + 02935B752BCEE6D600B22F66 /* PrimaryEnrollment.swift in Sources */, 020C31C9290AC3F700D6DEA2 /* PickerFields.swift in Sources */, 02F6EF3B28D9B8EC00835477 /* CourseCellView.swift in Sources */, 023A1138291432FD00D0D354 /* FieldConfiguration.swift in Sources */, DBF6F24A2B0380E00098414B /* FeaturesConfig.swift in Sources */, - 02F164372902A9EB0090DDEF /* StringExtension.swift in Sources */, 0231CDBE2922422D00032416 /* CSSInjector.swift in Sources */, 064987982B4D69FF0071642A /* CSSInjectionProtocol.swift in Sources */, - BAAD62C62AFCF00B000E6103 /* CustomDisclosureGroup.swift in Sources */, - BADB3F5B2AD6EC56004D5CFA /* ResultExtension.swift in Sources */, + 029A132A2C2471DF005FB830 /* OfflineSyncRepository.swift in Sources */, 0236961928F9A26900EEF206 /* AuthRepository.swift in Sources */, 023A1136291432B200D0D354 /* RegistrationTextField.swift in Sources */, 02A463112AEA966C00331037 /* AppReviewView.swift in Sources */, 025B36752A13B7D5001A640E /* UnitButtonView.swift in Sources */, 028F9F39293A452B00DE65D0 /* ResetPassword.swift in Sources */, 0233D56F2AF13EB200BAC8BD /* StarRatingView.swift in Sources */, + 029A13302C2479E7005FB830 /* OfflineSyncInteractor.swift in Sources */, 0604C9AA2B22FACF00AD5DBF /* UIComponentsConfig.swift in Sources */, 027BD3B82909476200392132 /* DismissKeyboardTapViewModifier.swift in Sources */, - 024BE3DF29B2615500BCDEE2 /* CGColorExtension.swift in Sources */, 0770DE6128D0B2CB006D8A5D /* Assets.swift in Sources */, 07E0939F2B308D2800F1E4B2 /* Data_Certificate.swift in Sources */, 020D72F42BB76DFE00773319 /* VisualEffectView.swift in Sources */, @@ -1229,7 +1278,6 @@ isa = PBXVariantGroup; children = ( 0770DE5C28D0B209006D8A5D /* en */, - 02ED50CB29A64B84008341CD /* uk */, ); name = Localizable.strings; sourceTree = ""; @@ -1309,14 +1357,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1338,13 +1386,14 @@ }; 02DD1C9B29E80CE400F35DCE /* DebugStage */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8F3B171E9FA5E6F40B4890A8 /* Pods-App-Core-CoreTests.debugstage.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1424,14 +1473,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1452,13 +1501,14 @@ }; 02DD1C9E29E80CED00F35DCE /* ReleaseStage */ = { isa = XCBuildConfiguration; + baseConfigurationReference = FB6C49AC95A27A1222AD0F06 /* Pods-App-Core-CoreTests.releasestage.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1473,13 +1523,14 @@ }; 07169470296D996900E3DED6 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 5CEFA8766C44C519B86C681D /* Pods-App-Core-CoreTests.debug.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1495,13 +1546,14 @@ }; 07169471296D996900E3DED6 /* DebugProd */ = { isa = XCBuildConfiguration; + baseConfigurationReference = F4E50CE1DB6AA77E9B5D09EF /* Pods-App-Core-CoreTests.debugprod.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1517,13 +1569,14 @@ }; 07169472296D996900E3DED6 /* DebugDev */ = { isa = XCBuildConfiguration; + baseConfigurationReference = DD27D6BF5EB15C6C8B66969A /* Pods-App-Core-CoreTests.debugdev.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1539,13 +1592,14 @@ }; 07169473296D996900E3DED6 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9E0B33614CBD791307FFDEAE /* Pods-App-Core-CoreTests.release.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1560,13 +1614,14 @@ }; 07169474296D996900E3DED6 /* ReleaseProd */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 043DD0B526F919DFA1C5E600 /* Pods-App-Core-CoreTests.releaseprod.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1581,13 +1636,14 @@ }; 07169475296D996900E3DED6 /* ReleaseDev */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 3C63D5D2247C793C259341B8 /* Pods-App-Core-CoreTests.releasedev.xcconfig */; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1672,14 +1728,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1765,14 +1821,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1863,14 +1919,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1956,14 +2012,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2112,14 +2168,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2147,14 +2203,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2224,28 +2280,20 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 025EF2F42971740000B838AB /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */ = { + 02AA27922C2C1B61006F5B6A /* XCRemoteSwiftPackageReference "ZipArchive" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SvenTiigi/YouTubePlayerKit"; + repositoryURL = "https://github.com/ZipArchive/ZipArchive.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.8.0; + minimumVersion = 2.5.5; }; }; - 142EDD6A2B831D1400F9F320 /* XCRemoteSwiftPackageReference "ios-branch-sdk-spm" */ = { + CE924BE42CD8F8E4000137CA /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/BranchMetrics/ios-branch-sdk-spm"; + repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.2.0; - }; - }; - BA8FA65E2AD574D700EA029A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/google/GoogleSignIn-iOS"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 7.0.0; + kind = exactVersion; + version = 1.0.0; }; }; BA8FA6712AD6ABA300EA029A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */ = { @@ -2253,31 +2301,24 @@ repositoryURL = "https://github.com/facebook/facebook-ios-sdk"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 14.1.0; + minimumVersion = 16.3.1; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 025EF2F52971740000B838AB /* YouTubePlayerKit */ = { - isa = XCSwiftPackageProductDependency; - package = 025EF2F42971740000B838AB /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */; - productName = YouTubePlayerKit; - }; - 142EDD6B2B831D1400F9F320 /* BranchSDK */ = { + 02AA27932C2C1B88006F5B6A /* ZipArchive */ = { isa = XCSwiftPackageProductDependency; - package = 142EDD6A2B831D1400F9F320 /* XCRemoteSwiftPackageReference "ios-branch-sdk-spm" */; - productName = BranchSDK; + package = 02AA27922C2C1B61006F5B6A /* XCRemoteSwiftPackageReference "ZipArchive" */; + productName = ZipArchive; }; - BA8FA66B2AD59BBC00EA029A /* GoogleSignIn */ = { + CE57127B2CD109DB00D4AB17 /* OEXFoundation */ = { isa = XCSwiftPackageProductDependency; - package = BA8FA65E2AD574D700EA029A /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */; - productName = GoogleSignIn; + productName = OEXFoundation; }; - BAF0D4CA2AD6AE14007AC334 /* FacebookLogin */ = { + CE7CAF382CC1561E00E0AC9D /* OEXFoundation */ = { isa = XCSwiftPackageProductDependency; - package = BA8FA6712AD6ABA300EA029A /* XCRemoteSwiftPackageReference "facebook-ios-sdk" */; - productName = FacebookLogin; + productName = OEXFoundation; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Core/Core/Analytics/CoreAnalytics.swift b/Core/Core/Analytics/CoreAnalytics.swift index c7d1eca7c..006b41c7d 100644 --- a/Core/Core/Analytics/CoreAnalytics.swift +++ b/Core/Core/Analytics/CoreAnalytics.swift @@ -11,6 +11,8 @@ import Foundation public protocol CoreAnalytics { func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) + func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) + func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) func videoQualityChanged( _ event: AnalyticsEvent, @@ -28,6 +30,14 @@ public extension CoreAnalytics { func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { trackEvent(event, biValue: biValue, parameters: nil) } + + func trackScreenEvent(_ event: AnalyticsEvent) { + trackScreenEvent(event, parameters: nil) + } + + func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + trackScreenEvent(event, biValue: biValue, parameters: nil) + } } #if DEBUG @@ -35,6 +45,8 @@ public class CoreAnalyticsMock: CoreAnalytics { public init() {} public func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]? = nil) {} public func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) {} + public func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) {} + public func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) {} public func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String? = nil, rating: Int? = 0) {} public func videoQualityChanged( _ event: AnalyticsEvent, @@ -100,6 +112,7 @@ public enum AnalyticsEvent: String { case finishVerticalBackToOutlineClicked = "Course:Unit Finish Back To Outline Clicked" case courseOutlineCourseTabClicked = "Course:Home Tab" case courseOutlineVideosTabClicked = "Course:Videos Tab" + case courseOutlineOfflineTabClicked = "Course:Offline Tab" case courseOutlineDatesTabClicked = "Course:Dates Tab" case courseOutlineDiscussionTabClicked = "Course:Discussion Tab" case courseOutlineHandoutsTabClicked = "Course:Handouts Tab" @@ -124,6 +137,10 @@ public enum AnalyticsEvent: String { case whatnewPopup = "WhatsNew:Pop up Viewed" case whatnewDone = "WhatsNew:Done" case whatnewClose = "WhatsNew:Close" + case logistration = "Logistration" + case logistrationSignIn = "Logistration:Sign In" + case logistrationRegister = "Logistration:Register" + case profileEdit = "Profile:Edit Profile" } public enum EventBIValue: String { @@ -173,6 +190,7 @@ public enum EventBIValue: String { case bulkDeleteVideosSubsection = "edx.bi.app.video.delete.subsection" case dashboardCourseClicked = "edx.bi.app.course.dashboard" case courseOutlineVideosTabClicked = "edx.bi.app.course.video_tab" + case courseOutlineOfflineTabClicked = "edx.bi.app.course.offline_tab" case courseOutlineDatesTabClicked = "edx.bi.app.course.dates_tab" case courseOutlineDiscussionTabClicked = "edx.bi.app.course.discussion_tab" case courseOutlineHandoutsTabClicked = "edx.bi.app.course.handouts_tab" @@ -205,6 +223,10 @@ public enum EventBIValue: String { case whatnewPopup = "edx.bi.app.whats_new.popup.viewed" case whatnewDone = "edx.bi.app.whats_new.done" case whatnewClose = "edx.bi.app.whats_new.close" + case logistration = "edx.bi.app.logistration" + case logistrationSignIn = "edx.bi.app.logistration.signin" + case logistrationRegister = "edx.bi.app.logistration.register" + case profileEdit = "edx.bi.app.profile.edit" } public struct EventParamKey { diff --git a/Theme/Theme/Assets.xcassets/Auth/Contents.json b/Core/Core/Assets.xcassets/Calendar/Contents.json similarity index 100% rename from Theme/Theme/Assets.xcassets/Auth/Contents.json rename to Core/Core/Assets.xcassets/Calendar/Contents.json diff --git a/Core/Core/Assets.xcassets/Calendar/calendarAccess.imageset/Contents.json b/Core/Core/Assets.xcassets/Calendar/calendarAccess.imageset/Contents.json new file mode 100644 index 000000000..b78b96492 --- /dev/null +++ b/Core/Core/Assets.xcassets/Calendar/calendarAccess.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "calendarAccess.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Calendar/calendarAccess.imageset/calendarAccess.svg b/Core/Core/Assets.xcassets/Calendar/calendarAccess.imageset/calendarAccess.svg new file mode 100644 index 000000000..d80a2356a --- /dev/null +++ b/Core/Core/Assets.xcassets/Calendar/calendarAccess.imageset/calendarAccess.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/Calendar/syncFailed.imageset/Contents.json b/Core/Core/Assets.xcassets/Calendar/syncFailed.imageset/Contents.json new file mode 100644 index 000000000..6ff388e4c --- /dev/null +++ b/Core/Core/Assets.xcassets/Calendar/syncFailed.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "syncFailed.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Calendar/syncFailed.imageset/syncFailed.svg b/Core/Core/Assets.xcassets/Calendar/syncFailed.imageset/syncFailed.svg new file mode 100644 index 000000000..fe6e39f14 --- /dev/null +++ b/Core/Core/Assets.xcassets/Calendar/syncFailed.imageset/syncFailed.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/Calendar/syncOffline.imageset/Contents.json b/Core/Core/Assets.xcassets/Calendar/syncOffline.imageset/Contents.json new file mode 100644 index 000000000..1a75410c4 --- /dev/null +++ b/Core/Core/Assets.xcassets/Calendar/syncOffline.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "syncOffline.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Calendar/syncOffline.imageset/syncOffline.svg b/Core/Core/Assets.xcassets/Calendar/syncOffline.imageset/syncOffline.svg new file mode 100644 index 000000000..6c7bec7f2 --- /dev/null +++ b/Core/Core/Assets.xcassets/Calendar/syncOffline.imageset/syncOffline.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/Calendar/synced.imageset/Contents.json b/Core/Core/Assets.xcassets/Calendar/synced.imageset/Contents.json new file mode 100644 index 000000000..5d1255461 --- /dev/null +++ b/Core/Core/Assets.xcassets/Calendar/synced.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "synced.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/Calendar/synced.imageset/synced.svg b/Core/Core/Assets.xcassets/Calendar/synced.imageset/synced.svg new file mode 100644 index 000000000..71652aafd --- /dev/null +++ b/Core/Core/Assets.xcassets/Calendar/synced.imageset/synced.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/noVideos.imageset/Contents.json b/Core/Core/Assets.xcassets/CourseNavigationBar/noVideos.imageset/Contents.json new file mode 100644 index 000000000..48bb67ce1 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/noVideos.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "noVideos.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Core/Core/Assets.xcassets/CourseNavigationBar/noVideos.imageset/noVideos.svg b/Core/Core/Assets.xcassets/CourseNavigationBar/noVideos.imageset/noVideos.svg new file mode 100644 index 000000000..07b71b885 --- /dev/null +++ b/Core/Core/Assets.xcassets/CourseNavigationBar/noVideos.imageset/noVideos.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Contents.json b/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Contents.json index d1927cffc..41ea480fe 100644 --- a/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Frame-17.svg", + "filename" : "deleteDownloading.svg", "idiom" : "universal" } ], diff --git a/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Frame-17.svg b/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Frame-17.svg deleted file mode 100644 index 0ae948676..000000000 --- a/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/Frame-17.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/deleteDownloading.svg b/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/deleteDownloading.svg new file mode 100644 index 000000000..ed2659aab --- /dev/null +++ b/Core/Core/Assets.xcassets/DownloadManager/deleteDownloading.imageset/deleteDownloading.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json index 866687bad..672c958c5 100644 --- a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Contents.json @@ -1,12 +1,25 @@ { "images" : [ { - "filename" : "Frame-16.svg", + "filename" : "download_light.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "download_dark.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Frame-16.svg b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Frame-16.svg deleted file mode 100644 index 24d291489..000000000 --- a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/Frame-16.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_dark.svg b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_dark.svg new file mode 100644 index 000000000..8a29b74a2 --- /dev/null +++ b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_light.svg b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_light.svg new file mode 100644 index 000000000..1f933d639 --- /dev/null +++ b/Core/Core/Assets.xcassets/DownloadManager/startDownloading.imageset/download_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/Handouts/noAnnouncements.imageset/Contents.json b/Core/Core/Assets.xcassets/Handouts/noAnnouncements.imageset/Contents.json new file mode 100644 index 000000000..d92ccd5d2 --- /dev/null +++ b/Core/Core/Assets.xcassets/Handouts/noAnnouncements.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "noAnnouncements.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Core/Core/Assets.xcassets/Handouts/noAnnouncements.imageset/noAnnouncements.svg b/Core/Core/Assets.xcassets/Handouts/noAnnouncements.imageset/noAnnouncements.svg new file mode 100644 index 000000000..750c81c0a --- /dev/null +++ b/Core/Core/Assets.xcassets/Handouts/noAnnouncements.imageset/noAnnouncements.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Core/Core/Assets.xcassets/Handouts/noHandouts.imageset/Contents.json b/Core/Core/Assets.xcassets/Handouts/noHandouts.imageset/Contents.json new file mode 100644 index 000000000..5a65a06fd --- /dev/null +++ b/Core/Core/Assets.xcassets/Handouts/noHandouts.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "noHandouts.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Core/Core/Assets.xcassets/Handouts/noHandouts.imageset/noHandouts.svg b/Core/Core/Assets.xcassets/Handouts/noHandouts.imageset/noHandouts.svg new file mode 100644 index 000000000..870076e9a --- /dev/null +++ b/Core/Core/Assets.xcassets/Handouts/noHandouts.imageset/noHandouts.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/Contents.json b/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/Contents.json new file mode 100644 index 000000000..718131171 --- /dev/null +++ b/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "learn filled.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/learn filled.svg b/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/learn filled.svg new file mode 100644 index 000000000..c961205bc --- /dev/null +++ b/Core/Core/Assets.xcassets/NavigationBar/learn.imageset/learn filled.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Contents.json b/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Contents.json new file mode 100644 index 000000000..9a8a529ea --- /dev/null +++ b/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "deleteAccount.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/deleteAccount.svg b/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/deleteAccount.svg new file mode 100644 index 000000000..9c2a082f2 --- /dev/null +++ b/Core/Core/Assets.xcassets/Profile/deleteAccount.imageset/deleteAccount.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/arrowLeft.imageset/Contents.json b/Core/Core/Assets.xcassets/arrowLeft.imageset/Contents.json index cfa90a49f..117428b1d 100644 --- a/Core/Core/Assets.xcassets/arrowLeft.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/arrowLeft.imageset/Contents.json @@ -2,7 +2,8 @@ "images" : [ { "filename" : "icon left.svg", - "idiom" : "universal" + "idiom" : "universal", + "language-direction" : "left-to-right" }, { "appearances" : [ @@ -12,7 +13,8 @@ } ], "filename" : "icon left-2.svg", - "idiom" : "universal" + "idiom" : "universal", + "language-direction" : "left-to-right" } ], "info" : { diff --git a/Core/Core/Assets.xcassets/arrowRight16.imageset/Contents.json b/Core/Core/Assets.xcassets/arrowRight16.imageset/Contents.json index 2d22dfa63..bbe56d546 100644 --- a/Core/Core/Assets.xcassets/arrowRight16.imageset/Contents.json +++ b/Core/Core/Assets.xcassets/arrowRight16.imageset/Contents.json @@ -2,7 +2,8 @@ "images" : [ { "filename" : "arrowRight16.svg", - "idiom" : "universal" + "idiom" : "universal", + "language-direction" : "left-to-right" } ], "info" : { diff --git a/Core/Core/Assets.xcassets/calendarSyncIcon.imageset/Contents.json b/Core/Core/Assets.xcassets/calendarSyncIcon.imageset/Contents.json new file mode 100644 index 000000000..d75d6b0b4 --- /dev/null +++ b/Core/Core/Assets.xcassets/calendarSyncIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "calendarSyncIcon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/calendarSyncIcon.imageset/calendarSyncIcon.svg b/Core/Core/Assets.xcassets/calendarSyncIcon.imageset/calendarSyncIcon.svg new file mode 100644 index 000000000..7708fa304 --- /dev/null +++ b/Core/Core/Assets.xcassets/calendarSyncIcon.imageset/calendarSyncIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Core/Core/Assets.xcassets/check_circle.imageset/Contents.json b/Core/Core/Assets.xcassets/check_circle.imageset/Contents.json new file mode 100644 index 000000000..5a764afa0 --- /dev/null +++ b/Core/Core/Assets.xcassets/check_circle.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "check_circle.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/check_circle.imageset/check_circle.svg b/Core/Core/Assets.xcassets/check_circle.imageset/check_circle.svg new file mode 100644 index 000000000..d95b7f0fe --- /dev/null +++ b/Core/Core/Assets.xcassets/check_circle.imageset/check_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json b/Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json new file mode 100644 index 000000000..a21ea6e5d --- /dev/null +++ b/Core/Core/Assets.xcassets/chevron_right.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "chevron_right.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg b/Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg new file mode 100644 index 000000000..e951c4282 --- /dev/null +++ b/Core/Core/Assets.xcassets/chevron_right.imageset/chevron_right.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/download.imageset/Contents.json b/Core/Core/Assets.xcassets/download.imageset/Contents.json new file mode 100644 index 000000000..b9e339ad9 --- /dev/null +++ b/Core/Core/Assets.xcassets/download.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "download.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/download.imageset/download.svg b/Core/Core/Assets.xcassets/download.imageset/download.svg new file mode 100644 index 000000000..94fb61149 --- /dev/null +++ b/Core/Core/Assets.xcassets/download.imageset/download.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/finished_sequence.imageset/Contents.json b/Core/Core/Assets.xcassets/finished_sequence.imageset/Contents.json new file mode 100644 index 000000000..9d0a51bf3 --- /dev/null +++ b/Core/Core/Assets.xcassets/finished_sequence.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "finished_sequence.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/finished_sequence.imageset/finished_sequence.svg b/Core/Core/Assets.xcassets/finished_sequence.imageset/finished_sequence.svg new file mode 100644 index 000000000..51ed61934 --- /dev/null +++ b/Core/Core/Assets.xcassets/finished_sequence.imageset/finished_sequence.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/information.imageset/Contents.json b/Core/Core/Assets.xcassets/information.imageset/Contents.json new file mode 100644 index 000000000..702db438f --- /dev/null +++ b/Core/Core/Assets.xcassets/information.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Vector.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Core/Core/Assets.xcassets/information.imageset/Vector.svg b/Core/Core/Assets.xcassets/information.imageset/Vector.svg new file mode 100644 index 000000000..93a8bd6ce --- /dev/null +++ b/Core/Core/Assets.xcassets/information.imageset/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/learn_empty.imageset/Contents.json b/Core/Core/Assets.xcassets/learn_empty.imageset/Contents.json new file mode 100644 index 000000000..f823d5953 --- /dev/null +++ b/Core/Core/Assets.xcassets/learn_empty.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "learn_big.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/learn_empty.imageset/learn_big.svg b/Core/Core/Assets.xcassets/learn_empty.imageset/learn_big.svg new file mode 100644 index 000000000..a1874861e --- /dev/null +++ b/Core/Core/Assets.xcassets/learn_empty.imageset/learn_big.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/remove.imageset/Contents.json b/Core/Core/Assets.xcassets/remove.imageset/Contents.json new file mode 100644 index 000000000..d72a4557b --- /dev/null +++ b/Core/Core/Assets.xcassets/remove.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "remove.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/remove.imageset/remove.svg b/Core/Core/Assets.xcassets/remove.imageset/remove.svg new file mode 100644 index 000000000..a896b5a5a --- /dev/null +++ b/Core/Core/Assets.xcassets/remove.imageset/remove.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/report_octagon.imageset/Contents.json b/Core/Core/Assets.xcassets/report_octagon.imageset/Contents.json new file mode 100644 index 000000000..c24699e07 --- /dev/null +++ b/Core/Core/Assets.xcassets/report_octagon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "report.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/report_octagon.imageset/report.svg b/Core/Core/Assets.xcassets/report_octagon.imageset/report.svg new file mode 100644 index 000000000..4eeadf69b --- /dev/null +++ b/Core/Core/Assets.xcassets/report_octagon.imageset/report.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/resumeCourse.imageset/Contents.json b/Core/Core/Assets.xcassets/resumeCourse.imageset/Contents.json new file mode 100644 index 000000000..1ab6cc7ba --- /dev/null +++ b/Core/Core/Assets.xcassets/resumeCourse.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "resumeCourse.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/resumeCourse.imageset/resumeCourse.svg b/Core/Core/Assets.xcassets/resumeCourse.imageset/resumeCourse.svg new file mode 100644 index 000000000..0af03cb0c --- /dev/null +++ b/Core/Core/Assets.xcassets/resumeCourse.imageset/resumeCourse.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/Assets.xcassets/settings.imageset/Contents.json b/Core/Core/Assets.xcassets/settings.imageset/Contents.json new file mode 100644 index 000000000..30cb38b07 --- /dev/null +++ b/Core/Core/Assets.xcassets/settings.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icon-manage_accounts.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/settings.imageset/icon-manage_accounts.svg b/Core/Core/Assets.xcassets/settings.imageset/icon-manage_accounts.svg new file mode 100644 index 000000000..5cf416fb2 --- /dev/null +++ b/Core/Core/Assets.xcassets/settings.imageset/icon-manage_accounts.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Core/Core/Assets.xcassets/viewAll.imageset/Contents.json b/Core/Core/Assets.xcassets/viewAll.imageset/Contents.json new file mode 100644 index 000000000..b044a6ae9 --- /dev/null +++ b/Core/Core/Assets.xcassets/viewAll.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "viewAll.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Core/Core/Assets.xcassets/viewAll.imageset/viewAll.svg b/Core/Core/Assets.xcassets/viewAll.imageset/viewAll.svg new file mode 100644 index 000000000..da32ef8c1 --- /dev/null +++ b/Core/Core/Assets.xcassets/viewAll.imageset/viewAll.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Core/Core/Assets.xcassets/visibility.imageset/Contents.json b/Core/Core/Assets.xcassets/visibility.imageset/Contents.json new file mode 100644 index 000000000..af9f4586d --- /dev/null +++ b/Core/Core/Assets.xcassets/visibility.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "visibility.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Core/Core/Assets.xcassets/visibility.imageset/visibility.svg b/Core/Core/Assets.xcassets/visibility.imageset/visibility.svg new file mode 100644 index 000000000..2710fa5d1 --- /dev/null +++ b/Core/Core/Assets.xcassets/visibility.imageset/visibility.svg @@ -0,0 +1,3 @@ + + + diff --git a/Core/Core/AvoidingHelpers/Scroller/KeyboardScroller.swift b/Core/Core/AvoidingHelpers/Scroller/KeyboardScroller.swift index ff5c17c11..2d5fe602e 100644 --- a/Core/Core/AvoidingHelpers/Scroller/KeyboardScroller.swift +++ b/Core/Core/AvoidingHelpers/Scroller/KeyboardScroller.swift @@ -1,6 +1,7 @@ // import UIKit +import OEXFoundation final class KeyboardScroller { static func scroll( @@ -57,7 +58,7 @@ final class KeyboardScroller { self.options = options self.partialAvoidingPadding = partialAvoidingPadding - globalWindow = UIApplication.shared.keyWindow ?? UIWindow() + globalWindow = UIApplication.shared.oexKeyWindow ?? UIWindow() calculateGlobalFrames() } diff --git a/Core/Core/Configuration/BaseRouter.swift b/Core/Core/Configuration/BaseRouter.swift index e2a2a714b..4f2ae5bba 100644 --- a/Core/Core/Configuration/BaseRouter.swift +++ b/Core/Core/Configuration/BaseRouter.swift @@ -34,6 +34,8 @@ public protocol BaseRouter { func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) func showWebBrowser(title: String, url: URL) + + func showSSOWebBrowser(title: String) func presentAlert( alertTitle: String, @@ -100,6 +102,8 @@ open class BaseRouterMock: BaseRouter { public func removeLastView(controllers: Int) {} public func showWebBrowser(title: String, url: URL) {} + + public func showSSOWebBrowser(title: String) {} public func presentAlert( alertTitle: String, diff --git a/Core/Core/Configuration/Config/AgreementConfig.swift b/Core/Core/Configuration/Config/AgreementConfig.swift index 6dc4d518e..df29f8d3f 100644 --- a/Core/Core/Configuration/Config/AgreementConfig.swift +++ b/Core/Core/Configuration/Config/AgreementConfig.swift @@ -6,6 +6,7 @@ // import Foundation +import OEXFoundation private enum AgreementKeys: String, RawStringExtractable { case privacyPolicyURL = "PRIVACY_POLICY_URL" @@ -42,12 +43,7 @@ public class AgreementConfig: NSObject { } private func completePath(url: String) -> String { - let langCode: String - if #available(iOS 16, *) { - langCode = Locale.current.language.languageCode?.identifier ?? "" - } else { - langCode = Locale.current.languageCode ?? "" - } + let langCode = Locale.current.language.languageCode?.identifier ?? "" if let supportedLanguages = supportedLanguages, !supportedLanguages.contains(langCode) { diff --git a/Core/Core/Configuration/Config/BranchConfig.swift b/Core/Core/Configuration/Config/BranchConfig.swift index 6f2a785b6..ffc44793a 100644 --- a/Core/Core/Configuration/Config/BranchConfig.swift +++ b/Core/Core/Configuration/Config/BranchConfig.swift @@ -6,6 +6,7 @@ // import Foundation +import OEXFoundation private enum BranchKeys: String, RawStringExtractable { case enabled = "ENABLED" diff --git a/Core/Core/Configuration/Config/BrazeConfig.swift b/Core/Core/Configuration/Config/BrazeConfig.swift index 0cbc10db8..9a882a1c5 100644 --- a/Core/Core/Configuration/Config/BrazeConfig.swift +++ b/Core/Core/Configuration/Config/BrazeConfig.swift @@ -6,6 +6,7 @@ // import Foundation +import OEXFoundation private enum BrazeKeys: String, RawStringExtractable { case enabled = "ENABLED" diff --git a/Core/Core/Configuration/Config/Config.swift b/Core/Core/Configuration/Config/Config.swift index 0c3aa5782..dbd29f0de 100644 --- a/Core/Core/Configuration/Config/Config.swift +++ b/Core/Core/Configuration/Config/Config.swift @@ -7,8 +7,12 @@ import Foundation +//sourcery: AutoMockable public protocol ConfigProtocol { var baseURL: URL { get } + var baseSSOURL: URL { get } + var ssoFinishedURL: URL { get } + var ssoButtonTitle: [String: Any] { get } var oAuthClientId: String { get } var tokenType: TokenType { get } var feedbackEmail: String { get } @@ -25,9 +29,9 @@ public protocol ConfigProtocol { var theme: ThemeConfig { get } var uiComponents: UIComponentsConfig { get } var discovery: DiscoveryConfig { get } + var dashboard: DashboardConfig { get } var braze: BrazeConfig { get } var branch: BranchConfig { get } - var segment: SegmentConfig { get } var program: DiscoveryConfig { get } var URIScheme: String { get } } @@ -39,6 +43,9 @@ public enum TokenType: String { private enum ConfigKeys: String { case baseURL = "API_HOST_URL" + case ssoBaseURL = "SSO_URL" + case ssoFinishedURL = "SSO_FINISHED_URL" + case ssoButtonTitle = "SSO_BUTTON_TITLE" case oAuthClientID = "OAUTH_CLIENT_ID" case tokenType = "TOKEN_TYPE" case feedbackEmailAddress = "FEEDBACK_EMAIL_ADDRESS" @@ -118,6 +125,29 @@ extension Config: ConfigProtocol { return url } + public var baseSSOURL: URL { + guard let urlString = string(for: ConfigKeys.ssoBaseURL.rawValue), + let url = URL(string: urlString) else { + fatalError("Unable to find SSO base url in config.") + } + return url + } + + public var ssoFinishedURL: URL { + guard let urlString = string(for: ConfigKeys.ssoFinishedURL.rawValue), + let url = URL(string: urlString) else { + fatalError("Unable to find SSO successful login url in config.") + } + return url + } + + public var ssoButtonTitle: [String: Any] { + guard let ssoButtonTitle = dict(for: ConfigKeys.ssoButtonTitle.rawValue) else { + return ["en": CoreLocalization.SignIn.logInWithSsoBtn] + } + return ssoButtonTitle + } + public var oAuthClientId: String { guard let clientID = string(for: ConfigKeys.oAuthClientID.rawValue) else { fatalError("Unable to find OAuth ClientID in config.") @@ -166,6 +196,7 @@ extension Config: ConfigProtocol { public class ConfigMock: Config { private let config: [String: Any] = [ "API_HOST_URL": "https://www.example.com", + "SSO_URL" : "https://www.example.com", "OAUTH_CLIENT_ID": "oauth_client_id", "FEEDBACK_EMAIL_ADDRESS": "example@mail.com", "PLATFORM_NAME": "OpenEdx", diff --git a/Core/Core/Configuration/Config/DashboardConfig.swift b/Core/Core/Configuration/Config/DashboardConfig.swift new file mode 100644 index 000000000..c87045ced --- /dev/null +++ b/Core/Core/Configuration/Config/DashboardConfig.swift @@ -0,0 +1,35 @@ +// +// DashboardConfig.swift +// Core +// +// Created by  Stepanok Ivan on 23.04.2024. +// + +import Foundation +import OEXFoundation + +public enum DashboardConfigType: String { + case gallery + case list +} + +private enum DashboardKeys: String, RawStringExtractable { + case dashboardType = "TYPE" +} + +public class DashboardConfig: NSObject { + public let type: DashboardConfigType + + init(dictionary: [String: AnyObject]) { + type = (dictionary[DashboardKeys.dashboardType] as? String).flatMap { + DashboardConfigType(rawValue: $0) + } ?? .gallery + } +} + +private let key = "DASHBOARD" +extension Config { + public var dashboard: DashboardConfig { + DashboardConfig(dictionary: self[key] as? [String: AnyObject] ?? [:]) + } +} diff --git a/Core/Core/Configuration/Config/DiscoveryConfig.swift b/Core/Core/Configuration/Config/DiscoveryConfig.swift index 884800441..a646658bb 100644 --- a/Core/Core/Configuration/Config/DiscoveryConfig.swift +++ b/Core/Core/Configuration/Config/DiscoveryConfig.swift @@ -6,6 +6,7 @@ // import Foundation +import OEXFoundation public enum DiscoveryConfigType: String { case native @@ -36,11 +37,16 @@ public class DiscoveryWebviewConfig: NSObject { public class DiscoveryConfig: NSObject { public let type: DiscoveryConfigType public let webview: DiscoveryWebviewConfig + public var isWebViewConfigured: Bool { + get { + return type == .webview && webview.baseURL != nil + } + } init(dictionary: [String: AnyObject]) { type = (dictionary[DiscoveryKeys.discoveryType] as? String).flatMap { DiscoveryConfigType(rawValue: $0) - } ?? .none + } ?? .native webview = DiscoveryWebviewConfig(dictionary: dictionary[DiscoveryKeys.webview] as? [String: AnyObject] ?? [:]) } diff --git a/Core/Core/Configuration/Config/MicrosoftConfig.swift b/Core/Core/Configuration/Config/MicrosoftConfig.swift index 4175a53e2..eb3d4d825 100644 --- a/Core/Core/Configuration/Config/MicrosoftConfig.swift +++ b/Core/Core/Configuration/Config/MicrosoftConfig.swift @@ -14,7 +14,7 @@ private enum MicrosoftKeys: String { public final class MicrosoftConfig: NSObject { public var enabled: Bool = false - private(set) var appID: String? + public private(set) var appID: String? private var requiredKeysAvailable: Bool { return appID != nil diff --git a/Core/Core/Configuration/Config/SegmentConfig.swift b/Core/Core/Configuration/Config/SegmentConfig.swift deleted file mode 100644 index 937a78015..000000000 --- a/Core/Core/Configuration/Config/SegmentConfig.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// SegmentConfig.swift -// Core -// -// Created by Anton Yarmolenka on 02/02/2024. -// - -import Foundation - -private enum SegmentKeys: String, RawStringExtractable { - case enabled = "ENABLED" - case writeKey = "SEGMENT_IO_WRITE_KEY" -} - -public final class SegmentConfig: NSObject { - public var enabled: Bool = false - public var writeKey: String = "" - - init(dictionary: [String: AnyObject]) { - super.init() - writeKey = dictionary[SegmentKeys.writeKey] as? String ?? "" - enabled = dictionary[SegmentKeys.enabled] as? Bool == true && !writeKey.isEmpty - } -} - -private let segmentKey = "SEGMENT_IO" -extension Config { - public var segment: SegmentConfig { - SegmentConfig(dictionary: self[segmentKey] as? [String: AnyObject] ?? [:]) - } -} diff --git a/Core/Core/Configuration/Config/UIComponentsConfig.swift b/Core/Core/Configuration/Config/UIComponentsConfig.swift index cb5ce3e68..c62b20774 100644 --- a/Core/Core/Configuration/Config/UIComponentsConfig.swift +++ b/Core/Core/Configuration/Config/UIComponentsConfig.swift @@ -6,19 +6,29 @@ // import Foundation +import OEXFoundation private enum Keys: String, RawStringExtractable { - case courseNestedListEnabled = "COURSE_NESTED_LIST_ENABLED" + case courseDropDownNavigationEnabled = "COURSE_DROPDOWN_NAVIGATION_ENABLED" case courseUnitProgressEnabled = "COURSE_UNIT_PROGRESS_ENABLED" + case loginRegistrationEnabled = "LOGIN_REGISTRATION_ENABLED" + case samlSSOLoginEnabled = "SAML_SSO_LOGIN_ENABLED" + case samlSSODefaultLoginButton = "SAML_SSO_DEFAULT_LOGIN_BUTTON" } public class UIComponentsConfig: NSObject { - public var courseNestedListEnabled: Bool + public var courseDropDownNavigationEnabled: Bool public var courseUnitProgressEnabled: Bool + public var loginRegistrationEnabled: Bool + public var samlSSOLoginEnabled: Bool + public var samlSSODefaultLoginButton: Bool init(dictionary: [String: Any]) { - courseNestedListEnabled = dictionary[Keys.courseNestedListEnabled] as? Bool ?? false + courseDropDownNavigationEnabled = dictionary[Keys.courseDropDownNavigationEnabled] as? Bool ?? false courseUnitProgressEnabled = dictionary[Keys.courseUnitProgressEnabled] as? Bool ?? false + loginRegistrationEnabled = dictionary[Keys.loginRegistrationEnabled] as? Bool ?? true + samlSSOLoginEnabled = dictionary[Keys.samlSSOLoginEnabled] as? Bool ?? false + samlSSODefaultLoginButton = dictionary[Keys.samlSSODefaultLoginButton] as? Bool ?? false super.init() } } diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index c1ea53d82..c69ee18b9 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -7,6 +7,7 @@ import Alamofire import Combine +import Foundation public enum InternetState { case reachable diff --git a/Core/Core/Data/CoreStorage.swift b/Core/Core/Data/CoreStorage.swift index b3a8faca0..021d32ec8 100644 --- a/Core/Core/Data/CoreStorage.swift +++ b/Core/Core/Data/CoreStorage.swift @@ -7,16 +7,20 @@ import Foundation +//sourcery: AutoMockable public protocol CoreStorage { var accessToken: String? {get set} var refreshToken: String? {get set} + var pushToken: String? {get set} var appleSignFullName: String? {get set} var appleSignEmail: String? {get set} - var cookiesDate: String? {get set} + var cookiesDate: Date? {get set} var reviewLastShownVersion: String? {get set} var lastReviewDate: Date? {get set} var user: DataLayer.User? {get set} var userSettings: UserSettings? {get set} + var resetAppSupportDirectoryUserData: Bool? {get set} + var useRelativeDates: Bool {get set} func clear() } @@ -24,13 +28,16 @@ public protocol CoreStorage { public class CoreStorageMock: CoreStorage { public var accessToken: String? public var refreshToken: String? + public var pushToken: String? public var appleSignFullName: String? public var appleSignEmail: String? - public var cookiesDate: String? + public var cookiesDate: Date? public var reviewLastShownVersion: String? public var lastReviewDate: Date? public var user: DataLayer.User? public var userSettings: UserSettings? + public var resetAppSupportDirectoryUserData: Bool? + public var useRelativeDates: Bool = true public func clear() {} public init() {} diff --git a/Course/Course/Data/Model/Data_CourseDates.swift b/Core/Core/Data/Model/Data_CourseDates.swift similarity index 86% rename from Course/Course/Data/Model/Data_CourseDates.swift rename to Core/Core/Data/Model/Data_CourseDates.swift index 2ef5d339b..e17f767ce 100644 --- a/Course/Course/Data/Model/Data_CourseDates.swift +++ b/Core/Core/Data/Model/Data_CourseDates.swift @@ -1,12 +1,11 @@ // // Data_CourseDates.swift -// Course +// Core // -// Created by Muhammad Umer on 10/18/23. +// Created by  Stepanok Ivan on 05.06.2024. // import Foundation -import Core public extension DataLayer { struct CourseDates: Codable { @@ -100,46 +99,46 @@ public extension DataLayer { case upgradeToResetBanner case resetDatesBanner - var header: String { + public var header: String { switch self { case .datesTabInfoBanner: - CourseLocalization.CourseDates.ResetDate.TabInfoBanner.header + CoreLocalization.CourseDates.ResetDate.TabInfoBanner.header case .upgradeToCompleteGradedBanner: - CourseLocalization.CourseDates.ResetDate.UpgradeToCompleteGradedBanner.header + CoreLocalization.CourseDates.ResetDate.UpgradeToCompleteGradedBanner.header case .upgradeToResetBanner: - CourseLocalization.CourseDates.ResetDate.UpgradeToResetBanner.header + CoreLocalization.CourseDates.ResetDate.UpgradeToResetBanner.header case .resetDatesBanner: - CourseLocalization.CourseDates.ResetDate.ResetDateBanner.header + CoreLocalization.CourseDates.ResetDate.ResetDateBanner.header } } - var body: String { + public var body: String { switch self { case .datesTabInfoBanner: - CourseLocalization.CourseDates.ResetDate.TabInfoBanner.body + CoreLocalization.CourseDates.ResetDate.TabInfoBanner.body case .upgradeToCompleteGradedBanner: - CourseLocalization.CourseDates.ResetDate.UpgradeToCompleteGradedBanner.body + CoreLocalization.CourseDates.ResetDate.UpgradeToCompleteGradedBanner.body case .upgradeToResetBanner: - CourseLocalization.CourseDates.ResetDate.UpgradeToResetBanner.body + CoreLocalization.CourseDates.ResetDate.UpgradeToResetBanner.body case .resetDatesBanner: - CourseLocalization.CourseDates.ResetDate.ResetDateBanner.body + CoreLocalization.CourseDates.ResetDate.ResetDateBanner.body } } - var buttonTitle: String { + public var buttonTitle: String { switch self { case .upgradeToCompleteGradedBanner, .upgradeToResetBanner: // Mobile payments are not implemented yet and to avoid breaking appstore guidelines, // upgrade button is hidden, which leads user to payments "" case .resetDatesBanner: - CourseLocalization.CourseDates.ResetDate.ResetDateBanner.button + CoreLocalization.CourseDates.ResetDate.ResetDateBanner.button default: "" } } - var analyticsBannerType: String { + public var analyticsBannerType: String { switch self { case .datesTabInfoBanner: "info" @@ -167,7 +166,7 @@ public extension DataLayer { } public extension DataLayer.CourseDates { - var domain: CourseDates { + func domain(useRelativeDates: Bool) -> CourseDates { return CourseDates( datesBannerInfo: DatesBannerInfo( missedDeadlines: datesBannerInfo?.missedDeadlines ?? false, @@ -187,7 +186,8 @@ public extension DataLayer.CourseDates { linkText: block.linkText ?? nil, title: block.title, extraInfo: block.extraInfo, - firstComponentBlockID: block.firstComponentBlockID) + firstComponentBlockID: block.firstComponentBlockID, + useRelativeDates: useRelativeDates) }, hasEnded: hasEnded, learnerIsFullAccess: learnerIsFullAccess, diff --git a/Core/Core/Data/Model/Data_Discovery.swift b/Core/Core/Data/Model/Data_Discovery.swift index cb8dd9be8..1d21e1b6d 100644 --- a/Core/Core/Data/Model/Data_Discovery.swift +++ b/Core/Core/Data/Model/Data_Discovery.swift @@ -108,14 +108,17 @@ public extension DataLayer.DiscoveryResponce { CourseItem(name: $0.name, org: $0.org, shortDescription: $0.shortDescription ?? "", imageURL: $0.media.image?.small ?? "", - isActive: nil, + hasAccess: true, courseStart: Date(iso8601: $0.start ?? ""), courseEnd: Date(iso8601: $0.end ?? ""), enrollmentStart: Date(iso8601: $0.enrollmentStart ?? ""), enrollmentEnd: Date(iso8601: $0.enrollmentEnd ?? ""), courseID: $0.courseID ?? "", numPages: pagination.numPages, - coursesCount: pagination.count) + coursesCount: pagination.count, + courseRawImage: $0.media.image?.raw, + progressEarned: 0, + progressPossible: 0) }) return listReady } diff --git a/Core/Core/Data/Model/Data_Dashboard.swift b/Core/Core/Data/Model/Data_Enrollments.swift similarity index 92% rename from Core/Core/Data/Model/Data_Dashboard.swift rename to Core/Core/Data/Model/Data_Enrollments.swift index d39d8aa2d..5b6f834b6 100644 --- a/Core/Core/Data/Model/Data_Dashboard.swift +++ b/Core/Core/Data/Model/Data_Enrollments.swift @@ -1,5 +1,5 @@ // -// Data_Dashboard.swift +// Data_Enrollments.swift // Core // // Created by  Stepanok Ivan on 24.03.2023. @@ -29,7 +29,7 @@ public extension DataLayer { public let numPages: Int? public let currentPage: Int? public let start: Int? - public let results: [Result] + public let results: [Enrollment] enum CodingKeys: String, CodingKey { case next @@ -48,7 +48,7 @@ public extension DataLayer { numPages: Int?, currentPage: Int?, start: Int?, - results: [Result] + results: [Enrollment] ) { self.next = next self.previous = previous @@ -60,14 +60,15 @@ public extension DataLayer { } } - // MARK: - Result - struct Result: Codable { + // MARK: - Enrollment + struct Enrollment: Codable { public let auditAccessExpires: String? public let created: String public let mode: Mode public let isActive: Bool public let course: DashboardCourse public let courseModes: [CourseMode] + public let progress: CourseProgress? enum CodingKeys: String, CodingKey { case auditAccessExpires = "audit_access_expires" @@ -76,6 +77,7 @@ public extension DataLayer { case isActive = "is_active" case course case courseModes = "course_modes" + case progress = "course_progress" } public init( @@ -84,7 +86,8 @@ public extension DataLayer { mode: Mode, isActive: Bool, course: DashboardCourse, - courseModes: [CourseMode] + courseModes: [CourseMode], + progress: CourseProgress? ) { self.auditAccessExpires = auditAccessExpires self.created = created @@ -92,6 +95,7 @@ public extension DataLayer { self.isActive = isActive self.course = course self.courseModes = courseModes + self.progress = progress } } @@ -244,7 +248,7 @@ public extension DataLayer.CourseEnrollments { org: course.org, shortDescription: "", imageURL: fullImageURL, - isActive: true, + hasAccess: course.coursewareAccess.hasAccess, courseStart: course.start != nil ? Date(iso8601: course.start!) : nil, courseEnd: course.end != nil ? Date(iso8601: course.end!) : nil, enrollmentStart: course.start != nil @@ -255,7 +259,10 @@ public extension DataLayer.CourseEnrollments { : nil, courseID: course.id, numPages: enrollments.numPages ?? 1, - coursesCount: enrollments.count ?? 0 + coursesCount: enrollments.count ?? 0, + courseRawImage: course.media.courseImage?.url, + progressEarned: 0, + progressPossible: 0 ) } } diff --git a/Core/Core/Data/Model/Data_EnrollmentsStatus.swift b/Core/Core/Data/Model/Data_EnrollmentsStatus.swift new file mode 100644 index 000000000..7b2cc76fd --- /dev/null +++ b/Core/Core/Data/Model/Data_EnrollmentsStatus.swift @@ -0,0 +1,31 @@ +// +// Data_EnrollmentsStatus.swift +// Core +// +// Created by  Stepanok Ivan on 28.05.2024. +// + +import Foundation + +extension DataLayer { + // MARK: - EnrollmentsStatusElement + public struct EnrollmentsStatusElement: Codable { + public let courseID: String? + public let courseName: String? + public let recentlyActive: Bool? + + public enum CodingKeys: String, CodingKey { + case courseID = "course_id" + case courseName = "course_name" + case recentlyActive = "recently_active" + } + + public init(courseID: String?, courseName: String?, recentlyActive: Bool?) { + self.courseID = courseID + self.courseName = courseName + self.recentlyActive = recentlyActive + } + } + + public typealias EnrollmentsStatus = [EnrollmentsStatusElement] +} diff --git a/Core/Core/Data/Model/Data_PrimaryEnrollment.swift b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift new file mode 100644 index 000000000..16af30373 --- /dev/null +++ b/Core/Core/Data/Model/Data_PrimaryEnrollment.swift @@ -0,0 +1,270 @@ +// +// Data_PrimaryEnrollment.swift +// Core +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import Foundation + +public extension DataLayer { + struct PrimaryEnrollment: Codable { + public let userTimezone: String? + public let enrollments: Enrollments? + public let primary: ActiveEnrollment? + + enum CodingKeys: String, CodingKey { + case userTimezone = "user_timezone" + case enrollments + case primary + } + + public init(userTimezone: String?, enrollments: Enrollments?, primary: ActiveEnrollment?) { + self.userTimezone = userTimezone + self.enrollments = enrollments + self.primary = primary + } + } + + // MARK: - Primary + struct ActiveEnrollment: Codable { + public let auditAccessExpires: String? + public let created: String? + public let mode: String? + public let isActive: Bool? + public let course: DashboardCourse? + public let certificate: DataLayer.Certificate? + public let courseModes: [CourseMode]? + public let courseStatus: CourseStatus? + public let progress: DataLayer.CourseProgress? + public let courseAssignments: CourseAssignments? + + enum CodingKeys: String, CodingKey { + case auditAccessExpires = "audit_access_expires" + case created + case mode + case isActive = "is_active" + case course + case certificate + case courseModes = "course_modes" + case courseStatus = "course_status" + case progress = "course_progress" + case courseAssignments = "course_assignments" + } + + public init( + auditAccessExpires: String?, + created: String?, + mode: String?, + isActive: Bool?, + course: DashboardCourse?, + certificate: DataLayer.Certificate?, + courseModes: [CourseMode]?, + courseStatus: CourseStatus?, + progress: CourseProgress?, + courseAssignments: CourseAssignments? + ) { + self.auditAccessExpires = auditAccessExpires + self.created = created + self.mode = mode + self.isActive = isActive + self.course = course + self.certificate = certificate + self.courseModes = courseModes + self.courseStatus = courseStatus + self.progress = progress + self.courseAssignments = courseAssignments + } + } + + // MARK: - CourseStatus + struct CourseStatus: Codable { + public let lastVisitedModuleID: String? + public let lastVisitedModulePath: [String]? + public let lastVisitedBlockID: String? + public let lastVisitedUnitDisplayName: String? + + enum CodingKeys: String, CodingKey { + case lastVisitedModuleID = "last_visited_module_id" + case lastVisitedModulePath = "last_visited_module_path" + case lastVisitedBlockID = "last_visited_block_id" + case lastVisitedUnitDisplayName = "last_visited_unit_display_name" + } + } + + // MARK: - CourseAssignments + struct CourseAssignments: Codable { + public let futureAssignments: [Assignment]? + public let pastAssignments: [Assignment]? + + enum CodingKeys: String, CodingKey { + case futureAssignments = "future_assignments" + case pastAssignments = "past_assignments" + } + + public init(futureAssignments: [Assignment]?, pastAssignments: [Assignment]?) { + self.futureAssignments = futureAssignments + self.pastAssignments = pastAssignments + } + } + + // MARK: - Assignment + struct Assignment: Codable { + public let assignmentType: String? + public let complete: Bool? + public let date: String? + public let dateType: String? + public let description: String? + public let learnerHasAccess: Bool? + public let link: String? + public let linkText: String? + public let title: String? + public let extraInfo: String? + public let firstComponentBlockID: String? + + enum CodingKeys: String, CodingKey { + case assignmentType = "assignment_type" + case complete + case date + case dateType = "date_type" + case description + case learnerHasAccess = "learner_has_access" + case link + case linkText = "link_text" + case title + case extraInfo = "extra_info" + case firstComponentBlockID = "first_component_block_id" + } + + public init( + assignmentType: String?, + complete: Bool?, + date: String?, + dateType: String?, + description: String?, + learnerHasAccess: Bool?, + link: String?, + linkText: String?, + title: String?, + extraInfo: String?, + firstComponentBlockID: String? + ) { + self.assignmentType = assignmentType + self.complete = complete + self.date = date + self.dateType = dateType + self.description = description + self.learnerHasAccess = learnerHasAccess + self.link = link + self.linkText = linkText + self.title = title + self.extraInfo = extraInfo + self.firstComponentBlockID = firstComponentBlockID + } + } + + // MARK: - CourseProgress + struct CourseProgress: Codable { + public let assignmentsCompleted: Int? + public let totalAssignmentsCount: Int? + + enum CodingKeys: String, CodingKey { + case assignmentsCompleted = "assignments_completed" + case totalAssignmentsCount = "total_assignments_count" + } + + public init(assignmentsCompleted: Int?, totalAssignmentsCount: Int?) { + self.assignmentsCompleted = assignmentsCompleted + self.totalAssignmentsCount = totalAssignmentsCount + } + } +} + +public extension DataLayer.PrimaryEnrollment { + + func domain(baseURL: String) -> PrimaryEnrollment { + let primaryCourse = createPrimaryCourse(from: self.primary, baseURL: baseURL) + let courses = createCourseItems(from: self.enrollments, baseURL: baseURL) + + return PrimaryEnrollment( + primaryCourse: primaryCourse, + courses: courses, + totalPages: enrollments?.numPages ?? 1, + count: enrollments?.count ?? 1 + ) + } + + private func createPrimaryCourse(from primary: DataLayer.ActiveEnrollment?, baseURL: String) -> PrimaryCourse? { + guard let primary = primary else { return nil } + + let futureAssignments = primary.courseAssignments?.futureAssignments ?? [] + let pastAssignments = primary.courseAssignments?.pastAssignments ?? [] + + return PrimaryCourse( + name: primary.course?.name ?? "", + org: primary.course?.org ?? "", + courseID: primary.course?.id ?? "", + hasAccess: primary.course?.coursewareAccess.hasAccess ?? true, + courseStart: primary.course?.start.flatMap { Date(iso8601: $0) }, + courseEnd: primary.course?.end.flatMap { Date(iso8601: $0) }, + courseBanner: baseURL + (primary.course?.media.courseImage?.url ?? ""), + futureAssignments: futureAssignments.map { createAssignment(from: $0) }, + pastAssignments: pastAssignments.map { createAssignment(from: $0) }, + progressEarned: primary.progress?.assignmentsCompleted ?? 0, + progressPossible: primary.progress?.totalAssignmentsCount ?? 0, + lastVisitedBlockID: primary.courseStatus?.lastVisitedBlockID, + resumeTitle: primary.courseStatus?.lastVisitedUnitDisplayName + ) + } + + private func createAssignment(from assignment: DataLayer.Assignment) -> Assignment { + return Assignment( + type: assignment.assignmentType ?? "", + title: assignment.title ?? "", + description: assignment.description ?? "", + date: Date(iso8601: assignment.date ?? ""), + complete: assignment.complete ?? false, + firstComponentBlockId: assignment.firstComponentBlockID + ) + } + + private func createCourseItems(from enrollments: DataLayer.Enrollments?, baseURL: String) -> [CourseItem] { + return enrollments?.results.map { + createCourseItem( + from: $0, + baseURL: baseURL, + numPages: enrollments?.numPages ?? 1, + count: enrollments?.count ?? 0 + ) + } ?? [] + } + + private func createCourseItem( + from enrollment: DataLayer.Enrollment, + baseURL: String, + numPages: Int, + count: Int + ) -> CourseItem { + let imageUrl = enrollment.course.media.courseImage?.url ?? "" + let encodedUrl = imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let fullImageURL = baseURL + encodedUrl + + return CourseItem( + name: enrollment.course.name, + org: enrollment.course.org, + shortDescription: "", + imageURL: fullImageURL, + hasAccess: enrollment.course.coursewareAccess.hasAccess, + courseStart: enrollment.course.start.flatMap { Date(iso8601: $0) }, + courseEnd: enrollment.course.end.flatMap { Date(iso8601: $0) }, + enrollmentStart: enrollment.course.start.flatMap { Date(iso8601: $0) }, + enrollmentEnd: enrollment.course.end.flatMap { Date(iso8601: $0) }, + courseID: enrollment.course.id, + numPages: numPages, + coursesCount: count, + courseRawImage: enrollment.course.media.image?.raw, + progressEarned: enrollment.progress?.assignmentsCompleted ?? 0, + progressPossible: enrollment.progress?.totalAssignmentsCount ?? 0 + ) + } +} diff --git a/Core/Core/Data/Model/Data_UserProfile.swift b/Core/Core/Data/Model/Data_UserProfile.swift index d3541cfd2..fe0e675bf 100644 --- a/Core/Core/Data/Model/Data_UserProfile.swift +++ b/Core/Core/Data/Model/Data_UserProfile.swift @@ -133,6 +133,7 @@ public extension DataLayer.UserProfile { country: country ?? "", spokenLanguage: languageProficiencies?[safe: 0]?.code ?? "", shortBiography: bio ?? "", - isFullProfile: accountPrivacy?.boolValue ?? true) + isFullProfile: accountPrivacy?.boolValue ?? true, + email: email ?? "") } } diff --git a/Core/Core/Data/Model/UserSettings.swift b/Core/Core/Data/Model/UserSettings.swift index 1b25e9d6c..52ce12557 100644 --- a/Core/Core/Data/Model/UserSettings.swift +++ b/Core/Core/Data/Model/UserSettings.swift @@ -32,6 +32,19 @@ public enum StreamingQuality: Codable { public var value: String? { return String(describing: self).components(separatedBy: "(").first } + + public var resolution: CGSize { + switch self { + case .auto: + return CGSize(width: 1280, height: 720) + case .low: + return CGSize(width: 640, height: 360) + case .medium: + return CGSize(width: 854, height: 480) + case .high: + return CGSize(width: 1280, height: 720) + } + } } public enum DownloadQuality: Codable, CaseIterable { diff --git a/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents index 2fd252f0d..acc66b8db 100644 --- a/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents +++ b/Core/Core/Data/Persistence/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -7,6 +7,7 @@ + @@ -19,4 +20,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift index 3a6ec2acf..b17c62a1f 100644 --- a/Core/Core/Data/Persistence/CorePersistenceProtocol.swift +++ b/Core/Core/Data/Persistence/CorePersistenceProtocol.swift @@ -8,20 +8,52 @@ import CoreData import Combine +//sourcery: AutoMockable public protocol CorePersistenceProtocol { func set(userId: Int) func getUserID() -> Int? func publisher() -> AnyPublisher - func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) - func nextBlockForDownloading() -> DownloadDataTask? + func addToDownloadQueue(tasks: [DownloadDataTask]) + func saveOfflineProgress(progress: OfflineProgress) + func loadProgress(for blockID: String) -> OfflineProgress? + func loadAllOfflineProgress() -> [OfflineProgress] + func deleteProgress(for blockID: String) + func deleteAllProgress() + + func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) async + func nextBlockForDownloading() async -> DownloadDataTask? func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) - func deleteDownloadDataTask(id: String) throws + func deleteDownloadDataTask(id: String) async throws func saveDownloadDataTask(_ task: DownloadDataTask) func downloadDataTask(for blockId: String) -> DownloadDataTask? - func downloadDataTask(for blockId: String, completion: @escaping (DownloadDataTask?) -> Void) - func getDownloadDataTasks(completion: @escaping ([DownloadDataTask]) -> Void) - func getDownloadDataTasksForCourse(_ courseId: String, completion: @escaping ([DownloadDataTask]) -> Void) + func getDownloadDataTasks() async -> [DownloadDataTask] + func getDownloadDataTasksForCourse(_ courseId: String) async -> [DownloadDataTask] +} + +#if DEBUG +public class CorePersistenceMock: CorePersistenceProtocol { + + public init() {} + + public func set(userId: Int) {} + public func getUserID() -> Int? {1} + public func publisher() -> AnyPublisher { Just(0).eraseToAnyPublisher() } + public func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) {} + public func addToDownloadQueue(tasks: [DownloadDataTask]) {} + public func nextBlockForDownloading() -> DownloadDataTask? { nil } + public func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) {} + public func deleteDownloadDataTask(id: String) throws {} + public func downloadDataTask(for blockId: String) -> DownloadDataTask? { nil } + public func saveOfflineProgress(progress: OfflineProgress) {} + public func loadProgress(for blockID: String) -> OfflineProgress? { nil } + public func loadAllOfflineProgress() -> [OfflineProgress] { [] } + public func deleteProgress(for blockID: String) {} + public func deleteAllProgress() {} + public func saveDownloadDataTask(_ task: DownloadDataTask) {} + public func getDownloadDataTasks() async -> [DownloadDataTask] {[]} + public func getDownloadDataTasksForCourse(_ courseId: String) async -> [DownloadDataTask] {[]} } +#endif public final class CoreBundle { private init() {} diff --git a/Core/Core/Data/Repository/AuthRepository.swift b/Core/Core/Data/Repository/AuthRepository.swift index e4adf93ea..20e89e603 100644 --- a/Core/Core/Data/Repository/AuthRepository.swift +++ b/Core/Core/Data/Repository/AuthRepository.swift @@ -6,10 +6,12 @@ // import Foundation +import OEXFoundation public protocol AuthRepositoryProtocol { func login(username: String, password: String) async throws -> User func login(externalToken: String, backend: String) async throws -> User + func login(ssoToken: String) async throws -> User func getCookies(force: Bool) async throws func getRegistrationFields() async throws -> [PickerFields] func registerUser(fields: [String: String], isSocial: Bool) async throws -> User @@ -80,6 +82,16 @@ public class AuthRepository: AuthRepositoryProtocol { return user.domain } + public func login(ssoToken: String) async throws -> User { + if appStorage.accessToken == nil { + appStorage.accessToken = ssoToken + } + + let user = try await api.requestData(AuthEndpoint.getUserInfo).mapResponse(DataLayer.User.self) + appStorage.user = user + return user.domain + } + public func resetPassword(email: String) async throws -> ResetPassword { let response = try await api.requestData(AuthEndpoint.resetPassword(email: email)) .mapResponse(DataLayer.ResetPassword.self) @@ -88,15 +100,14 @@ public class AuthRepository: AuthRepositoryProtocol { public func getCookies(force: Bool) async throws { if let cookiesCreatedDate = appStorage.cookiesDate, !force { - let cookiesCreated = Date(iso8601: cookiesCreatedDate) - let cookieLifetimeLimit = cookiesCreated.addingTimeInterval(60 * 60) + let cookieLifetimeLimit = cookiesCreatedDate.addingTimeInterval(60 * 60) if Date() > cookieLifetimeLimit { _ = try await api.requestData(AuthEndpoint.getAuthCookies) - appStorage.cookiesDate = Date().dateToString(style: .iso8601) + appStorage.cookiesDate = Date() } } else { _ = try await api.requestData(AuthEndpoint.getAuthCookies) - appStorage.cookiesDate = Date().dateToString(style: .iso8601) + appStorage.cookiesDate = Date() } } @@ -138,6 +149,10 @@ class AuthRepositoryMock: AuthRepositoryProtocol { User(id: 1, username: "User", email: "email@gmail.com", name: "User Name", userAvatar: "") } + func login(ssoToken: String) async throws -> User { + return User(id: 1, username: "User", email: "email@gmail.com", name: "User Name", userAvatar: "") + } + func resetPassword(email: String) async throws -> ResetPassword { ResetPassword(success: true, responseText: "Success reset") } diff --git a/Core/Core/Data/Repository/OfflineSyncRepository.swift b/Core/Core/Data/Repository/OfflineSyncRepository.swift new file mode 100644 index 000000000..67a535719 --- /dev/null +++ b/Core/Core/Data/Repository/OfflineSyncRepository.swift @@ -0,0 +1,43 @@ +// +// OfflineSyncRepository.swift +// Core +// +// Created by  Stepanok Ivan on 20.06.2024. +// + +import Foundation +import OEXFoundation + +public protocol OfflineSyncRepositoryProtocol { + func submitOfflineProgress(courseID: String, blockID: String, data: String) async throws -> Bool +} + +public class OfflineSyncRepository: OfflineSyncRepositoryProtocol { + + private let api: API + + public init(api: API) { + self.api = api + } + + public func submitOfflineProgress(courseID: String, blockID: String, data: String) async throws -> Bool { + let request = try await api.request( + OfflineSyncEndpoint.submitOfflineProgress( + courseID: courseID, + blockID: blockID, + data: data + ) + ) + + return request.statusCode == 200 + } +} + +// Mark - For testing and SwiftUI preview +#if DEBUG +class OfflineSyncRepositoryMock: OfflineSyncRepositoryProtocol { + public func submitOfflineProgress(courseID: String, blockID: String, data: String) async throws -> Bool { + true + } +} +#endif diff --git a/Core/Core/Domain/AuthInteractor.swift b/Core/Core/Domain/AuthInteractor.swift index 45868cbc9..6b3562f20 100644 --- a/Core/Core/Domain/AuthInteractor.swift +++ b/Core/Core/Domain/AuthInteractor.swift @@ -13,6 +13,7 @@ public protocol AuthInteractorProtocol { func login(username: String, password: String) async throws -> User @discardableResult func login(externalToken: String, backend: String) async throws -> User + func login(ssoToken: String) async throws -> User func resetPassword(email: String) async throws -> ResetPassword func getCookies(force: Bool) async throws func getRegistrationFields() async throws -> [PickerFields] @@ -37,6 +38,11 @@ public class AuthInteractor: AuthInteractorProtocol { return try await repository.login(externalToken: externalToken, backend: backend) } + @discardableResult + public func login(ssoToken: String) async throws -> User { + return try await repository.login(ssoToken: ssoToken) + } + public func resetPassword(email: String) async throws -> ResetPassword { try await repository.resetPassword(email: email) } diff --git a/Core/Core/Domain/Model/CourseBlockModel.swift b/Core/Core/Domain/Model/CourseBlockModel.swift index 406bea3ed..731e47176 100644 --- a/Core/Core/Domain/Model/CourseBlockModel.swift +++ b/Core/Core/Domain/Model/CourseBlockModel.swift @@ -24,6 +24,7 @@ public struct CourseStructure: Equatable { public let certificate: Certificate? public let org: String public let isSelfPaced: Bool + public let courseProgress: CourseProgress? public init( id: String, @@ -37,7 +38,8 @@ public struct CourseStructure: Equatable { media: DataLayer.CourseMedia, certificate: Certificate?, org: String, - isSelfPaced: Bool + isSelfPaced: Bool, + courseProgress: CourseProgress? ) { self.id = id self.graded = graded @@ -51,6 +53,7 @@ public struct CourseStructure: Equatable { self.certificate = certificate self.org = org self.isSelfPaced = isSelfPaced + self.courseProgress = courseProgress } public func totalVideosSizeInBytes(downloadQuality: DownloadQuality) -> Int { @@ -78,6 +81,16 @@ public struct CourseStructure: Equatable { } } +public struct CourseProgress { + public let totalAssignmentsCount: Int? + public let assignmentsCompleted: Int? + + public init(totalAssignmentsCount: Int, assignmentsCompleted: Int) { + self.totalAssignmentsCount = totalAssignmentsCount + self.assignmentsCompleted = assignmentsCompleted + } +} + public struct CourseChapter: Identifiable { public let blockId: String @@ -109,18 +122,26 @@ public struct CourseSequential: Identifiable { public let type: BlockType public let completion: Double public var childs: [CourseVertical] + public let sequentialProgress: SequentialProgress? + public let due: Date? public var isDownloadable: Bool { return childs.first(where: { $0.isDownloadable }) != nil } + public var totalSize: Int { + childs.flatMap { $0.childs.filter({ $0.isDownloadable }) }.reduce(0) { $0 + ($1.fileSize ?? 0) } + } + public init( blockId: String, id: String, displayName: String, type: BlockType, completion: Double, - childs: [CourseVertical] + childs: [CourseVertical], + sequentialProgress: SequentialProgress?, + due: Date? ) { self.blockId = blockId self.id = id @@ -128,6 +149,8 @@ public struct CourseSequential: Identifiable { self.type = type self.completion = completion self.childs = childs + self.sequentialProgress = sequentialProgress + self.due = due } } @@ -143,6 +166,7 @@ public struct CourseVertical: Identifiable, Hashable { public let type: BlockType public let completion: Double public var childs: [CourseBlock] + public var webUrl: String public var isDownloadable: Bool { return childs.first(where: { $0.isDownloadable }) != nil @@ -155,7 +179,8 @@ public struct CourseVertical: Identifiable, Hashable { displayName: String, type: BlockType, completion: Double, - childs: [CourseBlock] + childs: [CourseBlock], + webUrl: String ) { self.blockId = blockId self.id = id @@ -164,6 +189,7 @@ public struct CourseVertical: Identifiable, Hashable { self.type = type self.completion = completion self.childs = childs + self.webUrl = webUrl } } @@ -177,6 +203,18 @@ public struct SubtitleUrl: Equatable { } } +public struct SequentialProgress { + public let assignmentType: String? + public let numPointsEarned: Int? + public let numPointsPossible: Int? + + public init(assignmentType: String?, numPointsEarned: Int?, numPointsPossible: Int?) { + self.assignmentType = assignmentType + self.numPointsEarned = numPointsEarned + self.numPointsPossible = numPointsPossible + } +} + public struct CourseBlock: Hashable, Identifiable { public static func == (lhs: CourseBlock, rhs: CourseBlock) -> Bool { lhs.id == rhs.id && @@ -193,6 +231,7 @@ public struct CourseBlock: Hashable, Identifiable { public let courseId: String public let topicId: String? public let graded: Bool + public let due: Date? public var completion: Double public let type: BlockType public let displayName: String @@ -201,9 +240,31 @@ public struct CourseBlock: Hashable, Identifiable { public let subtitles: [SubtitleUrl]? public let encodedVideo: CourseBlockEncodedVideo? public let multiDevice: Bool? + public var offlineDownload: OfflineDownload? + public var actualFileSize: Int? public var isDownloadable: Bool { - encodedVideo?.isDownloadable ?? false + encodedVideo?.isDownloadable ?? false || offlineDownload?.isDownloadable ?? false + } + + public var fileSize: Int? { + if let actualFileSize { + return actualFileSize + } else if let fileSize = encodedVideo?.desktopMP4?.fileSize { + return fileSize + } else if let fileSize = encodedVideo?.fallback?.fileSize { + return fileSize + } else if let fileSize = encodedVideo?.hls?.fileSize { + return fileSize + } else if let fileSize = encodedVideo?.mobileHigh?.fileSize { + return fileSize + } else if let fileSize = encodedVideo?.mobileLow?.fileSize { + return fileSize + } else if let fileSize = offlineDownload?.fileSize { + return fileSize + } else { + return nil + } } public init( @@ -212,6 +273,7 @@ public struct CourseBlock: Hashable, Identifiable { courseId: String, topicId: String? = nil, graded: Bool, + due: Date?, completion: Double, type: BlockType, displayName: String, @@ -219,13 +281,15 @@ public struct CourseBlock: Hashable, Identifiable { webUrl: String, subtitles: [SubtitleUrl]? = nil, encodedVideo: CourseBlockEncodedVideo?, - multiDevice: Bool? + multiDevice: Bool?, + offlineDownload: OfflineDownload? ) { self.blockId = blockId self.id = id self.courseId = courseId self.topicId = topicId self.graded = graded + self.due = due self.completion = completion self.type = type self.displayName = displayName @@ -234,6 +298,23 @@ public struct CourseBlock: Hashable, Identifiable { self.subtitles = subtitles self.encodedVideo = encodedVideo self.multiDevice = multiDevice + self.offlineDownload = offlineDownload + } +} + +public struct OfflineDownload { + public let fileUrl: String + public var lastModified: String + public let fileSize: Int + + public init(fileUrl: String, lastModified: String, fileSize: Int) { + self.fileUrl = fileUrl + self.lastModified = lastModified + self.fileSize = fileSize + } + + public var isDownloadable: Bool { + [".zip"].contains(where: { fileUrl.contains($0) == true }) } } diff --git a/Course/Course/Domain/Model/CourseDates.swift b/Core/Core/Domain/Model/CourseDates.swift similarity index 66% rename from Course/Course/Domain/Model/CourseDates.swift rename to Core/Core/Domain/Model/CourseDates.swift index 966899cb9..11c0e943d 100644 --- a/Course/Course/Domain/Model/CourseDates.swift +++ b/Core/Core/Domain/Model/CourseDates.swift @@ -1,20 +1,20 @@ // // CourseDates.swift -// Course +// Core // -// Created by Muhammad Umer on 10/18/23. +// Created by  Stepanok Ivan on 05.06.2024. // import Foundation -import Core +import CryptoKit public struct CourseDates { - let datesBannerInfo: DatesBannerInfo - let courseDateBlocks: [CourseDateBlock] - let hasEnded, learnerIsFullAccess: Bool - let userTimezone: String? + public let datesBannerInfo: DatesBannerInfo + public let courseDateBlocks: [CourseDateBlock] + public let hasEnded, learnerIsFullAccess: Bool + public let userTimezone: String? - var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { + public var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] { var statusDatesBlocks: [CompletionStatus: [Date: [CourseDateBlock]]] = [:] var statusBlocks: [CompletionStatus: [CourseDateBlock]] = [:] @@ -56,15 +56,41 @@ public struct CourseDates { return statusDatesBlocks } - var dateBlocks: [Date: [CourseDateBlock]] { + public var dateBlocks: [Date: [CourseDateBlock]] { return courseDateBlocks.reduce(into: [:]) { result, block in let date = block.date result[date, default: []].append(block) } } + + public init( + datesBannerInfo: DatesBannerInfo, + courseDateBlocks: [CourseDateBlock], + hasEnded: Bool, + learnerIsFullAccess: Bool, + userTimezone: String? + ) { + self.datesBannerInfo = datesBannerInfo + self.courseDateBlocks = courseDateBlocks + self.hasEnded = hasEnded + self.learnerIsFullAccess = learnerIsFullAccess + self.userTimezone = userTimezone + } + + public var checksum: String { + var combinedString = "" + for block in self.courseDateBlocks { + let assignmentType = block.assignmentType ?? "" + combinedString += assignmentType + block.firstComponentBlockID + block.date.description + } + + let checksumData = SHA256.hash(data: Data(combinedString.utf8)) + let checksumString = checksumData.map { String(format: "%02hhx", $0) }.joined() + return checksumString + } } -extension Date { +public extension Date { static var today: Date { return Calendar.current.startOfDay(for: Date()) } @@ -120,26 +146,27 @@ extension Date { public struct CourseDateBlock: Identifiable { public let id: UUID = UUID() - let assignmentType: String? - let complete: Bool? - let date: Date - let dateType, description: String - let learnerHasAccess: Bool - let link: String - let linkText: String? - let title: String - let extraInfo: String? - let firstComponentBlockID: String + public let assignmentType: String? + public let complete: Bool? + public let date: Date + public let dateType, description: String + public let learnerHasAccess: Bool + public let link: String + public let linkText: String? + public let title: String + public let extraInfo: String? + public let firstComponentBlockID: String + public let useRelativeDates: Bool - var formattedDate: String { - return date.dateToString(style: .shortWeekdayMonthDayYear) + public var formattedDate: String { + return date.dateToString(style: .shortWeekdayMonthDayYear, useRelativeDates: useRelativeDates) } - var isInPast: Bool { + public var isInPast: Bool { return date.isInPast } - var isToday: Bool { + public var isToday: Bool { if dateType.isEmpty { return true } else { @@ -147,55 +174,55 @@ public struct CourseDateBlock: Identifiable { } } - var isInFuture: Bool { + public var isInFuture: Bool { return date.isInFuture } - var isThisWeek: Bool { + public var isThisWeek: Bool { return date.isThisWeek } - var isNextWeek: Bool { + public var isNextWeek: Bool { return date.isNextWeek } - var isUpcoming: Bool { + public var isUpcoming: Bool { return date.isUpcoming } - var isAssignment: Bool { + public var isAssignment: Bool { return BlockStatus.status(of: dateType) == .assignment } - var isVerifiedOnly: Bool { + public var isVerifiedOnly: Bool { return !learnerHasAccess } - var isComplete: Bool { + public var isComplete: Bool { return complete ?? false } - var isLearnerAssignment: Bool { + public var isLearnerAssignment: Bool { return learnerHasAccess && isAssignment } - var isPastDue: Bool { + public var isPastDue: Bool { return !isComplete && (date < .today) } - var isUnreleased: Bool { + public var isUnreleased: Bool { return link.isEmpty } - var canShowLink: Bool { + public var canShowLink: Bool { return !isUnreleased && isLearnerAssignment } - var isAvailable: Bool { + public var isAvailable: Bool { return learnerHasAccess && (!isUnreleased || !isLearnerAssignment) } - var blockStatus: BlockStatus { + public var blockStatus: BlockStatus { if isComplete { return .completed } @@ -215,7 +242,7 @@ public struct CourseDateBlock: Identifiable { return BlockStatus.status(of: dateType) } - var blockImage: ImageAsset? { + public var blockImage: ImageAsset? { if !learnerHasAccess { return CoreAssets.lockIcon } @@ -240,14 +267,33 @@ public struct CourseDateBlock: Identifiable { } public struct DatesBannerInfo { - let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool - let verifiedUpgradeLink: String? - let status: DataLayer.BannerInfoStatus? + public let missedDeadlines, contentTypeGatingEnabled, missedGatedContent: Bool + public let verifiedUpgradeLink: String? + public let status: DataLayer.BannerInfoStatus? + + public init( + missedDeadlines: Bool, + contentTypeGatingEnabled: Bool, + missedGatedContent: Bool, + verifiedUpgradeLink: String?, + status: DataLayer.BannerInfoStatus? + ) { + self.missedDeadlines = missedDeadlines + self.contentTypeGatingEnabled = contentTypeGatingEnabled + self.missedGatedContent = missedGatedContent + self.verifiedUpgradeLink = verifiedUpgradeLink + self.status = status + } } public struct CourseDateBanner { - let datesBannerInfo: DatesBannerInfo - let hasEnded: Bool + public let datesBannerInfo: DatesBannerInfo + public let hasEnded: Bool + + public init(datesBannerInfo: DatesBannerInfo, hasEnded: Bool) { + self.datesBannerInfo = datesBannerInfo + self.hasEnded = hasEnded + } } public enum BlockStatus { @@ -286,9 +332,26 @@ public enum CompletionStatus: String { case thisWeek = "This Week" case nextWeek = "Next Week" case upcoming = "Upcoming" + + public var localized: String { + switch self { + case .completed: + return CoreLocalization.CourseDates.completed + case .pastDue: + return CoreLocalization.CourseDates.pastDue + case .today: + return CoreLocalization.CourseDates.today + case .thisWeek: + return CoreLocalization.CourseDates.thisWeek + case .nextWeek: + return CoreLocalization.CourseDates.nextWeek + case .upcoming: + return CoreLocalization.CourseDates.upcoming + } + } } -extension Array { +public extension Array { mutating func modifyForEach(_ body: (_ element: inout Element) -> Void) { for index in indices { modifyElement(atIndex: index) { body(&$0) } diff --git a/Core/Core/Domain/Model/CourseForSync.swift b/Core/Core/Domain/Model/CourseForSync.swift new file mode 100644 index 000000000..5f3a6c343 --- /dev/null +++ b/Core/Core/Domain/Model/CourseForSync.swift @@ -0,0 +1,51 @@ +// +// CourseForSync.swift +// Core +// +// Created by  Stepanok Ivan on 12.06.2024. +// + +import Foundation + +// MARK: - CourseForSync +public struct CourseForSync: Identifiable { + public let id: UUID + public let courseID: String + public let name: String + public var synced: Bool + public var recentlyActive: Bool + + public init(id: UUID = UUID(), courseID: String, name: String, synced: Bool, recentlyActive: Bool) { + self.id = id + self.courseID = courseID + self.name = name + self.synced = synced + self.recentlyActive = recentlyActive + } +} + +extension DataLayer.EnrollmentsStatus { + public var domain: [CourseForSync] { + self.compactMap { + guard let courseID = $0.courseID, + let courseName = $0.courseName, + let recentlyActive = $0.recentlyActive else { return nil } + return CourseForSync( + id: UUID(), + courseID: courseID, + name: courseName, + synced: false, + recentlyActive: recentlyActive + ) + } + } +} + +extension CourseForSync: Equatable { + public static func == (lhs: CourseForSync, rhs: CourseForSync) -> Bool { + return lhs.courseID == rhs.courseID && + lhs.name == rhs.name && + lhs.synced == rhs.synced && + lhs.recentlyActive == rhs.recentlyActive + } +} diff --git a/Core/Core/Domain/Model/CourseItem.swift b/Core/Core/Domain/Model/CourseItem.swift index 9229417f1..19bd1f612 100644 --- a/Core/Core/Domain/Model/CourseItem.swift +++ b/Core/Core/Domain/Model/CourseItem.swift @@ -12,7 +12,7 @@ public struct CourseItem: Hashable { public let org: String public let shortDescription: String public let imageURL: String - public let isActive: Bool? + public let hasAccess: Bool public let courseStart: Date? public let courseEnd: Date? public let enrollmentStart: Date? @@ -20,24 +20,30 @@ public struct CourseItem: Hashable { public let courseID: String public let numPages: Int public let coursesCount: Int + public let courseRawImage: String? + public let progressEarned: Int + public let progressPossible: Int public init(name: String, org: String, shortDescription: String, imageURL: String, - isActive: Bool?, + hasAccess: Bool, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, courseID: String, numPages: Int, - coursesCount: Int) { + coursesCount: Int, + courseRawImage: String?, + progressEarned: Int, + progressPossible: Int) { self.name = name self.org = org self.shortDescription = shortDescription self.imageURL = imageURL - self.isActive = isActive + self.hasAccess = hasAccess self.courseStart = courseStart self.courseEnd = courseEnd self.enrollmentStart = enrollmentStart @@ -45,5 +51,8 @@ public struct CourseItem: Hashable { self.courseID = courseID self.numPages = numPages self.coursesCount = coursesCount + self.courseRawImage = courseRawImage + self.progressEarned = progressEarned + self.progressPossible = progressPossible } } diff --git a/Core/Core/Domain/Model/OfflineProgress.swift b/Core/Core/Domain/Model/OfflineProgress.swift new file mode 100644 index 000000000..efb1a982c --- /dev/null +++ b/Core/Core/Domain/Model/OfflineProgress.swift @@ -0,0 +1,50 @@ +// +// OfflineProgress.swift +// Core +// +// Created by  Stepanok Ivan on 20.06.2024. +// + +import Foundation + +public struct OfflineProgress { + public let blockID: String + public let data: String + public let courseID: String + public let progressJson: String + + public init(progressJson: String) { + self.progressJson = progressJson + if let jsonData = progressJson.data(using: .utf8) { + if let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] { + if let url = jsonObject["url"] as? String, + let data = jsonObject["data"] as? String { + self.blockID = extractBlockID(from: url) + self.data = data + self.courseID = extractCourseID(from: url) + return + } + } + } + // Default values if parsing fails + self.blockID = "" + self.data = "" + self.courseID = "" + + func extractBlockID(from url: String) -> String { + if let range = url.range(of: "xblock/")?.upperBound, + let endRange = url.range(of: "/handler", range: range.. String { + if let range = url.range(of: "courses/")?.upperBound, + let endRange = url.range(of: "/xblock", range: range.. Bool +} + +public class OfflineSyncInteractor: OfflineSyncInteractorProtocol { + private let repository: OfflineSyncRepositoryProtocol + + public init(repository: OfflineSyncRepositoryProtocol) { + self.repository = repository + } + + public func submitOfflineProgress(courseID: String, blockID: String, data: String) async throws -> Bool { + return try await repository.submitOfflineProgress( + courseID: courseID, + blockID: blockID, + data: data + ) + } +} diff --git a/Core/Core/Extensions/AVPlayerViewControllerExtension.swift b/Core/Core/Extensions/AVPlayerViewControllerExtension.swift deleted file mode 100644 index 1f9b3e27e..000000000 --- a/Core/Core/Extensions/AVPlayerViewControllerExtension.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// AVPlayerViewControllerExtension.swift -// Core -// -// Created by  Stepanok Ivan on 24.11.2022. -// - -import AVKit - -public extension AVPlayerViewController { - func enterFullScreen(animated: Bool) { - perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: animated, with: nil) - } - func exitFullScreen(animated: Bool) { - perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: animated, with: nil) - } -} diff --git a/Core/Core/Extensions/CGColorExtension.swift b/Core/Core/Extensions/CGColorExtension.swift deleted file mode 100644 index f1e871974..000000000 --- a/Core/Core/Extensions/CGColorExtension.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// CGColorExtension.swift -// Core -// -// Created by  Stepanok Ivan on 03.03.2023. -// - -import Foundation -import SwiftUI - -public extension CGColor { - var hexString: String? { - guard let components = self.components, components.count >= 3 else { - return nil - } - let red = components[0] - let green = components[1] - let blue = components[2] - let hexString = String( - format: "#%02lX%02lX%02lX", - lroundf(Float(red * 255)), - lroundf(Float(green * 255)), - lroundf(Float(blue * 255)) - ) - return hexString - } -} - -public extension Color { - func uiColor() -> UIColor { - return UIColor(self) - } -} diff --git a/Core/Core/Extensions/CollectionExtension.swift b/Core/Core/Extensions/CollectionExtension.swift deleted file mode 100644 index ba2ff088a..000000000 --- a/Core/Core/Extensions/CollectionExtension.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// CollectionExtension.swift -// Core -// -// Created by Vladimir Chekyrta on 15.12.2022. -// - -import Foundation - -public extension Collection { - /// Returns the element at the specified index if it is within bounds, otherwise nil. - subscript (safe index: Index) -> Element? { - return indices.contains(index) ? self[index] : nil - } -} diff --git a/Core/Core/Extensions/Container+App.swift b/Core/Core/Extensions/Container+App.swift deleted file mode 100644 index 01091f661..000000000 --- a/Core/Core/Extensions/Container+App.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Container+App.swift -// Core -// -// Created by  Stepanok Ivan on 13.10.2022. -// - -import Foundation -import Swinject - -public extension Container { - static var shared: Container = { - let container = Container() - return container - }() -} - -public extension UIViewController { - var diContainer: Container { - return Container.shared - } -} diff --git a/Core/Core/Extensions/DateExtension.swift b/Core/Core/Extensions/DateExtension.swift index 8a57079f4..cb07e81f6 100644 --- a/Core/Core/Extensions/DateExtension.swift +++ b/Core/Core/Extensions/DateExtension.swift @@ -7,7 +7,6 @@ import Foundation - public extension Date { init(iso8601: String) { let formats = ["yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ"] @@ -15,7 +14,7 @@ public extension Date { var date: Date var dateFormatter: DateFormatter? dateFormatter = DateFormatter() - dateFormatter?.locale = Locale(identifier: "en_US_POSIX") + dateFormatter?.locale = .current date = formats.compactMap { format in dateFormatter?.dateFormat = format @@ -34,16 +33,75 @@ public extension Date { self.init(timeInterval: 0, since: date) } - func timeAgoDisplay() -> String { - let formatter = RelativeDateTimeFormatter() - formatter.locale = .current - formatter.unitsStyle = .full - formatter.locale = Locale(identifier: "en_US_POSIX") - if description == Date().description { - return CoreLocalization.Date.justNow - } else { - return formatter.localizedString(for: self, relativeTo: Date()) + func timeAgoDisplay(dueIn: Bool = false) -> String { + let currentDate = Date() + let calendar = Calendar.current + + let dueString = dueIn ? CoreLocalization.Date.due : "" + let dueInString = dueIn ? CoreLocalization.Date.dueIn : "" + + let startOfCurrentDate = calendar.startOfDay(for: currentDate) + let startOfSelfDate = calendar.startOfDay(for: self) + + let daysRemaining = Calendar.current.dateComponents( + [.day], + from: startOfCurrentDate, + to: self + ).day ?? 0 + + // Calculate date ranges + guard let sevenDaysAgo = calendar.date(byAdding: .day, value: -7, to: startOfCurrentDate), + let sevenDaysAhead = calendar.date(byAdding: .day, value: 7, to: startOfCurrentDate) else { + return dueInString + self.dateToString(style: .mmddyy, useRelativeDates: false) + } + + let isCurrentYear = calendar.component(.year, from: self) == calendar.component(.year, from: startOfCurrentDate) + + if calendar.isDateInToday(startOfSelfDate) { + return dueString + CoreLocalization.Date.today + } + + if calendar.isDateInYesterday(startOfSelfDate) { + return dueString + CoreLocalization.yesterday + } + + if calendar.isDateInTomorrow(startOfSelfDate) { + return dueString + CoreLocalization.tomorrow } + + if startOfSelfDate > startOfCurrentDate && startOfSelfDate <= sevenDaysAhead { + let weekdayFormatter = DateFormatter() + weekdayFormatter.dateFormat = "EEEE" + if startOfSelfDate == calendar.date(byAdding: .day, value: 1, to: startOfCurrentDate) { + return dueInString + CoreLocalization.tomorrow + } else if startOfSelfDate == calendar.date(byAdding: .day, value: 7, to: startOfCurrentDate) { + return CoreLocalization.Date.next(weekdayFormatter.string(from: startOfSelfDate)) + } else { + return dueIn ? ( + CoreLocalization.Date.dueInDays(daysRemaining) + ) : weekdayFormatter.string(from: startOfSelfDate) + } + } + + if startOfSelfDate < startOfCurrentDate && startOfSelfDate >= sevenDaysAgo { + guard let daysAgo = calendar.dateComponents([.day], from: startOfSelfDate, to: startOfCurrentDate).day else { + return self.dateToString(style: .mmddyy, useRelativeDates: false) + } + return CoreLocalization.Date.daysAgo(daysAgo) + } + + let specificFormatter = DateFormatter() + specificFormatter.dateFormat = isCurrentYear ? "MMMM d" : "MMMM d, yyyy" + return dueInString + specificFormatter.string(from: self) + } + + func isDateInNextWeek(date: Date, currentDate: Date) -> Bool { + let calendar = Calendar.current + guard let nextWeek = calendar.date(byAdding: .weekOfYear, value: 1, to: currentDate) else { return false } + let startOfNextWeek = calendar.startOfDay(for: nextWeek) + guard let endOfNextWeek = calendar.date(byAdding: .day, value: 6, to: startOfNextWeek) else { return false } + let startOfSelfDate = calendar.startOfDay(for: date) + return startOfSelfDate >= startOfNextWeek && startOfSelfDate <= endOfNextWeek } init(subtitleTime: String) { @@ -76,6 +134,8 @@ public extension Date { } public enum DateStringStyle { + case courseStartsMonthDDYear + case courseEndsMonthDDYear case startDDMonthYear case endedMonthDay case mmddyy @@ -99,30 +159,47 @@ public extension Date { return totalSeconds } - func dateToString(style: DateStringStyle) -> String { + func dateToString(style: DateStringStyle, useRelativeDates: Bool, dueIn: Bool = false) -> String { let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - switch style { - case .endedMonthDay: - dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmmDd - case .mmddyy: - dateFormatter.dateFormat = "dd.MM.yy" - case .monthYear: - dateFormatter.dateFormat = "MMMM yyyy" - case .startDDMonthYear: - dateFormatter.dateFormat = "dd MMM yyyy" - case .lastPost: - dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy - case .iso8601: - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - case .shortWeekdayMonthDayYear: - applyShortWeekdayMonthDayYear(dateFormatter: dateFormatter) + dateFormatter.locale = .current + + if useRelativeDates { + return timeAgoDisplay(dueIn: dueIn) + } else { + switch style { + case .courseStartsMonthDDYear: + dateFormatter.dateStyle = .medium + case .courseEndsMonthDDYear: + dateFormatter.dateStyle = .medium + case .endedMonthDay: + dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmmDd + case .mmddyy: + dateFormatter.dateFormat = "dd.MM.yy" + case .monthYear: + dateFormatter.dateFormat = "MMMM yyyy" + case .startDDMonthYear: + dateFormatter.dateFormat = "dd MMM yyyy" + case .lastPost: + dateFormatter.dateFormat = CoreLocalization.DateFormat.mmmDdYyyy + case .iso8601: + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + case .shortWeekdayMonthDayYear: + applyShortWeekdayMonthDayYear(dateFormatter: dateFormatter) + } } let date = dateFormatter.string(from: self) switch style { + case .courseStartsMonthDDYear: + return CoreLocalization.Date.courseStarts + " " + date + case .courseEndsMonthDDYear: + if Date() < self { + return CoreLocalization.Date.courseEnds + " " + date + } else { + return CoreLocalization.Date.courseEnded + " " + date + } case .endedMonthDay: return CoreLocalization.Date.ended + " " + date case .mmddyy, .monthYear: @@ -147,52 +224,19 @@ public extension Date { case .iso8601: return date case .shortWeekdayMonthDayYear: - return getShortWeekdayMonthDayYear(dateFormatterString: date) + return ( + dueIn ? CoreLocalization.Date.dueIn : "" + ) + getShortWeekdayMonthDayYear(dateFormatterString: date) } } private func applyShortWeekdayMonthDayYear(dateFormatter: DateFormatter) { - if isCurrentYear() { - let days = Calendar.current.dateComponents([.day], from: self, to: Date()) - if let day = days.day, (-6 ... -2).contains(day) { - dateFormatter.dateFormat = "EEEE" - } else { - dateFormatter.dateFormat = "MMMM d" - } - } else { dateFormatter.dateFormat = "MMMM d, yyyy" - } } private func getShortWeekdayMonthDayYear(dateFormatterString: String) -> String { let days = Calendar.current.dateComponents([.day], from: self, to: Date()) - - if let day = days.day { - guard isCurrentYear() else { - // It's past year or future year - return dateFormatterString - } - - switch day { - case -6...(-2): - return dateFormatterString - case 2...6: - return timeAgoDisplay() - case -1: - return CoreLocalization.tomorrow - case 1: - return CoreLocalization.yesterday - default: - if day > 6 || day < -6 { - return dateFormatterString - } else { - // It means, date is in hours past due or upcoming - return timeAgoDisplay() - } - } - } else { - return dateFormatterString - } + return dateFormatterString } func isCurrentYear() -> Bool { diff --git a/Core/Core/Extensions/DebugLog.swift b/Core/Core/Extensions/DebugLog.swift deleted file mode 100644 index 1ceb35482..000000000 --- a/Core/Core/Extensions/DebugLog.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// DebugLog.swift -// Core -// -// Created by Eugene Yatsenko on 10.10.2023. -// - -import Foundation - -public func debugLog( - _ item: Any..., - filename: String = #file, - line: Int = #line, - funcname: String = #function -) { -#if DEBUG - print( - """ - 🕗 \(Date()) - 📄 \(filename.components(separatedBy: "/").last ?? "") \(line) \(funcname) - ℹ️ \(item) - """ - ) -#endif -} diff --git a/Core/Core/Extensions/Dictionary+JSON.swift b/Core/Core/Extensions/Dictionary+JSON.swift deleted file mode 100644 index 398fc3676..000000000 --- a/Core/Core/Extensions/Dictionary+JSON.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Dictionary+JSON.swift -// Core -// -// Created by Vadim Kuznetsov on 13.03.24. -// - -import Foundation - -public extension Dictionary where Key == String, Value == String { - public func toJson() -> String? { - guard let jsonData = try? JSONSerialization.data(withJSONObject: self, options: []) else { - return nil - } - - return String(data: jsonData, encoding: .utf8) - } -} diff --git a/Core/Core/Extensions/DispatchQueue+App.swift b/Core/Core/Extensions/DispatchQueue+App.swift deleted file mode 100644 index cd9423cfc..000000000 --- a/Core/Core/Extensions/DispatchQueue+App.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// DispatchQueue+App.swift -// Core -// -// Created by Vladimir Chekyrta on 15.09.2022. -// - -import Foundation - -public func doAfter(_ delay: TimeInterval? = nil, _ closure: @escaping () -> Void) { - DispatchQueue.main.asyncAfter(deadline: .now() + (delay ?? 0), execute: closure) -} - -public func dispatchQueueMain(_ closure: @escaping () -> Void) { - DispatchQueue.main.async(execute: closure) -} diff --git a/Core/Core/Extensions/Notification.swift b/Core/Core/Extensions/Notification.swift index ba9dfe70c..55ce77cc4 100644 --- a/Core/Core/Extensions/Notification.swift +++ b/Core/Core/Extensions/Notification.swift @@ -8,13 +8,19 @@ import Foundation public extension Notification.Name { + static let userAuthorized = Notification.Name("userAuthorized") + static let userLoggedOut = Notification.Name("userLoggedOut") static let onCourseEnrolled = Notification.Name("onCourseEnrolled") + static let onblockCompletionRequested = Notification.Name("onblockCompletionRequested") static let onTokenRefreshFailed = Notification.Name("onTokenRefreshFailed") - static let onActualVersionReceived = Notification.Name("onActualVersionReceived") static let onAppUpgradeAccountSettingsTapped = Notification.Name("onAppUpgradeAccountSettingsTapped") static let onNewVersionAvaliable = Notification.Name("onNewVersionAvaliable") static let webviewReloadNotification = Notification.Name("webviewReloadNotification") - static let onBlockCompletion = Notification.Name.init("onBlockCompletion") + static let onBlockCompletion = Notification.Name("onBlockCompletion") static let shiftCourseDates = Notification.Name("shiftCourseDates") static let profileUpdated = Notification.Name("profileUpdated") + static let getCourseDates = Notification.Name("getCourseDates") + static let showDownloadFailed = Notification.Name("showDownloadFailed") + static let tryDownloadAgain = Notification.Name("tryDownloadAgain") + static let refreshEnrollments = Notification.Name("refreshEnrollments") } diff --git a/Core/Core/Extensions/RawStringExtactable.swift b/Core/Core/Extensions/RawStringExtactable.swift deleted file mode 100644 index 1dcedb86c..000000000 --- a/Core/Core/Extensions/RawStringExtactable.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// RawStringExtactable.swift -// Core -// -// Created by SaeedBashir on 12/18/23. -// - -import Foundation - -public protocol RawStringExtractable { - var rawValue: String { get } -} - -public protocol DictionaryExtractionExtension { - associatedtype Key - associatedtype Value - subscript(key: Key) -> Value? { get } -} - -extension Dictionary: DictionaryExtractionExtension {} - -public extension DictionaryExtractionExtension where Self.Key == String { - - subscript(key: RawStringExtractable) -> Value? { - return self[key.rawValue] - } -} diff --git a/Core/Core/Extensions/ResultExtension.swift b/Core/Core/Extensions/ResultExtension.swift deleted file mode 100644 index d9a327768..000000000 --- a/Core/Core/Extensions/ResultExtension.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ResultExtension.swift -// Core -// -// Created by Eugene Yatsenko on 11.10.2023. -// - -import Foundation - -extension Result { - @discardableResult - public func success(_ handler: (Success) -> Void) -> Self { - guard case let .success(value) = self else { return self } - handler(value) - return self - } - @discardableResult - public func failure(_ handler: (Failure) -> Void) -> Self { - guard case let .failure(error) = self else { return self } - handler(error) - return self - } -} diff --git a/Core/Core/Extensions/SKStoreReviewControllerExtension.swift b/Core/Core/Extensions/SKStoreReviewControllerExtension.swift deleted file mode 100644 index be214f661..000000000 --- a/Core/Core/Extensions/SKStoreReviewControllerExtension.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// SKStoreReviewControllerExtension.swift -// Core -// -// Created by  Stepanok Ivan on 16.11.2023. -// - -import Foundation -import StoreKit - -extension SKStoreReviewController { - public static func requestReviewInCurrentScene() { - if let scene = UIApplication.shared.connectedScenes - .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - DispatchQueue.main.async { - requestReview(in: scene) - } - } - } -} diff --git a/Core/Core/Extensions/Sequence+Extensions.swift b/Core/Core/Extensions/Sequence+Extensions.swift deleted file mode 100644 index 6feea9dd7..000000000 --- a/Core/Core/Extensions/Sequence+Extensions.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Sequence+Extensions.swift -// Core -// -// Created by Eugene Yatsenko on 28.02.2024. -// - -import Foundation - -public extension Sequence { - func firstAs(_ type: T.Type = T.self) -> T? { - first { $0 is T } as? T - } -} diff --git a/Core/Core/Extensions/String+JSON.swift b/Core/Core/Extensions/String+JSON.swift deleted file mode 100644 index ab171369e..000000000 --- a/Core/Core/Extensions/String+JSON.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// String+JSON.swift -// Core -// -// Created by Vadim Kuznetsov on 13.03.24. -// - -import Foundation - -public extension String { - public func jsonStringToDictionary() -> [String: Any]? { - guard let jsonData = self.data(using: .utf8) else { - return nil - } - - guard let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []), - let dictionary = jsonObject as? [String: Any] else { - return nil - } - - return dictionary - } -} diff --git a/Core/Core/Extensions/StringExtension.swift b/Core/Core/Extensions/StringExtension.swift deleted file mode 100644 index e7e6805fb..000000000 --- a/Core/Core/Extensions/StringExtension.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// StringExtension.swift -// Core -// -// Created by  Stepanok Ivan on 29.09.2022. -// - -import Foundation - -public extension String { - - func find(from: String, to: String) -> [String] { - components(separatedBy: from).dropFirst().compactMap { sub in - (sub.range(of: to)?.lowerBound).flatMap { endRange in - String(sub[sub.startIndex ..< endRange]) - } - } - } - - func hideHtmlTagsAndUrls() -> String { - guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { - return self - } - return detector.stringByReplacingMatches( - in: self, - options: [], - range: NSRange(location: 0, length: self.utf16.count), - withTemplate: "" - ) - .replacingOccurrences(of: "<[^>]+>", with: "", options: String.CompareOptions.regularExpression, range: nil) - .replacingOccurrences(of: "

", with: "") - .replacingOccurrences(of: "

", with: "") - } - - func hideHtmlTags() -> String { - return self - .replacingOccurrences(of: "<[^>]+>", with: "", options: String.CompareOptions.regularExpression, range: nil) - } - - func extractURLs() -> [URL] { - var urls: [URL] = [] - do { - let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - detector.enumerateMatches( - in: self, options: [], - range: NSRange(location: 0, length: self.count), - using: { (result, _, _) in - if let match = result, let url = match.url { - urls.append(url) - } - } - ) - } catch let error as NSError { - print(error.localizedDescription) - } - return urls - } - - func decodedHTMLEntities() -> String { - guard let regex = try? NSRegularExpression(pattern: "&#([0-9]+);", options: []) else { - return self - } - - let range = NSRange(location: 0, length: count) - let matches = regex.matches(in: self, options: [], range: range) - - var decodedString = self - for match in matches { - guard match.numberOfRanges > 1, - let range = Range(match.range(at: 1), in: self), - let codePoint = Int(self[range]), - let unicodeScalar = UnicodeScalar(codePoint) else { - continue - } - - let replacement = String(unicodeScalar) - guard let totalRange = Range(match.range, in: self) else { - continue - } - decodedString = decodedString.replacingOccurrences(of: self[totalRange], with: replacement) - } - - return decodedString - } -} diff --git a/Core/Core/Extensions/Thread.swift b/Core/Core/Extensions/Thread.swift deleted file mode 100644 index a78be4eac..000000000 --- a/Core/Core/Extensions/Thread.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Thread.swift -// Core -// -// Created by  Stepanok Ivan on 28.12.2022. -// - -import Foundation - -extension Thread { - - var threadName: String { - if let currentOperationQueue = OperationQueue.current?.name { - return "OperationQueue: \(currentOperationQueue)" - } else if let underlyingDispatchQueue = OperationQueue.current?.underlyingQueue?.label { - return "DispatchQueue: \(underlyingDispatchQueue)" - } else { - let name = __dispatch_queue_get_label(nil) - return String(cString: name, encoding: .utf8) ?? Thread.current.description - } - } -} diff --git a/Core/Core/Extensions/UIApplicationExtension.swift b/Core/Core/Extensions/UIApplicationExtension.swift deleted file mode 100644 index fe149414f..000000000 --- a/Core/Core/Extensions/UIApplicationExtension.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// UIApplicationExtension.swift -// Core -// -// Created by  Stepanok Ivan on 15.06.2023. -// - -import UIKit -import Theme - -extension UIApplication { - - public var keyWindow: UIWindow? { - UIApplication.shared.windows.first { $0.isKeyWindow } - } - - public func endEditing(force: Bool = true) { - windows.forEach { $0.endEditing(force) } - } - - public class func topViewController( - controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController - ) -> UIViewController? { - if let navigationController = controller as? UINavigationController { - return topViewController(controller: navigationController.visibleViewController) - } - if let tabController = controller as? UITabBarController { - if let selected = tabController.selectedViewController { - return topViewController(controller: selected) - } - } - if let presented = controller?.presentedViewController { - return topViewController(controller: presented) - } - return controller - } -} - -extension UINavigationController { - open override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - navigationBar.topItem?.backButtonDisplayMode = .minimal - navigationBar.barTintColor = .clear - navigationBar.setBackgroundImage(UIImage(), for: .default) - navigationBar.shadowImage = UIImage() - - let image = CoreAssets.arrowLeft.image - navigationBar.backIndicatorImage = image.withTintColor(Theme.UIColors.accentXColor) - navigationBar.backIndicatorTransitionMaskImage = image.withTintColor(Theme.UIColors.accentXColor) - navigationBar.titleTextAttributes = [ - .foregroundColor: Theme.UIColors.navigationBarTintColor, - .font: Theme.UIFonts.titleMedium() - ] - - UISegmentedControl.appearance().setTitleTextAttributes( - [ - .foregroundColor: Theme.Colors.textPrimary.uiColor(), - .font: Theme.UIFonts.labelLarge() - ], - for: .normal - ) - UISegmentedControl.appearance().setTitleTextAttributes( - [ - .foregroundColor: Theme.Colors.primaryButtonTextColor.uiColor(), - .font: Theme.UIFonts.labelLarge() - ], - for: .selected - ) - UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(Theme.Colors.accentXColor) - - UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = Theme.UIColors.accentXColor - } -} - -extension UINavigationController: UIGestureRecognizerDelegate { - override open func viewDidLoad() { - super.viewDidLoad() - interactivePopGestureRecognizer?.delegate = self - navigationItem.backButtonDisplayMode = .minimal - } - - public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - if #available(iOS 17, *) { - return false - } else { - return viewControllers.count > 1 - } - } -} diff --git a/Core/Core/Extensions/UINavigationController+Animation.swift b/Core/Core/Extensions/UINavigationController+Animation.swift deleted file mode 100644 index 4f720f78c..000000000 --- a/Core/Core/Extensions/UINavigationController+Animation.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// UINavigationController+Animation.swift -// Core -// -// Created by Vladimir Chekyrta on 23.02.2023. -// - -import Foundation -import UIKit - -public extension UINavigationController { - - func popFade( - transitionType type: CATransitionType = .fade, - duration: CFTimeInterval = 0.3 - ) { - addTransition(transitionType: type, duration: duration) - popViewController(animated: false) - } - - func pushFade( - viewController vc: UIViewController, - transitionType type: CATransitionType = .fade, - duration: CFTimeInterval = 0.3 - ) { - addTransition(transitionType: type, duration: duration) - pushViewController(vc, animated: UIAccessibility.isVoiceOverRunning) - } - - private func addTransition( - transitionType type: CATransitionType = .fade, - duration: CFTimeInterval = 0.3 - ) { - let transition = CATransition() - transition.duration = duration - transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) - transition.type = type - view.layer.add(transition, forKey: nil) - } - -} diff --git a/Core/Core/Extensions/UIResponder+CurrentResponder.swift b/Core/Core/Extensions/UIResponder+CurrentResponder.swift deleted file mode 100644 index f324d0596..000000000 --- a/Core/Core/Extensions/UIResponder+CurrentResponder.swift +++ /dev/null @@ -1,26 +0,0 @@ -// - -import Foundation -import UIKit - -extension UIResponder { - static var currentFirstResponder: UIResponder? { - _currentFirstResponder = nil - UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder(_:)), to: nil, from: nil, for: nil) - return _currentFirstResponder - } - - private weak static var _currentFirstResponder: UIResponder? - - @objc private func findFirstResponder(_: Any) { - UIResponder._currentFirstResponder = self - } - - var globalFrame: CGRect? { - guard let view = self as? UIView else { - return nil - } - - return view.superview?.convert(view.frame, to: nil) - } -} diff --git a/Core/Core/Extensions/UIView+EnclosingScrollView.swift b/Core/Core/Extensions/UIView+EnclosingScrollView.swift deleted file mode 100644 index 05314e334..000000000 --- a/Core/Core/Extensions/UIView+EnclosingScrollView.swift +++ /dev/null @@ -1,18 +0,0 @@ -// - -import UIKit - -extension UIView { - func enclosingScrollView() -> UIScrollView? { - var next: UIView? = self - - repeat { - next = next?.superview - if let scrollview = next as? UIScrollView { - return scrollview - } - } while next != nil - - return nil - } -} diff --git a/Core/Core/Extensions/UrlExtension.swift b/Core/Core/Extensions/UrlExtension.swift deleted file mode 100644 index cb09fdfc2..000000000 --- a/Core/Core/Extensions/UrlExtension.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// UrlExtension.swift -// Core -// -// Created by  Stepanok Ivan on 10.11.2022. -// - -import Foundation -import SwiftUI - -public extension URL { - func isImage() -> Bool { - if self.pathExtension == "jpg" - || self.pathExtension == "png" - || self.pathExtension == "PNG" - || self.pathExtension == "gif" - || self.pathExtension == "jpeg" - || self.pathExtension == "JPEG" - || self.pathExtension == "JPG" - || self.pathExtension == "bmp" { - return true - } else { - return false - } - } -} diff --git a/Core/Core/Extensions/ViewExtension.swift b/Core/Core/Extensions/ViewExtension.swift index 4d98df77d..cec65c994 100644 --- a/Core/Core/Extensions/ViewExtension.swift +++ b/Core/Core/Extensions/ViewExtension.swift @@ -90,55 +90,6 @@ public extension View { .padding(.horizontal, 48) } - func frameLimit(width: CGFloat? = nil) -> some View { - modifier(ReadabilityModifier(width: width)) - } - - @ViewBuilder - func adaptiveHStack( - spacing: CGFloat = 0, - currentOrientation: UIInterfaceOrientation, - @ViewBuilder content: () -> Content - ) -> some View { - if currentOrientation.isLandscape && UIDevice.current.userInterfaceIdiom != .pad { - VStack(alignment: .center, spacing: spacing, content: content) - } else if currentOrientation.isPortrait && UIDevice.current.userInterfaceIdiom != .pad { - HStack(spacing: spacing, content: content) - } else if UIDevice.current.userInterfaceIdiom != .phone { - HStack(spacing: spacing, content: content) - } - } - - @ViewBuilder - func adaptiveStack( - spacing: CGFloat = 0, - isHorizontal: Bool, - @ViewBuilder content: () -> Content - ) -> some View { - if isHorizontal, UIDevice.current.userInterfaceIdiom != .pad { - HStack(spacing: spacing, content: content) - } else { - VStack(alignment: .center, spacing: spacing, content: content) - } - } - - @ViewBuilder - func adaptiveNavigationStack( - spacing: CGFloat = 0, - isHorizontal: Bool, - @ViewBuilder content: () -> Content - ) -> some View { - if UIDevice.current.userInterfaceIdiom == .pad { - HStack(spacing: spacing, content: content) - } else { - if isHorizontal { - HStack(alignment: .top, spacing: spacing, content: content) - } else { - VStack(alignment: .center, spacing: spacing, content: content) - } - } - } - func roundedBackground( _ color: Color = Theme.Colors.background, strokeColor: Color = Theme.Colors.backgroundStroke, @@ -154,7 +105,7 @@ public extension View { .offset(y: 2) .foregroundColor(color) self - .offset(y: 2) + .offset(y: 2) } } @@ -178,80 +129,6 @@ public extension View { } } } - - func hideNavigationBar() -> some View { - if #available(iOS 16.0, *) { - return self.navigationBarHidden(true) - } else { - return self.introspect( - .navigationView(style: .stack), - on: .iOS(.v15...), - scope: .ancestor) { - $0.isNavigationBarHidden = true - } - } - } - - func hideScrollContentBackground() -> some View { - if #available(iOS 16.0, *) { - return self.scrollContentBackground(.hidden) - } else { - return self.onAppear { - UITextView.appearance().backgroundColor = .clear - } - } - } - - func onRightSwipeGesture(perform action: @escaping () -> Void) -> some View { - self.gesture( - DragGesture(minimumDistance: 20, coordinateSpace: .local) - .onEnded { value in - if value.translation.width > 0 && abs(value.translation.height) < 50 { - action() - } - } - ) - } - - func onBackground(_ f: @escaping () -> Void) -> some View { - self.onReceive( - NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification), - perform: { _ in f() } - ) - } - - func onForeground(_ f: @escaping () -> Void) -> some View { - self.onReceive( - NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification), - perform: { _ in f() } - ) - } - - func onFirstAppear(_ action: @escaping () -> Void) -> some View { - modifier(FirstAppear(action: action)) - } - - func backViewStyle(topPadding: CGFloat = -10) -> some View { - return self - .frame(height: 24) - .padding(.horizontal, 8) - .offset(y: topPadding) - } -} - -public extension View { - /// Applies the given transform if the given condition evaluates to `true`. - /// - Parameters: - /// - condition: The condition to evaluate. - /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. - @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { - if condition { - transform(self) - } else { - self - } - } } public extension View { @@ -285,22 +162,6 @@ public extension View { } } -private struct FirstAppear: ViewModifier { - let action: () -> Void - - // Use this to only fire your block one time - @State private var hasAppeared = false - - func body(content: Content) -> some View { - // And then, track it here - content.onAppear { - guard !hasAppeared else { return } - hasAppeared = true - action() - } - } -} - public extension Image { func backButtonStyle(topPadding: CGFloat = -10, color: Color = Theme.Colors.accentColor) -> some View { return self @@ -311,14 +172,3 @@ public extension Image { .backViewStyle(topPadding: topPadding) } } - -public extension EnvironmentValues { - var isHorizontal: Bool { - if UIDevice.current.userInterfaceIdiom != .pad { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { - return windowScene.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? true - } - } - return false - } -} diff --git a/Core/Core/Network/API.swift b/Core/Core/Network/API.swift deleted file mode 100644 index 7e3964b62..000000000 --- a/Core/Core/Network/API.swift +++ /dev/null @@ -1,304 +0,0 @@ -// -// API.swift -// Core -// -// Created by Vladimir Chekyrta on 13.09.2022. -// - -import Foundation -import Alamofire -import WebKit - -public final class API { - - private let session: Alamofire.Session - private let config: ConfigProtocol - - public init(session: Session, config: ConfigProtocol) { - self.session = session - self.config = config - } - - @discardableResult - public func requestData( - _ route: EndPointType - ) async throws -> Data { - switch route.task { - case .request: - return try await callData(route) - case let .requestParameters(parameters, encoding): - return try await callData(route, parameters: parameters, encoding: encoding) - case let .requestCodable(parameters, encoding): - let params = try? parameters?.asDictionary() - return try await callData(route, parameters: params, encoding: encoding) - case .requestCookies: - return try await callCookies(route) - case .upload: - throw APIError.invalidRequest - } - } - - public func request( - _ route: EndPointType - ) async throws -> HTTPURLResponse { - switch route.task { - case .request: - return try await callResponse(route) - case let .requestParameters(parameters, encoding): - return try await callResponse(route, parameters: parameters, encoding: encoding) - case let .requestCodable(parameters, encoding): - let params = try? parameters?.asDictionary() - return try await callResponse(route, parameters: params, encoding: encoding) - case .requestCookies: - return try await callResponse(route) - case let .upload(data): - return try await uploadData(route, data: data) - } - } - - private func callData( - _ route: EndPointType, - parameters: Parameters? = nil, - encoding: ParameterEncoding = URLEncoding.default - ) async throws -> Data { - var url = config.baseURL - if !route.path.isEmpty { - url = url.appendingPathComponent(route.path) - } - - let result = session.request( - url, - method: route.httpMethod, - parameters: parameters, - encoding: encoding, - headers: route.headers - ).validateResponse().serializingData() - - let latestVersion = await result.response.response?.headers["EDX-APP-LATEST-VERSION"] - - if await result.response.response?.statusCode != 426 { - if let latestVersion = latestVersion { - NotificationCenter.default.post(name: .onActualVersionReceived, object: latestVersion) - } - } - - return try await result.value - - } - - private func callCookies( - _ route: EndPointType, - parameters: Parameters? = nil, - encoding: ParameterEncoding = URLEncoding.default - ) async throws -> Data { - var url = config.baseURL - if !route.path.isEmpty { - url = url.appendingPathComponent(route.path) - } - let response = session.request( - url, - method: route.httpMethod, - parameters: parameters, - encoding: encoding, - headers: route.headers - ) - - let value = try await response.validateResponse().serializingData().value - - parseAndSetCookies(response: response.response) - return value - } - - private func uploadData( - _ route: EndPointType, - data: Data - ) async throws -> HTTPURLResponse { - var url = config.baseURL - if !route.path.isEmpty { - url = url.appendingPathComponent(route.path) - } - - let response = await session.request( - url, - method: route.httpMethod, - encoding: UploadBodyEncoding(body: data), - headers: route.headers - ).validateResponse().serializingResponse(using: .string).response - - if let response = response.response { - return response - } else if let error = response.error { - throw error - } else { - throw APIError.unknown - } - } - - private func parseAndSetCookies(response: HTTPURLResponse?) { - guard let fields = response?.allHeaderFields as? [String: String] else { return } - let url = config.baseURL - let cookies = HTTPCookie.cookies(withResponseHeaderFields: fields, for: url) - HTTPCookieStorage.shared.cookies?.forEach { HTTPCookieStorage.shared.deleteCookie($0) } - HTTPCookieStorage.shared.setCookies(cookies, for: url, mainDocumentURL: nil) - DispatchQueue.main.async { - let cookies = HTTPCookieStorage.shared.cookies ?? [] - for c in cookies { - WKWebsiteDataStore.default().httpCookieStore.setCookie(c) - } - } - } - - private func callResponse( - _ route: EndPointType, - parameters: Parameters? = nil, - encoding: ParameterEncoding = URLEncoding.default - ) async throws -> HTTPURLResponse { - var url = config.baseURL - if !route.path.isEmpty { - url = url.appendingPathComponent(route.path) - } - let serializer = DataResponseSerializer(emptyResponseCodes: [200, 204, 205]) - - let response = await session.request( - url, - method: route.httpMethod, - parameters: parameters, - encoding: encoding, - headers: route.headers - ).validateResponse().serializingResponse(using: serializer).response - - if let error = response.error { - throw error - } else if let response = response.response { - return response - } else { - throw APIError.unknown - } - } -} - -public enum APIError: Int, LocalizedError { - case unknown = -100 - case emptyData = -200 - case invalidGrant = -300 - case parsingError = -400 - case invalidRequest = -500 - case uploadError = -600 - - public var errorDescription: String? { - switch self { - default: - return nil - } - } - - public var localizedDescription: String { - return errorDescription ?? "" - } -} - -public struct CustomValidationError: LocalizedError { - public let statusCode: Int - public let data: [String: Any]? - - public init(statusCode: Int, data: [String: Any]?) { - self.statusCode = statusCode - self.data = data - } -} - -extension DataRequest { - func validateResponse() -> Self { - return validateStatusCode().validateContentType() - } - - func validateStatusCode() -> Self { - return validate { _, response, data in - switch response.statusCode { - case 200...299: - return .success(()) - case 400...403: - if let data { - if let dataString = String(data: data, encoding: .utf8) { - if dataString.first == "{" && dataString.last == "}" { - let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] - return .failure(CustomValidationError(statusCode: response.statusCode, data: json)) - } else { - let reason: AFError.ResponseValidationFailureReason - = .unacceptableStatusCode(code: response.statusCode) - return .failure(AFError.responseValidationFailed(reason: reason)) - } - } - } - let reason: AFError.ResponseValidationFailureReason = .unacceptableStatusCode(code: response.statusCode) - return .failure(AFError.responseValidationFailed(reason: reason)) - default: - let reason: AFError.ResponseValidationFailureReason = .unacceptableStatusCode(code: response.statusCode) - return .failure(AFError.responseValidationFailed(reason: reason)) - } - } - } - - func validateContentType() -> Self { - let contentTypes: () -> [String] = { [unowned self] in - if let accept = request?.value(forHTTPHeaderField: "Accept") { - return accept.components(separatedBy: ",") - } - return ["*/*"] - } - return validate(contentType: contentTypes()) - } -} - -public struct CustomGetEncoding: ParameterEncoding { - public init() {} - public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { - var request = try URLEncoding().encode(urlRequest, with: parameters) - request.url = URL(string: request.url!.absoluteString.replacingOccurrences(of: "%5B%5D=", with: "=")) - return request - } -} - -extension Encodable { - func asDictionary() throws -> [String: Any] { - let data = try JSONEncoder().encode(self) - guard let dictionary = try JSONSerialization.jsonObject( - with: data, - options: .fragmentsAllowed - ) as? [String: Any] else { - throw NSError() - } - return dictionary - } -} - -public extension Data { - func mapResponse(_ decodableType: NewSuccess.Type) throws -> NewSuccess where NewSuccess: Decodable { - do { - let baseResponse = try JSONDecoder().decode(NewSuccess.self, from: self) - - return baseResponse - } catch { - print(error) - throw APIError.parsingError - } - } -} - -public extension Error { - var validationError: CustomValidationError? { - if let afError = self.asAFError, case AFError.responseValidationFailed(let reason) = afError { - if case AFError.ResponseValidationFailureReason.customValidationFailed(let error) = reason { - return error as? CustomValidationError - } - } - return nil - } -} - -// Mark - For testing and SwiftUI preview -#if DEBUG -public extension API { - static let mock: API = .init(session: Alamofire.Session.default, config: ConfigMock()) -} -#endif diff --git a/Core/Core/Network/Alamofire+Error.swift b/Core/Core/Network/Alamofire+Error.swift deleted file mode 100644 index 277a79fed..000000000 --- a/Core/Core/Network/Alamofire+Error.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// Alamofire+Error.swift -// Core -// -// Created by Vladimir Chekyrta on 14.09.2022. -// - -import Alamofire - -public extension Error { - var isUpdateRequeiredError: Bool { - self.asAFError?.responseCode == 426 - } - - var isInternetError: Bool { - guard let afError = self.asAFError, - let urlError = afError.underlyingError as? URLError else { - return false - } - switch urlError.code { - case .timedOut, .cannotConnectToHost, .networkConnectionLost, - .notConnectedToInternet, .resourceUnavailable, .internationalRoamingOff, - .dataNotAllowed: - return true - default: - return false - } - } -} diff --git a/Core/Core/Network/AuthEndpoint.swift b/Core/Core/Network/AuthEndpoint.swift index e93e5c860..c4f70adc3 100644 --- a/Core/Core/Network/AuthEndpoint.swift +++ b/Core/Core/Network/AuthEndpoint.swift @@ -7,6 +7,7 @@ import Foundation import Alamofire +import OEXFoundation enum AuthEndpoint: EndPointType { case getAccessToken(username: String, password: String, clientId: String, tokenType: String) @@ -29,9 +30,9 @@ enum AuthEndpoint: EndPointType { case .getAuthCookies: return "/oauth2/login/" case .getRegisterFields: - return "user_api/v1/account/registration/" + return "/user_api/v1/account/registration/" case .registerUser: - return "user_api/v1/account/registration/" + return "/user_api/v1/account/registration/" case .validateRegistrationFields: return "/api/user/v1/validation/registration" case .resetPassword: diff --git a/Core/Core/Network/DownloadManager.swift b/Core/Core/Network/DownloadManager.swift index 2f967597a..d7e277066 100644 --- a/Core/Core/Network/DownloadManager.swift +++ b/Core/Core/Network/DownloadManager.swift @@ -5,9 +5,11 @@ // Created by  Stepanok Ivan on 08.03.2023. // -import Alamofire import SwiftUI import Combine +import ZipArchive +import OEXFoundation +import Alamofire public enum DownloadState: String { case waiting @@ -17,17 +19,18 @@ public enum DownloadState: String { public var order: Int { switch self { case .inProgress: - 1 + return 1 case .waiting: - 2 + return 2 case .finished: - 3 + return 3 } } } public enum DownloadType: String { case video + case html, problem } public struct DownloadDataTask: Identifiable, Hashable { @@ -43,6 +46,7 @@ public struct DownloadDataTask: Identifiable, Hashable { public var state: DownloadState public let type: DownloadType public let fileSize: Int + public var lastModified: String? public var fileSizeInMb: Double { Double(fileSize) / 1024.0 / 1024.0 @@ -64,7 +68,8 @@ public struct DownloadDataTask: Identifiable, Hashable { resumeData: Data?, state: DownloadState, type: DownloadType, - fileSize: Int + fileSize: Int, + lastModified: String ) { self.id = id self.courseId = courseId @@ -78,6 +83,7 @@ public struct DownloadDataTask: Identifiable, Hashable { self.state = state self.type = type self.fileSize = fileSize + self.lastModified = lastModified } public init(sourse: CDDownloadData) { @@ -93,6 +99,7 @@ public struct DownloadDataTask: Identifiable, Hashable { self.state = DownloadState(rawValue: sourse.state ?? "") ?? .waiting self.type = DownloadType(rawValue: sourse.type ?? "") ?? .video self.fileSize = Int(sourse.fileSize) + self.lastModified = sourse.lastModified } } @@ -106,7 +113,7 @@ public protocol DownloadManagerProtocol { func publisher() -> AnyPublisher func eventPublisher() -> AnyPublisher - func addToDownloadQueue(blocks: [CourseBlock]) throws + func addToDownloadQueue(blocks: [CourseBlock]) async throws func getDownloadTasks() async -> [DownloadDataTask] func getDownloadTasksForCourse(_ courseId: String) async -> [DownloadDataTask] @@ -119,11 +126,13 @@ public protocol DownloadManagerProtocol { func deleteFile(blocks: [CourseBlock]) async func deleteAllFiles() async - func fileUrl(for blockId: String) async -> URL? - - func resumeDownloading() throws func fileUrl(for blockId: String) -> URL? + func updateUnzippedFileSize(for sequentials: [CourseSequential]) -> [CourseSequential] + + func resumeDownloading() async throws func isLargeVideosSize(blocks: [CourseBlock]) -> Bool + + func removeAppSupportDirectoryUnusedContent() } public enum DownloadManagerEvent { @@ -140,7 +149,6 @@ public enum DownloadManagerEvent { } public class DownloadManager: DownloadManagerProtocol { - // MARK: - Properties public var currentDownloadTask: DownloadDataTask? @@ -152,6 +160,9 @@ public class DownloadManager: DownloadManagerProtocol { private var currentDownloadEventPublisher: PassthroughSubject = .init() private let backgroundTaskProvider = BackgroundTaskProvider() private var cancellables = Set() + private var failedDownloads: [DownloadDataTask] = [] + + private let indexPage = "index.html" private var downloadQuality: DownloadQuality { appStorage.userSettings?.downloadQuality ?? .auto @@ -171,7 +182,23 @@ public class DownloadManager: DownloadManagerProtocol { self.appStorage = appStorage self.connectivity = connectivity self.backgroundTask() - try? self.resumeDownloading() + Task { + try? await self.resumeDownloading() + } + + NotificationCenter.default.publisher(for: .tryDownloadAgain) + .compactMap { $0.object as? [DownloadDataTask] } + .sink { [weak self] downloads in + self?.tryDownloadAgain(downloads: downloads) + } + .store(in: &cancellables) + } + + private func tryDownloadAgain(downloads: [DownloadDataTask]) { + persistence.addToDownloadQueue(tasks: downloads) + Task { + try? await newDownload() + } } // MARK: - Publishers @@ -189,70 +216,67 @@ public class DownloadManager: DownloadManagerProtocol { // MARK: - Intents public func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { - (blocks.reduce(0) { - $0 + Double($1.encodedVideo?.video(downloadQuality: downloadQuality)?.fileSize ?? 0) - } / 1024 / 1024 / 1024) > 1 + let totalSizeInBytes = blocks.reduce(0) { accumulator, block in + let videoSize = block.encodedVideo?.video(downloadQuality: downloadQuality)?.fileSize ?? 0 + return accumulator + Double(videoSize) + } + + let totalSizeInGB = totalSizeInBytes / (1024 * 1024 * 1024) + + return totalSizeInGB > 1 } public func getDownloadTasks() async -> [DownloadDataTask] { - await withCheckedContinuation { continuation in - persistence.getDownloadDataTasks { downloads in - continuation.resume(returning: downloads) - } - } + await persistence.getDownloadDataTasks() } public func getDownloadTasksForCourse(_ courseId: String) async -> [DownloadDataTask] { - await withCheckedContinuation { continuation in - persistence.getDownloadDataTasksForCourse(courseId) { downloads in - continuation.resume(returning: downloads) - } - } + await persistence.getDownloadDataTasksForCourse(courseId) } - public func addToDownloadQueue(blocks: [CourseBlock]) throws { + public func addToDownloadQueue(blocks: [CourseBlock]) async throws { if userCanDownload() { - persistence.addToDownloadQueue( + await persistence.addToDownloadQueue( blocks: blocks, downloadQuality: downloadQuality ) currentDownloadEventPublisher.send(.added) guard !isDownloadingInProgress else { return } - try newDownload() + try await newDownload() } else { throw NoWiFiError() } } - public func resumeDownloading() throws { - try newDownload() + public func resumeDownloading() async throws { + try await newDownload() } public func cancelDownloading(courseId: String, blocks: [CourseBlock]) async throws { downloadRequest?.cancel() - let downloaded = await getDownloadTasksForCourse(courseId).filter { $0.state == .finished } + let downloaded = await getDownloadTasksForCourse(courseId) let blocksForDelete = blocks.filter { block in - downloaded.first(where: { $0.blockId == block.id }) == nil + downloaded.first(where: { $0.blockId == block.id }) != nil } await deleteFile(blocks: blocksForDelete) downloaded.forEach { currentDownloadEventPublisher.send(.canceled($0)) } - try newDownload() + try await newDownload() } public func cancelDownloading(task: DownloadDataTask) async throws { downloadRequest?.cancel() do { - try persistence.deleteDownloadDataTask(id: task.id) - if let fileUrl = await fileUrl(for: task.id) { + if let fileUrl = fileUrl(for: task.id) { try FileManager.default.removeItem(at: fileUrl) } + try await persistence.deleteDownloadDataTask(id: task.id) currentDownloadEventPublisher.send(.canceled(task)) } catch { NSLog("Error deleting file: \(error.localizedDescription)") } - try newDownload() + try await newDownload() } public func cancelDownloading(courseId: String) async throws { @@ -260,7 +284,7 @@ public class DownloadManager: DownloadManagerProtocol { await cancel(tasks: tasks) currentDownloadEventPublisher.send(.courseCanceled(courseId)) downloadRequest?.cancel() - try newDownload() + try await newDownload() } public func cancelAllDownloading() async throws { @@ -268,27 +292,83 @@ public class DownloadManager: DownloadManagerProtocol { await cancel(tasks: tasks) currentDownloadEventPublisher.send(.allCanceled) downloadRequest?.cancel() - try newDownload() + try await newDownload() } public func deleteFile(blocks: [CourseBlock]) async { for block in blocks { do { - try persistence.deleteDownloadDataTask(id: block.id) - currentDownloadEventPublisher.send(.deletedFile(block.id)) - if let fileURL = await fileUrl(for: block.id) { + if let fileURL = fileOrFolderUrl(for: block.id), + FileManager.default.fileExists(atPath: fileURL.path) { try FileManager.default.removeItem(at: fileURL) } + try await persistence.deleteDownloadDataTask(id: block.id) + currentDownloadEventPublisher.send(.deletedFile(block.id)) } catch { debugLog("Error deleting file: \(error.localizedDescription)") } } } + public func updateUnzippedFileSize(for sequentials: [CourseSequential]) -> [CourseSequential] { + var updatedSequentials = sequentials + + for i in 0.. Int { + let fileManager = FileManager.default + let resourceKeys: [URLResourceKey] = [.isDirectoryKey, .fileSizeKey] + var totalSize: Int64 = 0 + + if let enumerator = fileManager.enumerator( + at: url, + includingPropertiesForKeys: resourceKeys, + options: [], + errorHandler: nil + ) { + for case let fileUrl as URL in enumerator { + let resourceValues = try fileUrl.resourceValues(forKeys: Set(resourceKeys)) + if resourceValues.isDirectory == false { + if let fileSize = resourceValues.fileSize { + totalSize += Int64(fileSize) + } + } + } + } + + return Int(totalSize) + } + public func deleteAllFiles() async { let downloadsData = await getDownloadTasks() for downloadData in downloadsData { - if let fileURL = await fileUrl(for: downloadData.id) { + if let fileURL = fileOrFolderUrl(for: downloadData.id) { do { try FileManager.default.removeItem(at: fileURL) } catch { @@ -298,42 +378,74 @@ public class DownloadManager: DownloadManagerProtocol { } currentDownloadEventPublisher.send(.clearedAll) } - - public func fileUrl(for blockId: String) async -> URL? { - await withCheckedContinuation { continuation in - persistence.downloadDataTask(for: blockId) { [weak self] data in - guard let data = data, data.url.count > 0, data.state == .finished else { - continuation.resume(returning: nil) - return - } - let path = self?.videosFolderUrl - let fileName = data.fileName - continuation.resume(returning: path?.appendingPathComponent(fileName)) + + public func fileUrl(for blockId: String) -> URL? { + guard let data = persistence.downloadDataTask(for: blockId), + data.url.count > 0, + data.state == .finished else { return nil } + let path = filesFolderUrl + switch data.type { + case .html, .problem: + if let folderUrl = URL(string: data.url) { + let folder = folderUrl.deletingPathExtension().lastPathComponent + return path?.appendingPathComponent(folder).appendingPathComponent(indexPage) + } else { + return nil } + case .video: + return path?.appendingPathComponent(data.fileName) } } - - public func fileUrl(for blockId: String) -> URL? { + + public func fileOrFolderUrl(for blockId: String) -> URL? { guard let data = persistence.downloadDataTask(for: blockId), data.url.count > 0, data.state == .finished else { return nil } - let path = videosFolderUrl - let fileName = data.fileName - return path?.appendingPathComponent(fileName) + let path = filesFolderUrl + switch data.type { + case .html, .problem: + if let folderUrl = URL(string: data.url) { + let folder = folderUrl.deletingPathExtension().lastPathComponent + return path?.appendingPathComponent(folder) + } else { + return nil + } + case .video: + return path?.appendingPathComponent(data.fileName) + } } // MARK: - Private Intents - private func newDownload() throws { + private func newDownload() async throws { guard userCanDownload() else { throw NoWiFiError() } - guard let downloadTask = persistence.nextBlockForDownloading() else { + guard let downloadTask = await persistence.nextBlockForDownloading() else { isDownloadingInProgress = false + if !failedDownloads.isEmpty { + DispatchQueue.main.async { + NotificationCenter.default.post( + name: .showDownloadFailed, + object: self.failedDownloads + ) + self.failedDownloads = [] + } + } return } + if !connectivity.isInternetAvaliable { + failedDownloads.append(downloadTask) + try await cancelDownloading(task: downloadTask) + return + } + currentDownloadTask = downloadTask - try downloadFileWithProgress(downloadTask) + if downloadTask.type == .html || downloadTask.type == .problem { + try downloadHTMLWithProgress(downloadTask) + } else { + try downloadFileWithProgress(downloadTask) + } currentDownloadEventPublisher.send(.started(downloadTask)) } @@ -350,7 +462,7 @@ public class DownloadManager: DownloadManagerProtocol { } private func downloadFileWithProgress(_ download: DownloadDataTask) throws { - guard let url = URL(string: download.url) else { + guard let url = URL(string: download.url), let folderURL = self.filesFolderUrl else { return } @@ -360,26 +472,40 @@ public class DownloadManager: DownloadManagerProtocol { resumeData: download.resumeData ) self.isDownloadingInProgress = true + + let destination: DownloadRequest.Destination = { _, _ in + let file = folderURL.appendingPathComponent(download.fileName) + return (file, [.createIntermediateDirectories, .removePreviousFile]) + } + if let resumeData = download.resumeData { - downloadRequest = AF.download(resumingWith: resumeData) + downloadRequest = AF.download(resumingWith: resumeData, to: destination) } else { - downloadRequest = AF.download(url) + downloadRequest = AF.download(url, to: destination) } - downloadRequest?.downloadProgress { [weak self] prog in - guard let self else { return } + downloadRequest?.downloadProgress { [weak self] prog in + guard let self = self else { return } let fractionCompleted = prog.fractionCompleted self.currentDownloadTask?.progress = fractionCompleted self.currentDownloadTask?.state = .inProgress self.currentDownloadEventPublisher.send(.progress(fractionCompleted, download)) let completed = Double(fractionCompleted * 100) - debugLog(">>>>> Downloading", download.url, completed, "%") + debugLog(">>>>> Downloading File", download.url, completed, "%") } - downloadRequest?.responseData { [weak self] data in - guard let self else { return } - if let data = data.value, let url = self.videosFolderUrl { - self.saveFile(fileName: download.fileName, data: data, folderURL: url) + downloadRequest?.responseURL { [weak self] response in + guard let self = self else { return } + if let error = response.error { + if error.asAFError?.isExplicitlyCancelledError == false { + self.failedDownloads.append(download) + Task { + try? await self.newDownload() + } + return + } + } + if response.fileURL != nil { self.persistence.updateDownloadState( id: download.id, state: .finished, @@ -387,35 +513,94 @@ public class DownloadManager: DownloadManagerProtocol { ) self.currentDownloadTask?.state = .finished self.currentDownloadEventPublisher.send(.finished(download)) - try? self.newDownload() + Task { + try? await self.newDownload() + } } } } - private func waitingAll() { - persistence.getDownloadDataTasks { [weak self] tasks in + private func downloadHTMLWithProgress(_ download: DownloadDataTask) throws { + guard let url = URL(string: download.url), let folderURL = self.filesFolderUrl else { + return + } + + persistence.updateDownloadState( + id: download.id, + state: .inProgress, + resumeData: download.resumeData + ) + self.isDownloadingInProgress = true + + let destination: DownloadRequest.Destination = { _, _ in + let fileName = URL(string: download.url)?.lastPathComponent ?? "file.zip" + let file = folderURL.appendingPathComponent(fileName) + return (file, [.createIntermediateDirectories, .removePreviousFile]) + } + + if let resumeData = download.resumeData { + downloadRequest = AF.download(resumingWith: resumeData, to: destination) + } else { + downloadRequest = AF.download(url, to: destination) + } + + downloadRequest?.downloadProgress { [weak self] prog in guard let self else { return } - Task { - for task in tasks.filter({ $0.state == .inProgress }) { - self.persistence.updateDownloadState( - id: task.id, - state: .waiting, - resumeData: nil - ) - self.currentDownloadEventPublisher.send(.added) + let fractionCompleted = prog.fractionCompleted + self.currentDownloadTask?.progress = fractionCompleted + self.currentDownloadTask?.state = .inProgress + self.currentDownloadEventPublisher.send(.progress(fractionCompleted, download)) + let completed = Double(fractionCompleted * 100) + debugLog(">>>>> Downloading HTML", download.url, completed, "%") + } + + downloadRequest?.responseURL { [weak self] response in + guard let self else { return } + if let error = response.error { + if error.asAFError?.isExplicitlyCancelledError == false { + failedDownloads.append(download) + Task { + try? await self.newDownload() + } + return } - self.downloadRequest?.cancel() } + if let fileURL = response.fileURL { + self.unzipFile(url: fileURL) + self.persistence.updateDownloadState( + id: download.id, + state: .finished, + resumeData: nil + ) + self.currentDownloadTask?.state = .finished + self.currentDownloadEventPublisher.send(.finished(download)) + Task { + try? await self.newDownload() + } + } + } + } + + private func waitingAll() async { + let tasks = await persistence.getDownloadDataTasks() + for task in tasks.filter({ $0.state == .inProgress }) { + self.persistence.updateDownloadState( + id: task.id, + state: .waiting, + resumeData: nil + ) + self.currentDownloadEventPublisher.send(.added) } + self.downloadRequest?.cancel() } private func cancel(tasks: [DownloadDataTask]) async { for task in tasks { do { - try persistence.deleteDownloadDataTask(id: task.id) - if let fileUrl = await fileUrl(for: task.id) { + if let fileUrl = fileUrl(for: task.id) { try FileManager.default.removeItem(at: fileUrl) } + try await persistence.deleteDownloadDataTask(id: task.id) } catch { debugLog("Error deleting file: \(error.localizedDescription)") } @@ -426,16 +611,19 @@ public class DownloadManager: DownloadManagerProtocol { backgroundTaskProvider.eventPublisher() .sink { [weak self] state in guard let self else { return } - switch state { - case.didBecomeActive: try? self.resumeDownloading() - case .didEnterBackground: self.waitingAll() + Task { + switch state { + case.didBecomeActive: try? await self.resumeDownloading() + case .didEnterBackground: await self.waitingAll() + } } } .store(in: &cancellables) } - lazy var videosFolderUrl: URL? = { + var filesFolderUrl: URL? { let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + guard let folderPathComponent else { return nil } let directoryURL = documentDirectoryURL.appendingPathComponent(folderPathComponent, isDirectory: true) if FileManager.default.fileExists(atPath: directoryURL.path) { @@ -453,13 +641,13 @@ public class DownloadManager: DownloadManagerProtocol { return nil } } - }() + } - private var folderPathComponent: String { + private var folderPathComponent: String? { if let id = appStorage.user?.id { return "\(id)_Files" } - return "Files" + return nil } private func saveFile(fileName: String, data: Data, folderURL: URL) { @@ -470,6 +658,87 @@ public class DownloadManager: DownloadManagerProtocol { debugLog("SaveFile Error", error.localizedDescription) } } + + private func unzipFile(url: URL) { + let fileName = url.deletingPathExtension().lastPathComponent + guard let directoryURL = filesFolderUrl else { + return + } + let uniqueDirectory = directoryURL.appendingPathComponent(fileName, isDirectory: true) + + try? FileManager.default.removeItem(at: uniqueDirectory) + + do { + try FileManager.default.createDirectory( + at: uniqueDirectory, + withIntermediateDirectories: true, + attributes: nil + ) + } catch { + debugLog("Error creating temporary directory: \(error.localizedDescription)") + } + SSZipArchive.unzipFile(atPath: url.path, toDestination: uniqueDirectory.path) + + do { + try FileManager.default.removeItem(at: url) + } catch { + debugLog("Error removing file: \(error.localizedDescription)") + } + } + + public func removeAppSupportDirectoryUnusedContent() { + deleteMD5HashedFolders() + } + + private func getApplicationSupportDirectory() -> URL? { + let fileManager = FileManager.default + do { + let appSupportDirectory = try fileManager.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + return appSupportDirectory + } catch { + debugPrint("Error getting Application Support Directory: \(error)") + return nil + } + } + + func isMD5Hash(_ folderName: String) -> Bool { + let md5Regex = "^[a-fA-F0-9]{32}$" + let predicate = NSPredicate(format: "SELF MATCHES %@", md5Regex) + return predicate.evaluate(with: folderName) + } + + private func deleteMD5HashedFolders() { + guard let appSupportDirectory = getApplicationSupportDirectory() else { + return + } + + let fileManager = FileManager.default + do { + let folderContents = try fileManager.contentsOfDirectory( + at: appSupportDirectory, + includingPropertiesForKeys: nil, + options: [] + ) + for folderURL in folderContents { + let folderName = folderURL.lastPathComponent + if isMD5Hash(folderName) { + do { + try fileManager.removeItem(at: folderURL) + debugPrint("Deleted folder: \(folderName)") + } catch { + debugPrint("Error deleting folder \(folderName): \(error)") + } + } + } + } catch { + debugPrint("Error reading contents of Application Support directory: \(error)") + } + } } @available(iOSApplicationExtension, unavailable) @@ -551,12 +820,13 @@ public final class BackgroundTaskProvider { } // Mark - For testing and SwiftUI preview +// swiftlint:disable file_length #if DEBUG public class DownloadManagerMock: DownloadManagerProtocol { - public init() { - - } + public init() {} + + public func updateUnzippedFileSize(for sequentials: [CourseSequential]) -> [CourseSequential] {[]} public var currentDownloadTask: DownloadDataTask? { return nil @@ -581,15 +851,14 @@ public class DownloadManagerMock: DownloadManagerProtocol { resumeData: nil, state: .inProgress, type: .video, - fileSize: 0 + fileSize: 0, + lastModified: "" ) ) ).eraseToAnyPublisher() } - public func addToDownloadQueue(blocks: [CourseBlock]) { - - } + public func addToDownloadQueue(blocks: [CourseBlock]) {} public func getDownloadTasks() -> [DownloadDataTask] { [] @@ -601,34 +870,20 @@ public class DownloadManagerMock: DownloadManagerProtocol { } } - public func cancelDownloading(courseId: String, blocks: [CourseBlock]) async throws { + public func cancelDownloading(courseId: String, blocks: [CourseBlock]) async throws {} - } + public func cancelDownloading(task: DownloadDataTask) {} - public func cancelDownloading(task: DownloadDataTask) { + public func cancelDownloading(courseId: String) async {} - } + public func cancelAllDownloading() async throws {} - public func cancelDownloading(courseId: String) async { + public func resumeDownloading() {} - } + public func deleteFile(blocks: [CourseBlock]) {} - public func cancelAllDownloading() async throws { + public func deleteAllFiles() {} - } - - public func resumeDownloading() { - - } - - public func deleteFile(blocks: [CourseBlock]) { - - } - - public func deleteAllFiles() { - - } - public func fileUrl(for blockId: String) -> URL? { return nil } @@ -637,5 +892,7 @@ public class DownloadManagerMock: DownloadManagerProtocol { false } + public func removeAppSupportDirectoryUnusedContent() {} } #endif +// swiftlint:enable file_length diff --git a/Core/Core/Network/EndPointType.swift b/Core/Core/Network/EndPointType.swift deleted file mode 100644 index f27534429..000000000 --- a/Core/Core/Network/EndPointType.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// EndPointType.swift -// Core -// -// Created by Vladimir Chekyrta on 13.09.2022. -// - -import Foundation -import Alamofire - -public protocol EndPointType { - var path: String { get } - var httpMethod: HTTPMethod { get } - var headers: HTTPHeaders? { get } - var task: HTTPTask { get } -} diff --git a/Core/Core/Network/HTTPTask.swift b/Core/Core/Network/HTTPTask.swift deleted file mode 100644 index 5e07200f7..000000000 --- a/Core/Core/Network/HTTPTask.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// HTTPTask.swift -// Core -// -// Created by Vladimir Chekyrta on 13.09.2022. -// - -import Foundation -import Alamofire - -public enum HTTPTask { - case request - case requestCookies - case requestParameters(parameters: Parameters? = nil, encoding: ParameterEncoding) - case requestCodable(parameters: Encodable? = nil, encoding: ParameterEncoding) - case upload(Data) -} diff --git a/Core/Core/Network/HeadersRedirectHandler.swift b/Core/Core/Network/HeadersRedirectHandler.swift deleted file mode 100644 index 3653392bd..000000000 --- a/Core/Core/Network/HeadersRedirectHandler.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// HeadersRedirectHandler.swift -// Core -// -// Created by Vladimir Chekyrta on 14.09.2022. -// - -import Foundation -import Alamofire - -public class HeadersRedirectHandler: RedirectHandler { - - public init() { - } - - public func task( - _ task: URLSessionTask, - willBeRedirectedTo request: URLRequest, - for response: HTTPURLResponse, - completion: @escaping (URLRequest?) -> Void - ) { - var redirectedRequest = request - - if let originalRequest = task.originalRequest, - let headers = originalRequest.allHTTPHeaderFields { - for (key, value) in headers { - redirectedRequest.setValue(value, forHTTPHeaderField: key) - } - } - - completion(redirectedRequest) - } -} diff --git a/Core/Core/Network/NetworkLogger.swift b/Core/Core/Network/NetworkLogger.swift deleted file mode 100644 index f41f1f103..000000000 --- a/Core/Core/Network/NetworkLogger.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// NetworkLogger.swift -// Core -// -// Created by Vladimir Chekyrta on 13.09.2022. -// - -import Alamofire - -public class NetworkLogger: EventMonitor { - - public let queue = DispatchQueue(label: "com.raccoongang.networklogger") - - public init() { - } - - public func requestDidResume(_ request: Request) { - print("Request:", request.description) - if let headers = request.request?.headers { - print("Headers:") - print(headers) - print("------") - } - if let body = request.request?.httpBody, let value = String(data: body, encoding: .utf8) { - print("Body:") - print(value) - print("------") - } - } - - public func request(_ request: DataRequest, didParseResponse response: DataResponse) { - guard let data = response.data else { - return - } - guard let responseValue = String(data: data, encoding: .utf8) else { - return - } - print("Response:", request.description) - print(responseValue) - -// if let json = try? JSONSerialization -// .jsonObject(with: data, options: .mutableContainers) { -// print(json) -// } - } - -} diff --git a/Core/Core/Network/OfflineSyncEndpoint.swift b/Core/Core/Network/OfflineSyncEndpoint.swift new file mode 100644 index 000000000..0db8fdbc6 --- /dev/null +++ b/Core/Core/Network/OfflineSyncEndpoint.swift @@ -0,0 +1,65 @@ +// +// OfflineSyncEndpoint.swift +// Core +// +// Created by  Stepanok Ivan on 20.06.2024. +// + +import Foundation +import Alamofire +import OEXFoundation + +enum OfflineSyncEndpoint: EndPointType { + case submitOfflineProgress(courseID: String, blockID: String, data: String) + + var path: String { + switch self { + case let .submitOfflineProgress(courseID, blockID, _): + return "/courses/\(courseID)/xblock/\(blockID)/handler/xmodule_handler/problem_check" + } + } + + var httpMethod: HTTPMethod { + switch self { + case .submitOfflineProgress: + return .post + } + } + + var headers: HTTPHeaders? { + nil + } + + var task: HTTPTask { + switch self { + case let .submitOfflineProgress(_, _, data): + return .requestParameters(parameters: decode(query: data), encoding: URLEncoding.httpBody) + } + } + + func decode(query: String) -> Parameters { + var parameters: Parameters = [:] + + let pairs = query.split(separator: "&") + for pair in pairs { + let keyValue = pair.split(separator: "=") + if keyValue.count == 2 { + let key = String(keyValue[0]).removingPercentEncoding! + let value = String(keyValue[1]).removingPercentEncoding! + + if key.hasSuffix("[]") { + let trimmedKey = String(key.dropLast(2)) + if parameters[trimmedKey] == nil { + parameters[trimmedKey] = [value] + } else if var existingArray = parameters[trimmedKey] as? [String] { + existingArray.append(value) + parameters[trimmedKey] = existingArray + } + } else { + parameters[key] = value + } + } + } + return parameters + } +} diff --git a/Core/Core/Network/OfflineSyncManager.swift b/Core/Core/Network/OfflineSyncManager.swift new file mode 100644 index 000000000..295d79bec --- /dev/null +++ b/Core/Core/Network/OfflineSyncManager.swift @@ -0,0 +1,86 @@ +// +// OfflineSyncManager.swift +// Core +// +// Created by  Stepanok Ivan on 20.06.2024. +// + +import Foundation +import WebKit +import Combine +import Swinject +import OEXFoundation + +public protocol OfflineSyncManagerProtocol { + func handleMessage(message: WKScriptMessage, blockID: String) + func syncOfflineProgress() async +} + +public class OfflineSyncManager: OfflineSyncManagerProtocol { + + let persistence: CorePersistenceProtocol + let interactor: OfflineSyncInteractorProtocol + let connectivity: ConnectivityProtocol + private var cancellables = Set() + + public init( + persistence: CorePersistenceProtocol, + interactor: OfflineSyncInteractorProtocol, + connectivity: ConnectivityProtocol + ) { + self.persistence = persistence + self.interactor = interactor + self.connectivity = connectivity + + self.connectivity.internetReachableSubject.sink(receiveValue: { state in + switch state { + case .reachable: + Task(priority: .low) { + await self.syncOfflineProgress() + } + case .notReachable, nil: + break + } + }).store(in: &cancellables) + } + + public func handleMessage(message: WKScriptMessage, blockID: String) { + if message.name == "IOSBridge", + let progressJson = message.body as? String { + persistence.saveOfflineProgress( + progress: OfflineProgress( + progressJson: progressJson + ) + ) + var correctedProgressJson = progressJson + correctedProgressJson = correctedProgressJson.removingPercentEncoding ?? correctedProgressJson + message.webView?.evaluateJavaScript("markProblemCompleted('\(correctedProgressJson)')") + } else if let offlineProgress = persistence.loadProgress(for: blockID) { + var correctedProgressJson = offlineProgress.progressJson + correctedProgressJson = correctedProgressJson.removingPercentEncoding ?? correctedProgressJson + message.webView?.evaluateJavaScript("markProblemCompleted('\(correctedProgressJson)')") + } + } + + public func syncOfflineProgress() async { + let offlineProgress = persistence.loadAllOfflineProgress() + let cookies = HTTPCookieStorage.shared.cookies + HTTPCookieStorage.shared.cookies?.forEach { HTTPCookieStorage.shared.deleteCookie($0) } + for progress in offlineProgress { + do { + if try await interactor.submitOfflineProgress( + courseID: progress.courseID, + blockID: progress.blockID, + data: progress.data + ) { + persistence.deleteProgress(for: progress.blockID) + } + if let config = Container.shared.resolve(ConfigProtocol.self), let cookies { + HTTPCookieStorage.shared.setCookies(cookies, for: config.baseURL, mainDocumentURL: nil) + } + } catch { + debugLog("Error submitting offline progress: \(error.localizedDescription)") + } + } + } +} diff --git a/Core/Core/Network/RequestInterceptor.swift b/Core/Core/Network/RequestInterceptor.swift index f27b6f310..860fa070d 100644 --- a/Core/Core/Network/RequestInterceptor.swift +++ b/Core/Core/Network/RequestInterceptor.swift @@ -93,7 +93,11 @@ final public class RequestInterceptor: Alamofire.RequestInterceptor { } self.requestsToRetry.removeAll() } else { - NotificationCenter.default.post(name: .onTokenRefreshFailed, object: nil) + NotificationCenter.default.post( + name: .userLoggedOut, + object: nil, + userInfo: [Notification.UserInfoKey.isForced: true] + ) } } } diff --git a/Core/Core/Network/UploadBodyEncoding.swift b/Core/Core/Network/UploadBodyEncoding.swift deleted file mode 100644 index bfbc808cd..000000000 --- a/Core/Core/Network/UploadBodyEncoding.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// UploadBodyEncoding.swift -// Core -// -// Created by  Stepanok Ivan on 29.11.2022. -// - -import Foundation -import Alamofire - -public struct UploadBodyEncoding: ParameterEncoding { - - private var body: Data - - public init(body: Data) { - self.body = body - } - - public func encode( - _ urlRequest: Alamofire.URLRequestConvertible, - with parameters: Alamofire.Parameters? - ) throws -> URLRequest { - var urlRequest = try urlRequest.asURLRequest() - - guard let url = urlRequest.url else { - throw AFError.parameterEncodingFailed(reason: .missingURL) - } - - if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) { - let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") - urlComponents.percentEncodedQuery = percentEncodedQuery - urlRequest.url = urlComponents.url - } - - if urlRequest.headers["Content-Type"] == nil { - urlRequest.headers.update(.contentType("image/jpeg")) - } - - urlRequest.httpBody = body - - return urlRequest - } - -} diff --git a/Core/Core/SwiftGen/Assets.swift b/Core/Core/SwiftGen/Assets.swift index e25f452d0..48ebcc9cc 100644 --- a/Core/Core/SwiftGen/Assets.swift +++ b/Core/Core/SwiftGen/Assets.swift @@ -24,6 +24,10 @@ public typealias AssetImageTypeAlias = ImageAsset.Image // swiftlint:disable identifier_name line_length nesting type_body_length type_name public enum CoreAssets { + public static let calendarAccess = ImageAsset(name: "calendarAccess") + public static let syncFailed = ImageAsset(name: "syncFailed") + public static let syncOffline = ImageAsset(name: "syncOffline") + public static let synced = ImageAsset(name: "synced") public static let appleButtonColor = ColorAsset(name: "AppleButtonColor") public static let facebookButtonColor = ColorAsset(name: "FacebookButtonColor") public static let googleButtonColor = ColorAsset(name: "GoogleButtonColor") @@ -40,6 +44,7 @@ public enum CoreAssets { public static let downloads = ImageAsset(name: "downloads") public static let home = ImageAsset(name: "home") public static let more = ImageAsset(name: "more") + public static let noVideos = ImageAsset(name: "noVideos") public static let videos = ImageAsset(name: "videos") public static let dashboardEmptyPage = ImageAsset(name: "DashboardEmptyPage") public static let addComment = ImageAsset(name: "addComment") @@ -68,13 +73,17 @@ public enum CoreAssets { public static let stopDownloading = ImageAsset(name: "stopDownloading") public static let announcements = ImageAsset(name: "announcements") public static let handouts = ImageAsset(name: "handouts") + public static let noAnnouncements = ImageAsset(name: "noAnnouncements") + public static let noHandouts = ImageAsset(name: "noHandouts") public static let dashboard = ImageAsset(name: "dashboard") public static let discovery = ImageAsset(name: "discovery") + public static let learn = ImageAsset(name: "learn") public static let profile = ImageAsset(name: "profile") public static let programs = ImageAsset(name: "programs") public static let addPhoto = ImageAsset(name: "addPhoto") public static let bgDelete = ImageAsset(name: "bg_delete") public static let checkmark = ImageAsset(name: "checkmark") + public static let deleteAccount = ImageAsset(name: "deleteAccount") public static let deleteChar = ImageAsset(name: "delete_char") public static let deleteEyes = ImageAsset(name: "delete_eyes") public static let done = ImageAsset(name: "done") @@ -92,14 +101,21 @@ public enum CoreAssets { public static let alarm = ImageAsset(name: "alarm") public static let arrowLeft = ImageAsset(name: "arrowLeft") public static let arrowRight16 = ImageAsset(name: "arrowRight16") + public static let calendarSyncIcon = ImageAsset(name: "calendarSyncIcon") public static let certificate = ImageAsset(name: "certificate") public static let certificateBadge = ImageAsset(name: "certificateBadge") public static let check = ImageAsset(name: "check") public static let checkEmail = ImageAsset(name: "checkEmail") + public static let checkCircle = ImageAsset(name: "check_circle") + public static let chevronRight = ImageAsset(name: "chevron_right") public static let clearInput = ImageAsset(name: "clearInput") + public static let download = ImageAsset(name: "download") public static let edit = ImageAsset(name: "edit") public static let favorite = ImageAsset(name: "favorite") + public static let finishedSequence = ImageAsset(name: "finished_sequence") public static let goodWork = ImageAsset(name: "goodWork") + public static let information = ImageAsset(name: "information") + public static let learnEmpty = ImageAsset(name: "learn_empty") public static let airmail = ImageAsset(name: "airmail") public static let defaultMail = ImageAsset(name: "defaultMail") public static let fastmail = ImageAsset(name: "fastmail") @@ -113,8 +129,14 @@ public enum CoreAssets { public static let noWifiMini = ImageAsset(name: "noWifiMini") public static let notAvaliable = ImageAsset(name: "notAvaliable") public static let playVideo = ImageAsset(name: "playVideo") + public static let remove = ImageAsset(name: "remove") + public static let reportOctagon = ImageAsset(name: "report_octagon") + public static let resumeCourse = ImageAsset(name: "resumeCourse") + public static let settings = ImageAsset(name: "settings") public static let star = ImageAsset(name: "star") public static let starOutline = ImageAsset(name: "star_outline") + public static let viewAll = ImageAsset(name: "viewAll") + public static let visibility = ImageAsset(name: "visibility") public static let warning = ImageAsset(name: "warning") public static let warningFilled = ImageAsset(name: "warning_filled") } diff --git a/Core/Core/SwiftGen/Strings.swift b/Core/Core/SwiftGen/Strings.swift index a5782d497..672300224 100644 --- a/Core/Core/SwiftGen/Strings.swift +++ b/Core/Core/SwiftGen/Strings.swift @@ -10,18 +10,22 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum CoreLocalization { + /// Back + public static let back = CoreLocalization.tr("Localizable", "BACK", fallback: "Back") + /// Close + public static let close = CoreLocalization.tr("Localizable", "CLOSE", fallback: "Close") /// Done public static let done = CoreLocalization.tr("Localizable", "DONE", fallback: "Done") + /// Ok + public static let ok = CoreLocalization.tr("Localizable", "OK", fallback: "Ok") /// View in Safari public static let openInBrowser = CoreLocalization.tr("Localizable", "OPEN_IN_BROWSER", fallback: "View in Safari") - /// Register - public static let register = CoreLocalization.tr("Localizable", "REGISTER", fallback: "Register") /// The user canceled the sign-in flow. public static let socialSignCanceled = CoreLocalization.tr("Localizable", "SOCIAL_SIGN_CANCELED", fallback: "The user canceled the sign-in flow.") /// Tomorrow public static let tomorrow = CoreLocalization.tr("Localizable", "TOMORROW", fallback: "Tomorrow") /// View - public static let view = CoreLocalization.tr("Localizable", "VIEW ", fallback: "View") + public static let view = CoreLocalization.tr("Localizable", "VIEW", fallback: "View") /// Yesterday public static let yesterday = CoreLocalization.tr("Localizable", "YESTERDAY", fallback: "Yesterday") public enum Alert { @@ -78,21 +82,99 @@ public enum CoreLocalization { return CoreLocalization.tr("Localizable", "COURSEWARE.SECTION_COMPLETED", String(describing: p1), fallback: "You've completed “%@”.") } } + public enum CourseDates { + /// Completed + public static let completed = CoreLocalization.tr("Localizable", "COURSE_DATES.COMPLETED", fallback: "Completed") + /// Next week + public static let nextWeek = CoreLocalization.tr("Localizable", "COURSE_DATES.NEXT_WEEK", fallback: "Next week") + /// Past due + public static let pastDue = CoreLocalization.tr("Localizable", "COURSE_DATES.PAST_DUE", fallback: "Past due") + /// This week + public static let thisWeek = CoreLocalization.tr("Localizable", "COURSE_DATES.THIS_WEEK", fallback: "This week") + /// Today + public static let today = CoreLocalization.tr("Localizable", "COURSE_DATES.TODAY", fallback: "Today") + /// Upcoming + public static let upcoming = CoreLocalization.tr("Localizable", "COURSE_DATES.UPCOMING", fallback: "Upcoming") + public enum ResetDate { + /// Your dates could not be shifted. Please try again. + public static let errorMessage = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.ERROR_MESSAGE", fallback: "Your dates could not be shifted. Please try again.") + /// Your dates have been successfully shifted. + public static let successMessage = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.SUCCESS_MESSAGE", fallback: "Your dates have been successfully shifted.") + /// Course Dates + public static let title = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.TITLE", fallback: "Course Dates") + public enum ResetDateBanner { + /// Don't worry - shift our suggested schedule to complete past due assignments without losing any progress. + public static let body = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.BODY", fallback: "Don't worry - shift our suggested schedule to complete past due assignments without losing any progress.") + /// Shift due dates + public static let button = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.BUTTON", fallback: "Shift due dates") + /// Missed some deadlines? + public static let header = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.HEADER", fallback: "Missed some deadlines?") + } + public enum TabInfoBanner { + /// We built a suggested schedule to help you stay on track. But don’t worry – it’s flexible so you can learn at your own pace. If you happen to fall behind, you’ll be able to adjust the dates to keep yourself on track. + public static let body = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.TAB_INFO_BANNER.BODY", fallback: "We built a suggested schedule to help you stay on track. But don’t worry – it’s flexible so you can learn at your own pace. If you happen to fall behind, you’ll be able to adjust the dates to keep yourself on track.") + /// + public static let header = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.TAB_INFO_BANNER.HEADER", fallback: "") + } + public enum UpgradeToCompleteGradedBanner { + /// To complete graded assignments as part of this course, you can upgrade today. + public static let body = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.BODY", fallback: "To complete graded assignments as part of this course, you can upgrade today.") + /// + public static let button = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.BUTTON", fallback: "") + /// + public static let header = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.HEADER", fallback: "") + } + public enum UpgradeToResetBanner { + /// You are auditing this course, which means that you are unable to participate in graded assignments. It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today. + public static let body = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.BODY", fallback: "You are auditing this course, which means that you are unable to participate in graded assignments. It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today.") + /// + public static let button = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.BUTTON", fallback: "") + /// + public static let header = CoreLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.HEADER", fallback: "") + } + } + } public enum Date { + /// Course Ended + public static let courseEnded = CoreLocalization.tr("Localizable", "DATE.COURSE_ENDED", fallback: "Course Ended") + /// Course Ends + public static let courseEnds = CoreLocalization.tr("Localizable", "DATE.COURSE_ENDS", fallback: "Course Ends") + /// Course Starts + public static let courseStarts = CoreLocalization.tr("Localizable", "DATE.COURSE_STARTS", fallback: "Course Starts") + /// %@ Days Ago + public static func daysAgo(_ p1: Any) -> String { + return CoreLocalization.tr("Localizable", "DATE.DAYS_AGO", String(describing: p1), fallback: "%@ Days Ago") + } + /// Due + public static let due = CoreLocalization.tr("Localizable", "DATE.DUE", fallback: "Due ") + /// Due in + public static let dueIn = CoreLocalization.tr("Localizable", "DATE.DUE_IN", fallback: "Due in ") + /// Due in %@ Days + public static func dueInDays(_ p1: Any) -> String { + return CoreLocalization.tr("Localizable", "DATE.DUE_IN_DAYS", String(describing: p1), fallback: "Due in %@ Days") + } /// Ended public static let ended = CoreLocalization.tr("Localizable", "DATE.ENDED", fallback: "Ended") /// Just now public static let justNow = CoreLocalization.tr("Localizable", "DATE.JUST_NOW", fallback: "Just now") + /// Next %@ + public static func next(_ p1: Any) -> String { + return CoreLocalization.tr("Localizable", "DATE.NEXT", String(describing: p1), fallback: "Next %@") + } /// Start public static let start = CoreLocalization.tr("Localizable", "DATE.START", fallback: "Start") /// Started public static let started = CoreLocalization.tr("Localizable", "DATE.STARTED", fallback: "Started") + /// Today + public static let today = CoreLocalization.tr("Localizable", "DATE.TODAY", fallback: "Today") } public enum DateFormat { /// MMM dd, yyyy public static let mmmDdYyyy = CoreLocalization.tr("Localizable", "DATE_FORMAT.MMM_DD_YYYY", fallback: "MMM dd, yyyy") /// MMMM dd public static let mmmmDd = CoreLocalization.tr("Localizable", "DATE_FORMAT.MMMM_DD", fallback: "MMMM dd") + /// MMMM dd, yyyy + public static let mmmmDdYyyy = CoreLocalization.tr("Localizable", "DATE_FORMAT.MMMM_DD_YYYY", fallback: "MMMM dd, yyyy") } public enum DownloadManager { /// Completed @@ -136,6 +218,8 @@ public enum CoreLocalization { public static let discovery = CoreLocalization.tr("Localizable", "MAINSCREEN.DISCOVERY", fallback: "Discover") /// In developing public static let inDeveloping = CoreLocalization.tr("Localizable", "MAINSCREEN.IN_DEVELOPING", fallback: "In developing") + /// Learn + public static let learn = CoreLocalization.tr("Localizable", "MAINSCREEN.LEARN", fallback: "Learn") /// Profile public static let profile = CoreLocalization.tr("Localizable", "MAINSCREEN.PROFILE", fallback: "Profile") /// Programs @@ -210,6 +294,10 @@ public enum CoreLocalization { public enum SignIn { /// Sign in public static let logInBtn = CoreLocalization.tr("Localizable", "SIGN_IN.LOG_IN_BTN", fallback: "Sign in") + /// Sign in with SSO + public static let logInWithSsoBtn = CoreLocalization.tr("Localizable", "SIGN_IN.LOG_IN_WITH_SSO_BTN", fallback: "Sign in with SSO") + /// Register + public static let registerBtn = CoreLocalization.tr("Localizable", "SIGN_IN.REGISTER_BTN", fallback: "Register") } public enum View { public enum Snackbar { diff --git a/Core/Core/View/Base/AlertView.swift b/Core/Core/View/Base/AlertView.swift index 388c70c54..6b754e564 100644 --- a/Core/Core/View/Base/AlertView.swift +++ b/Core/Core/View/Base/AlertView.swift @@ -109,15 +109,8 @@ public struct AlertView: View { .fixedSize(horizontal: false, vertical: false) ) .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke( - style: .init( - lineWidth: 1, - lineCap: .round, - lineJoin: .round, - miterLimit: 1 - ) - ) + Theme.Shapes.buttonShape + .stroke(lineWidth: 1) .foregroundColor(Theme.Colors.backgroundStroke) .fixedSize(horizontal: false, vertical: false) ) @@ -256,7 +249,7 @@ public struct AlertView: View { .frame(maxWidth: 215) } UnitButtonView(type: .custom(action), - bgColor: .clear, + bgColor: Theme.Colors.secondaryButtonBGColor, action: { okTapped() }) .frame(maxWidth: 215) @@ -417,7 +410,7 @@ public struct AlertView: View { } label: { ZStack { Text(primaryButtonTitle) - .foregroundColor(Theme.Colors.primaryButtonTextColor) + .foregroundColor(Theme.Colors.styledButtonText) .font(Theme.Fonts.labelLarge) .frame(maxWidth: .infinity) .padding(.horizontal, 16) @@ -426,7 +419,7 @@ public struct AlertView: View { } .background( Theme.Shapes.buttonShape - .fill(Theme.Colors.accentColor) + .fill(Theme.Colors.accentButtonColor) ) .overlay( RoundedRectangle(cornerRadius: 8) @@ -454,7 +447,7 @@ public struct AlertView: View { }) .background( Theme.Shapes.buttonShape - .fill(.clear) + .fill(Theme.Colors.secondaryButtonBGColor) ) .overlay( RoundedRectangle(cornerRadius: 8) @@ -488,7 +481,7 @@ struct AlertView_Previews: PreviewProvider { .previewLayout(.sizeThatFits) .background(Color.gray) - AlertView(alertTitle: "Comfirm log out", + AlertView(alertTitle: "Confirm log out", alertMessage: "Are you sure you want to log out?", positiveAction: "Yes", onCloseTapped: {}, diff --git a/Core/Core/View/Base/AppReview/AppReviewView.swift b/Core/Core/View/Base/AppReview/AppReviewView.swift index 176c9707a..efe5df5bd 100644 --- a/Core/Core/View/Base/AppReview/AppReviewView.swift +++ b/Core/Core/View/Base/AppReview/AppReviewView.swift @@ -13,8 +13,8 @@ public struct AppReviewView: View { @ObservedObject private var viewModel: AppReviewViewModel - @Environment (\.isHorizontal) private var isHorizontal - @Environment (\.presentationMode) private var presentationMode + @Environment(\.isHorizontal) private var isHorizontal + @Environment(\.presentationMode) private var presentationMode public init(viewModel: AppReviewViewModel) { self.viewModel = viewModel @@ -77,7 +77,7 @@ public struct AppReviewView: View { .foregroundColor(Theme.Colors.textPrimary) .padding(.horizontal, 12) .padding(.vertical, 4) - .hideScrollContentBackground() + .scrollContentBackground(.hidden) .background( Theme.Shapes.textInputShape .fill(Theme.Colors.commentCellBackground) diff --git a/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift b/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift index 76d48270e..5fdf14d34 100644 --- a/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift +++ b/Core/Core/View/Base/AppReview/ThirdPartyMailer/ThirdPartyMailClient.swift @@ -5,6 +5,8 @@ // // Licensed under MIT License +// swiftlint:disable all + import SwiftUI /// A third-party mail client, offering a custom URL scheme. @@ -145,3 +147,4 @@ public extension ThirdPartyMailClient { } } } +// swiftlint:enable all diff --git a/Core/Core/View/Base/BackNavigationButton.swift b/Core/Core/View/Base/BackNavigationButton.swift index 001f5d340..415433cd1 100644 --- a/Core/Core/View/Base/BackNavigationButton.swift +++ b/Core/Core/View/Base/BackNavigationButton.swift @@ -78,6 +78,8 @@ public struct BackNavigationButton: View { public var body: some View { BackNavigationButtonRepresentable(action: action, color: color, viewModel: viewModel) + .accessibilityIdentifier("back_button") + .accessibilityLabel(CoreLocalization.back) .onAppear { viewModel.loadItems() } diff --git a/Core/Core/View/Base/BackNavigationButtonViewModel.swift b/Core/Core/View/Base/BackNavigationButtonViewModel.swift index 00c4b5d76..59dfe97a5 100644 --- a/Core/Core/View/Base/BackNavigationButtonViewModel.swift +++ b/Core/Core/View/Base/BackNavigationButtonViewModel.swift @@ -7,6 +7,7 @@ import Swinject import UIKit +import OEXFoundation public protocol BackNavigationProtocol { func getBackMenuItems() -> [BackNavigationMenuItem] diff --git a/Core/Core/View/Base/CalendarManagerProtocol.swift b/Core/Core/View/Base/CalendarManagerProtocol.swift new file mode 100644 index 000000000..152c0dc21 --- /dev/null +++ b/Core/Core/View/Base/CalendarManagerProtocol.swift @@ -0,0 +1,37 @@ +// +// CalendarManagerProtocol.swift +// Core +// +// Created by  Stepanok Ivan on 12.06.2024. +// + +import Foundation + +//sourcery: AutoMockable +public protocol CalendarManagerProtocol { + func createCalendarIfNeeded() + func filterCoursesBySelected(fetchedCourses: [CourseForSync]) async -> [CourseForSync] + func removeOldCalendar() + func removeOutdatedEvents(courseID: String) async + func syncCourse(courseID: String, courseName: String, dates: CourseDates) async + func requestAccess() async -> Bool + func courseStatus(courseID: String) -> SyncStatus + func clearAllData(removeCalendar: Bool) + func isDatesChanged(courseID: String, checksum: String) -> Bool +} + +#if DEBUG +public struct CalendarManagerMock: CalendarManagerProtocol { + public func createCalendarIfNeeded() {} + public func filterCoursesBySelected(fetchedCourses: [CourseForSync]) async -> [CourseForSync] {[]} + public func removeOldCalendar() {} + public func removeOutdatedEvents(courseID: String) async {} + public func syncCourse(courseID: String, courseName: String, dates: CourseDates) async {} + public func requestAccess() async -> Bool { true } + public func courseStatus(courseID: String) -> SyncStatus { .synced } + public func clearAllData(removeCalendar: Bool) {} + public func isDatesChanged(courseID: String, checksum: String) -> Bool {false} + + public init() {} +} +#endif diff --git a/Core/Core/View/Base/CheckBoxView.swift b/Core/Core/View/Base/CheckBoxView.swift index efed96ccc..267d21463 100644 --- a/Core/Core/View/Base/CheckBoxView.swift +++ b/Core/Core/View/Base/CheckBoxView.swift @@ -13,11 +13,18 @@ public struct CheckBoxView: View { @Binding private var checked: Bool private var text: String private var font: Font + private let color: Color - public init(checked: Binding, text: String, font: Font = Theme.Fonts.labelLarge) { + public init( + checked: Binding, + text: String, + font: Font = Theme.Fonts.labelLarge, + color: Color = Theme.Colors.textPrimary + ) { self._checked = checked self.text = text self.font = font + self.color = color } public var body: some View { @@ -26,11 +33,11 @@ public struct CheckBoxView: View { systemName: checked ? "checkmark.square.fill" : "square" ) .foregroundColor( - checked ? Theme.Colors.accentXColor : Theme.Colors.textPrimary + checked ? Theme.Colors.accentXColor : color ) Text(text) .font(font) - .foregroundColor(Theme.Colors.textPrimary) + .foregroundColor(color) } .onTapGesture { withAnimation(.linear(duration: 0.1)) { diff --git a/Core/Core/View/Base/CourseButton.swift b/Core/Core/View/Base/CourseButton.swift index 9ff48554a..cfc999d5b 100644 --- a/Core/Core/View/Base/CourseButton.swift +++ b/Core/Core/View/Base/CourseButton.swift @@ -40,6 +40,7 @@ public struct CourseButton: View { .foregroundColor(Theme.Colors.textPrimary) Spacer() Image(systemName: "chevron.right") + .flipsForRightToLeftLayoutDirection(true) .padding(.vertical, 8) .foregroundColor(Theme.Colors.accentXColor) } diff --git a/Core/Core/View/Base/CourseCellView.swift b/Core/Core/View/Base/CourseCellView.swift index 6166b81a3..37dada165 100644 --- a/Core/Core/View/Base/CourseCellView.swift +++ b/Core/Core/View/Base/CourseCellView.swift @@ -27,12 +27,12 @@ public struct CourseCellView: View { private var cellsCount: Int private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - public init(model: CourseItem, type: CellType, index: Int, cellsCount: Int) { + public init(model: CourseItem, type: CellType, index: Int, cellsCount: Int, useRelativeDates: Bool) { self.type = type self.courseImage = model.imageURL self.courseName = model.name - self.courseStart = model.courseStart?.dateToString(style: .startDDMonthYear) ?? "" - self.courseEnd = model.courseEnd?.dateToString(style: .endedMonthDay) ?? "" + self.courseStart = model.courseStart?.dateToString(style: .startDDMonthYear, useRelativeDates: useRelativeDates) ?? "" + self.courseEnd = model.courseEnd?.dateToString(style: .endedMonthDay, useRelativeDates: useRelativeDates) ?? "" self.courseOrg = model.org self.index = Double(index) + 1 self.cellsCount = cellsCount @@ -116,7 +116,7 @@ public struct CourseCellView: View { .overlay(Theme.Colors.cardViewStroke) .padding(.vertical, 18) .padding(.horizontal, 3) - .accessibilityIdentifier("devider") + .accessibilityIdentifier("divider") } } } @@ -130,14 +130,18 @@ struct CourseCellView_Previews: PreviewProvider { org: "Edx", shortDescription: "", imageURL: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", - isActive: true, + hasAccess: true, courseStart: Date(iso8601: "2032-05-26T12:13:14Z"), courseEnd: Date(iso8601: "2033-05-26T12:13:14Z"), enrollmentStart: nil, enrollmentEnd: nil, courseID: "1", numPages: 1, - coursesCount: 10) + coursesCount: 10, + courseRawImage: nil, + progressEarned: 4, + progressPossible: 10 + ) static var previews: some View { ZStack { @@ -145,10 +149,10 @@ struct CourseCellView_Previews: PreviewProvider { .ignoresSafeArea() VStack(spacing: 0) { // Divider() - CourseCellView(model: course, type: .discovery, index: 1, cellsCount: 3) + CourseCellView(model: course, type: .discovery, index: 1, cellsCount: 3, useRelativeDates: true) .previewLayout(.fixed(width: 180, height: 260)) // Divider() - CourseCellView(model: course, type: .discovery, index: 2, cellsCount: 3) + CourseCellView(model: course, type: .discovery, index: 2, cellsCount: 3, useRelativeDates: false) .previewLayout(.fixed(width: 180, height: 260)) // Divider() } diff --git a/Core/Core/View/Base/CustomDisclosureGroup.swift b/Core/Core/View/Base/CustomDisclosureGroup.swift deleted file mode 100644 index c4a023ed3..000000000 --- a/Core/Core/View/Base/CustomDisclosureGroup.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// CustomDisclosureGroup.swift -// Core -// -// Created by Eugene Yatsenko on 09.11.2023. -// - -import SwiftUI - -public struct CustomDisclosureGroup: View { - - @Binding var isExpanded: Bool - - private var onClick: () -> Void - private var animation: Animation? - private let header: Header - private let content: Content - - public init( - animation: Animation?, - isExpanded: Binding, - onClick: @escaping () -> Void, - header: (_ isExpanded: Bool) -> Header, - content: () -> Content - ) { - self.onClick = onClick - self._isExpanded = isExpanded - self.animation = animation - self.header = header(isExpanded.wrappedValue) - self.content = content() - } - - public var body: some View { - VStack(spacing: 0) { - Button { - withAnimation(animation) { - onClick() - } - } label: { - header - .contentShape(Rectangle()) - } - if isExpanded { - content - } - } - .clipped() - } -} diff --git a/Core/Core/View/Base/DownloadView.swift b/Core/Core/View/Base/DownloadView.swift index 37f63e41d..ede7c2912 100644 --- a/Core/Core/View/Base/DownloadView.swift +++ b/Core/Core/View/Base/DownloadView.swift @@ -24,7 +24,6 @@ public struct DownloadAvailableView: View { .resizable() .scaledToFit() .frame(width: 24, height: 24) - .foregroundColor(Theme.Colors.textPrimary) } .frame(width: 30, height: 30) } @@ -41,6 +40,7 @@ public struct DownloadProgressView: View { .resizable() .scaledToFit() .frame(width: 20, height: 20) + .foregroundStyle(Theme.Colors.snackbarErrorColor) .foregroundColor(Theme.Colors.textPrimary) } } @@ -52,11 +52,10 @@ public struct DownloadFinishedView: View { public var body: some View { VStack(spacing: 0) { - CoreAssets.deleteDownloading.swiftUIImage.renderingMode(.template) + CoreAssets.deleteDownloading.swiftUIImage .resizable() .scaledToFit() .frame(width: 24, height: 24) - .foregroundColor(Theme.Colors.textPrimary) } .frame(width: 30, height: 30) } diff --git a/Core/Core/View/Base/DynamicOffsetView.swift b/Core/Core/View/Base/DynamicOffsetView.swift index 59bedd83d..1647af921 100644 --- a/Core/Core/View/Base/DynamicOffsetView.swift +++ b/Core/Core/View/Base/DynamicOffsetView.swift @@ -12,22 +12,32 @@ public struct DynamicOffsetView: View { private let padHeight: CGFloat = 290 private let collapsedHorizontalHeight: CGFloat = 120 private let collapsedVerticalHeight: CGFloat = 100 - private let expandedHeight: CGFloat = 240 + private var expandedHeight: CGFloat { + let topInset = UIApplication.shared.windowInsets.top + guard topInset > 0 else { + return 240 + } + return 300 - topInset + } private let coordinateBoundaryLower: CGFloat = -115 private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Binding private var coordinate: CGFloat @Binding private var collapsed: Bool + @Binding private var viewHeight: CGFloat @State private var collapseHeight: CGFloat = .zero @Environment(\.isHorizontal) private var isHorizontal + @State private var isOnTheScreen: Bool = false public init( coordinate: Binding, - collapsed: Binding + collapsed: Binding, + viewHeight: Binding ) { self._coordinate = coordinate self._collapsed = collapsed + self._viewHeight = viewHeight } public var body: some View { @@ -36,6 +46,9 @@ public struct DynamicOffsetView: View { .frame(height: collapseHeight) .overlay( GeometryReader { geometry -> Color in + if !isOnTheScreen { + return .clear + } guard idiom != .pad else { return .clear } @@ -50,28 +63,40 @@ public struct DynamicOffsetView: View { } ) .onAppear { - changeCollapsedHeight() + isOnTheScreen = true + changeCollapsedHeight(collapsed: collapsed, isHorizontal: isHorizontal) + } + .onDisappear { + isOnTheScreen = false } .onChange(of: collapsed) { collapsed in if !collapsed { - changeCollapsedHeight() + changeCollapsedHeight(collapsed: collapsed, isHorizontal: isHorizontal) } } .onChange(of: isHorizontal) { isHorizontal in if isHorizontal { collapsed = true } - changeCollapsedHeight() + changeCollapsedHeight(collapsed: collapsed, isHorizontal: isHorizontal) } } - private func changeCollapsedHeight() { - collapseHeight = idiom == .pad - ? padHeight - : ( - collapsed - ? (isHorizontal ? collapsedHorizontalHeight : collapsedVerticalHeight) - : expandedHeight - ) + private func changeCollapsedHeight( + collapsed: Bool, + isHorizontal: Bool + ) { + if idiom == .pad { + collapseHeight = padHeight + } else if collapsed { + if isHorizontal { + collapseHeight = collapsedHorizontalHeight + } else { + collapseHeight = collapsedVerticalHeight + } + } else { + collapseHeight = expandedHeight + } + viewHeight = collapseHeight } } diff --git a/Core/Core/View/Base/ErrorAlertView.swift b/Core/Core/View/Base/ErrorAlertView.swift new file mode 100644 index 000000000..5256f0f7f --- /dev/null +++ b/Core/Core/View/Base/ErrorAlertView.swift @@ -0,0 +1,35 @@ +// +// ErrorAlertView.swift +// Core +// +// Created by  Stepanok Ivan on 29.05.2024. +// + +import SwiftUI +import Theme + +public struct ErrorAlertView: View { + + @Binding var errorMessage: String? + + public init(errorMessage: Binding) { + self._errorMessage = errorMessage + } + + public var body: some View { + VStack { + Spacer() + SnackBarView(message: errorMessage) + .transition(.move(edge: .bottom)) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + Theme.Timeout.snackbarMessageLongTimeout) { + errorMessage = nil + } + } + } + } +} + +#Preview { + ErrorAlertView(errorMessage: .constant("Error message")) +} diff --git a/Core/Core/View/Base/FileWebView.swift b/Core/Core/View/Base/FileWebView.swift new file mode 100644 index 000000000..b6b98f4e5 --- /dev/null +++ b/Core/Core/View/Base/FileWebView.swift @@ -0,0 +1,65 @@ +// +// FileWebView.swift +// Core +// +// Created by  Stepanok Ivan on 08.07.2024. +// + +import Foundation +import WebKit +import SwiftUI + +public struct FileWebView: UIViewRepresentable { + public func makeUIView(context: Context) -> WKWebView { + let webview = WKWebView() + webview.scrollView.bounces = false + webview.scrollView.alwaysBounceHorizontal = false + webview.scrollView.showsHorizontalScrollIndicator = false + webview.scrollView.isScrollEnabled = true + webview.configuration.suppressesIncrementalRendering = true + webview.isOpaque = false + webview.configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs") + webview.configuration.defaultWebpagePreferences.allowsContentJavaScript = true + webview.backgroundColor = .clear + webview.scrollView.backgroundColor = UIColor.white + webview.scrollView.alwaysBounceVertical = false + webview.scrollView.layer.cornerRadius = 24 + webview.scrollView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + if let url = URL(string: viewModel.url) { + + if let fileURL = URL(string: url.absoluteString) { + let fileAccessURL = fileURL.deletingLastPathComponent() + if let pdfData = try? Data(contentsOf: url) { + webview.load( + pdfData, + mimeType: "application/pdf", + characterEncodingName: "", + baseURL: fileAccessURL + ) + } + } + } + + return webview + } + + public func updateUIView(_ webview: WKWebView, context: Context) { + + } + + public class ViewModel: ObservableObject { + + @Published var url: String + + public init(url: String) { + self.url = url + } + } + + @ObservedObject var viewModel: ViewModel + + public init(viewModel: ViewModel) { + self.viewModel = viewModel + } +} diff --git a/Core/Core/View/Base/FlexibleKeyboardInputView.swift b/Core/Core/View/Base/FlexibleKeyboardInputView.swift index 48a1119f5..fdf47efdc 100644 --- a/Core/Core/View/Base/FlexibleKeyboardInputView.swift +++ b/Core/Core/View/Base/FlexibleKeyboardInputView.swift @@ -12,7 +12,7 @@ public struct FlexibleKeyboardInputView: View { @State private var commentText: String = "" @State private var commentSize: CGFloat = .init(64) - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal public var sendText: ((String) -> Void) private let hint: String @@ -54,7 +54,7 @@ public struct FlexibleKeyboardInputView: View { TextEditor(text: $commentText) .padding(.horizontal, 8) .foregroundColor(Theme.Colors.textInputTextColor) - .hideScrollContentBackground() + .scrollContentBackground(.hidden) .frame(maxHeight: commentSize) .background( Theme.InputFieldBackground( diff --git a/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift b/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift new file mode 100644 index 000000000..4e17c0626 --- /dev/null +++ b/Core/Core/View/Base/FullScreenErrorView/FullScreenErrorView.swift @@ -0,0 +1,110 @@ +// +// FullScreenErrorView.swift +// Course +// +// Created by Shafqat Muneer on 5/14/24. +// + +import SwiftUI +import Theme + +public struct FullScreenErrorView: View { + + public enum ErrorType: Equatable { + case noInternet + case noInternetWithReload + case generic + case noContent(_ message: String, image: SwiftUI.Image) + } + + private let errorType: ErrorType + private var action: () -> Void = {} + + public init( + type: ErrorType + ) { + self.errorType = type + } + + public init( + type: ErrorType, + action: @escaping () -> Void + ) { + self.errorType = type + self.action = action + } + + public var body: some View { + VStack(spacing: 20) { + Spacer() + switch errorType { + case .noContent(let message, image: let image): + image + .resizable() + .renderingMode(.template) + .foregroundStyle(Theme.Colors.textSecondary) + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 72, maxHeight: 80) + + Text(message) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) + case .noInternet, + .noInternetWithReload: + CoreAssets.noWifi.swiftUIImage + .renderingMode(.template) + .foregroundStyle(Theme.Colors.textSecondary) + .scaledToFit() + + Text(CoreLocalization.Error.Internet.noInternetTitle) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + + Text(CoreLocalization.Error.Internet.noInternetDescription) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) + case .generic: + CoreAssets.notAvaliable.swiftUIImage + .renderingMode(.template) + .foregroundStyle(Theme.Colors.textSecondary) + .scaledToFit() + + Text(CoreLocalization.View.Snackbar.tryAgainBtn) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + + Text(CoreLocalization.Error.unknownError) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .padding(.horizontal, 50) + + } + if errorType == .noInternetWithReload || errorType == .generic { + UnitButtonView( + type: .reload, + action: { + self.action() + } + ) + } + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background( + Theme.Colors.background + ) + } +} + +#if DEBUG +struct FullScreenErrorView_Previews: PreviewProvider { + static var previews: some View { + FullScreenErrorView(type: .noInternetWithReload) + } +} +#endif diff --git a/Core/Core/View/Base/LogistrationBottomView.swift b/Core/Core/View/Base/LogistrationBottomView.swift index fca95cc04..faa3aaa35 100644 --- a/Core/Core/View/Base/LogistrationBottomView.swift +++ b/Core/Core/View/Base/LogistrationBottomView.swift @@ -15,14 +15,11 @@ public enum LogistrationSourceScreen: Equatable { case discovery case courseDetail(String, String) case programDetails(String) - - public var value: String? { - return String(describing: self).components(separatedBy: "(").first - } } public enum LogistrationAction { case signIn + case signInWithSSO case register } @@ -34,11 +31,11 @@ public struct LogistrationBottomView: View { public init(_ action: @escaping (LogistrationAction) -> Void) { self.action = action } - + public var body: some View { VStack(alignment: .leading) { HStack(spacing: 24) { - StyledButton(CoreLocalization.register) { + StyledButton(CoreLocalization.SignIn.registerBtn) { action(.register) } .accessibilityIdentifier("logistration_register_button") @@ -48,12 +45,24 @@ public struct LogistrationBottomView: View { action: { action(.signIn) }, - color: Theme.Colors.background, + color: Theme.Colors.secondaryButtonBGColor, textColor: Theme.Colors.secondaryButtonTextColor, borderColor: Theme.Colors.secondaryButtonBorderColor ) .frame(width: 100) .accessibilityIdentifier("logistration_signin_button") + + StyledButton( + CoreLocalization.SignIn.logInWithSsoBtn, + action: { + action(.signInWithSSO) + }, + color: Theme.Colors.white, + textColor: Theme.Colors.secondaryButtonTextColor, + borderColor: Theme.Colors.secondaryButtonBorderColor + ) + .frame(width: 100) + .accessibilityIdentifier("logistration_signin_withsso_button") } .padding(.horizontal, isHorizontal ? 0 : 0) } diff --git a/Core/Core/View/Base/NavigationBar.swift b/Core/Core/View/Base/NavigationBar.swift index 8815cf2b8..9377067c7 100644 --- a/Core/Core/View/Base/NavigationBar.swift +++ b/Core/Core/View/Base/NavigationBar.swift @@ -23,6 +23,7 @@ public struct NavigationBar: View { private let rightButtonType: ButtonType? private let rightButtonAction: (() -> Void)? @Binding private var rightButtonIsActive: Bool + @Environment(\.isHorizontal) private var isHorizontal public init(title: String, titleColor: Color = Theme.Colors.navigationBarTintColor, @@ -53,8 +54,9 @@ public struct NavigationBar: View { if leftButton { VStack { BackNavigationButton(color: leftButtonColor, action: leftButtonAction) - .padding(8) .backViewStyle() + .padding(.leading, isHorizontal ? 48 : 0) + .accessibilityIdentifier("back_button") } .frame(minWidth: 0, maxWidth: .infinity, diff --git a/Core/Core/View/Base/NavigationTitle.swift b/Core/Core/View/Base/NavigationTitle.swift new file mode 100644 index 000000000..7cae61193 --- /dev/null +++ b/Core/Core/View/Base/NavigationTitle.swift @@ -0,0 +1,52 @@ +// +// NavigationTitle.swift +// Core +// +// Created by  Stepanok Ivan on 29.05.2024. +// + +import SwiftUI +import Theme + +public struct NavigationTitle: View { + + private let title: String + private let backAction: () -> Void + + @Environment(\.isHorizontal) private var isHorizontal + + public init(title: String, backAction: @escaping () -> Void) { + self.title = title + self.backAction = backAction + } + + public var body: some View { + // MARK: - Navigation and Title + ZStack { + HStack { + Text(title) + .titleSettings(color: Theme.Colors.loginNavigationText) + .accessibilityIdentifier("\(title)_text") + } + VStack { + BackNavigationButton( + color: Theme.Colors.loginNavigationText, + action: { + backAction() + } + ) + .backViewStyle() + .foregroundColor(Theme.Colors.styledButtonText) + .padding(.leading, isHorizontal ? 48 : 0) + .accessibilityIdentifier("back_button") + + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + } + } +} + +#Preview { + NavigationTitle(title: "Title", backAction: {}) +} diff --git a/Core/Core/View/Base/OfflineSnackBarView.swift b/Core/Core/View/Base/OfflineSnackBarView.swift index 4928c6936..af1c9e778 100644 --- a/Core/Core/View/Base/OfflineSnackBarView.swift +++ b/Core/Core/View/Base/OfflineSnackBarView.swift @@ -29,23 +29,29 @@ public struct OfflineSnackBarView: View { HStack(spacing: 12) { Text(CoreLocalization.NoInternet.offline) .accessibilityIdentifier("no_internet_text") + .foregroundColor(Theme.Colors.snackbarTextColor) Spacer() - Button(CoreLocalization.NoInternet.dismiss, - action: { + Button(action: { withAnimation { dismiss = true } + }, label: { + Text(CoreLocalization.NoInternet.dismiss) + .foregroundColor(Theme.Colors.snackbarTextColor) }) .accessibilityIdentifier("no_internet_dismiss_button") - Button(CoreLocalization.NoInternet.reload, - action: { + Button(action: { Task { await reloadAction() } withAnimation { dismiss = true } - }) + }, label: { + Text(CoreLocalization.NoInternet.reload) + .foregroundColor(Theme.Colors.snackbarTextColor) + } + ) .accessibilityIdentifier("no_internet_reload_button") }.padding(.horizontal, 16) .font(Theme.Fonts.titleSmall) diff --git a/Core/Core/View/Base/PickerMenu.swift b/Core/Core/View/Base/PickerMenu.swift index 0de023381..c425191c0 100644 --- a/Core/Core/View/Base/PickerMenu.swift +++ b/Core/Core/View/Base/PickerMenu.swift @@ -26,13 +26,14 @@ public struct PickerMenu: View { @State private var search: String = "" @State public var selectedItem: PickerItem = PickerItem(key: "", value: "") - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal private let ipadPickerWidth: CGFloat = 300 private var items: [PickerItem] private let titleText: String private let router: BaseRouter private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } private var selected: ((PickerItem) -> Void) = { _ in } + private let emptyKey: String = "--empty--" public init( items: [PickerItem], @@ -50,18 +51,19 @@ public struct PickerMenu: View { private var filteredItems: [PickerItem] { if search.isEmpty { - return items + return items.isEmpty ? [PickerItem(key: emptyKey, value: "")] : items } else { - return items.filter { $0.value.localizedCaseInsensitiveContains(search) } + let filteredItems = items.filter { $0.value.localizedCaseInsensitiveContains(search) } + return filteredItems.isEmpty ? [PickerItem(key: emptyKey, value: "")] : filteredItems } } private var isSingleSelection: Bool { - return filteredItems.count == 1 + return filteredItems.count == 1 && filteredItems.first?.key != emptyKey } private var isItemSelected: Bool { - return filteredItems.contains(selectedItem) + return filteredItems.contains(selectedItem) && selectedItem.key != emptyKey } private var acceptButtonDisabled: Bool { diff --git a/Core/Core/View/Base/PickerView.swift b/Core/Core/View/Base/PickerView.swift index d0655ddb4..e0403b822 100644 --- a/Core/Core/View/Base/PickerView.swift +++ b/Core/Core/View/Base/PickerView.swift @@ -64,7 +64,7 @@ public struct PickerView: View { .stroke(lineWidth: 1) .fill(config.error == "" ? Theme.Colors.textInputStroke - : Theme.Colors.alert) + : Theme.Colors.irreversibleAlert) ) .shake($config.shake) Text(config.error == "" ? config.field.instructions @@ -72,7 +72,7 @@ public struct PickerView: View { .font(Theme.Fonts.labelMedium) .foregroundColor(config.error == "" ? Theme.Colors.textPrimary - : Theme.Colors.alert) + : Theme.Colors.irreversibleAlert) .accessibilityIdentifier("\(config.field.name)_instructions_text") } } diff --git a/Core/Core/View/Base/ProgressBar.swift b/Core/Core/View/Base/ProgressBar.swift index 7bc6e5195..b3be390e7 100644 --- a/Core/Core/View/Base/ProgressBar.swift +++ b/Core/Core/View/Base/ProgressBar.swift @@ -40,17 +40,18 @@ public struct ProgressBar: View { Circle() .stroke(lineWidth: lineWidth) .foregroundColor(Theme.Colors.accentColor.opacity(0.3)) - .frame(width: size, height: size) Circle() .trim(from: 0.0, to: 0.7) .stroke(gradient, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) - .frame(width: size, height: size) - .rotationEffect(Angle.degrees(isAnimating ? 360 : 0), anchor: .center) - .animation(animation, value: isAnimating) } + .frame(width: size, height: size) + .rotationEffect(Angle.degrees(isAnimating ? 360 : 0), anchor: .center) + .animation(animation, value: isAnimating) .onAppear { - isAnimating = true + DispatchQueue.main.async { + isAnimating = true + } } } } diff --git a/Core/Core/View/Base/RefreshableScrollView.swift b/Core/Core/View/Base/RefreshableScrollView.swift deleted file mode 100644 index 0905bdba6..000000000 --- a/Core/Core/View/Base/RefreshableScrollView.swift +++ /dev/null @@ -1,342 +0,0 @@ -// -// RefreshableScrollView.swift -// Core -// -// Created by  Stepanok Ivan on 14.02.2023. -// - -import SwiftUI - -// There are two type of positioning views - one that scrolls with the content, -// and one that stays fixed -private enum PositionType { - case fixed, moving -} - -// This struct is the currency of the Preferences, and has a type -// (fixed or moving) and the actual Y-axis value. -// It's Equatable because Swift requires it to be. -private struct Position: Equatable { - let type: PositionType - let y: CGFloat -} - -// This might seem weird, but it's necessary due to the funny nature of -// how Preferences work. We can't just store the last position and merge -// it with the next one - instead we have a queue of all the latest positions. -private struct PositionPreferenceKey: PreferenceKey { - typealias Value = [Position] - - static var defaultValue = [Position]() - - static func reduce(value: inout [Position], nextValue: () -> [Position]) { - value.append(contentsOf: nextValue()) - } -} - -private struct PositionIndicator: View { - let type: PositionType - - var body: some View { - GeometryReader { proxy in - // the View itself is an invisible Shape that fills as much as possible - Color.clear - // Compute the top Y position and emit it to the Preferences queue - .preference(key: PositionPreferenceKey.self, value: [Position(type: type, y: proxy.frame(in: .global).minY)]) - } - } -} - -// Callback that'll trigger once refreshing is done -public typealias RefreshComplete = () -> Void - -// The actual refresh action that's called once refreshing starts. It has the -// RefreshComplete callback to let the refresh action let the View know -// once it's done refreshing. -public typealias OnRefresh = (@escaping RefreshComplete) -> Void - -// The offset threshold. 68 is a good number, but you can play -// with it to your liking. -public let defaultRefreshThreshold: CGFloat = 68 - -// Tracks the state of the RefreshableScrollView - it's either: -// 1. waiting for a scroll to happen -// 2. has been primed by pulling down beyond THRESHOLD -// 3. is doing the refreshing. -public enum RefreshState { - case waiting, primed, loading -} - -// ViewBuilder for the custom progress View, that may render itself -// based on the current RefreshState. -public typealias RefreshProgressBuilder = (RefreshState) -> Progress - -// Default color of the rectangle behind the progress spinner -public let defaultLoadingViewBackgroundColor = Color(UIColor.clear) - -public struct RefreshableScrollView: View where Progress: View, Content: View { - let showsIndicators: Bool // if the ScrollView should show indicators - let shouldTriggerHapticFeedback: Bool // if key actions should trigger haptic feedback - let loadingViewBackgroundColor: Color - let threshold: CGFloat // what height do you have to pull down to trigger the refresh - let onRefresh: OnRefresh // the refreshing action - let progress: RefreshProgressBuilder // custom progress view - let content: () -> Content // the ScrollView content - @State private var offset: CGFloat = 0 - @State private var state = RefreshState.waiting // the current state - // Haptic Feedback - let finishedReloadingFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium) - let primedFeedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) - - // We use a custom constructor to allow for usage of a @ViewBuilder for the content - public init(showsIndicators: Bool = true, - shouldTriggerHapticFeedback: Bool = false, - loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - onRefresh: @escaping OnRefresh, - @ViewBuilder progress: @escaping RefreshProgressBuilder, - @ViewBuilder content: @escaping () -> Content) { - self.showsIndicators = showsIndicators - self.shouldTriggerHapticFeedback = shouldTriggerHapticFeedback - self.loadingViewBackgroundColor = loadingViewBackgroundColor - self.threshold = threshold - self.onRefresh = onRefresh - self.progress = progress - self.content = content - } - - public var body: some View { - // The root view is a regular ScrollView - ScrollView(showsIndicators: showsIndicators) { - // The ZStack allows us to position the PositionIndicator, - // the content and the loading view, all on top of each other. - ZStack(alignment: .top) { - // The moving positioning indicator, that sits at the top - // of the ScrollView and scrolls down with the content - PositionIndicator(type: .moving) - .frame(height: 0) - - // Your ScrollView content. If we're loading, we want - // to keep it below the loading view, hence the alignmentGuide. - content() - .alignmentGuide(.top, computeValue: { _ in - (state == .loading) ? -threshold + max(0, offset) : 0 - }) - - // The loading view. It's offset to the top of the content unless we're loading. - ZStack { - Rectangle() - .foregroundColor(loadingViewBackgroundColor) - .frame(height: threshold) - progress(state) - }.offset(y: (state == .loading) ? -max(0, offset) : -threshold) - } - } - // Put a fixed PositionIndicator in the background so that we have - // a reference point to compute the scroll offset. - .background(PositionIndicator(type: .fixed)) - // Once the scrolling offset changes, we want to see if there should - // be a state change. - .onPreferenceChange(PositionPreferenceKey.self) { values in - DispatchQueue.main.async { - // Compute the offset between the moving and fixed PositionIndicators - let movingY = values.first { $0.type == .moving }?.y ?? 0 - let fixedY = values.first { $0.type == .fixed }?.y ?? 0 - offset = movingY - fixedY - if state != .loading { // If we're already loading, ignore everything - // Map the preference change action to the UI thread - // If the user pulled down below the threshold, prime the view - if offset > threshold && state == .waiting { - state = .primed - if shouldTriggerHapticFeedback { - self.primedFeedbackGenerator.impactOccurred() - } - - // If the view is primed and we've crossed the threshold again on the - // way back, trigger the refresh - } else if offset < threshold && state == .primed { - state = .loading - onRefresh { // trigger the refreshing callback - // once refreshing is done, smoothly move the loading view - // back to the offset position - withAnimation { - self.state = .waiting - } - if shouldTriggerHapticFeedback { - self.finishedReloadingFeedbackGenerator.impactOccurred() - } - } - } - } - } - } - } -} - -// Extension that uses default RefreshActivityIndicator so that you don't have to -// specify it every time. -public extension RefreshableScrollView where Progress == RefreshActivityIndicator { - init(showsIndicators: Bool = true, - loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - onRefresh: @escaping OnRefresh, - @ViewBuilder content: @escaping () -> Content) { - self.init(showsIndicators: showsIndicators, - loadingViewBackgroundColor: loadingViewBackgroundColor, - threshold: threshold, - onRefresh: onRefresh, - progress: { state in - RefreshActivityIndicator(isAnimating: state == .loading) { - $0.hidesWhenStopped = false - } - }, - content: content) - } -} - -// Wraps a UIActivityIndicatorView as a loading spinner that works on all SwiftUI versions. -public struct RefreshActivityIndicator: UIViewRepresentable { - public typealias UIView = UIActivityIndicatorView - public var isAnimating: Bool = true - public var configuration = { (indicator: UIView) in } - - public init(isAnimating: Bool, configuration: ((UIView) -> Void)? = nil) { - self.isAnimating = isAnimating - if let configuration = configuration { - self.configuration = configuration - } - } - - public func makeUIView(context: UIViewRepresentableContext) -> UIView { - UIView() - } - - public func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext) { - isAnimating ? uiView.startAnimating() : uiView.stopAnimating() - configuration(uiView) - } -} - -#if compiler(>=5.5) -// Allows using RefreshableScrollView with an async block. -@available(iOS 15.0, *) -public extension RefreshableScrollView { - init(showsIndicators: Bool = true, - loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - action: @escaping @Sendable () async -> Void, - @ViewBuilder progress: @escaping RefreshProgressBuilder, - @ViewBuilder content: @escaping () -> Content) { - self.init(showsIndicators: showsIndicators, - loadingViewBackgroundColor: loadingViewBackgroundColor, - threshold: threshold, - onRefresh: { refreshComplete in - Task { - await action() - refreshComplete() - } - }, - progress: progress, - content: content) - } -} -#endif - -public struct RefreshableCompat: ViewModifier where Progress: View { - private let showsIndicators: Bool - private let loadingViewBackgroundColor: Color - private let threshold: CGFloat - private let onRefresh: OnRefresh - private let progress: RefreshProgressBuilder - - public init(showsIndicators: Bool = true, - loadingViewBackgroundColor: Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - onRefresh: @escaping OnRefresh, - @ViewBuilder progress: @escaping RefreshProgressBuilder) { - self.showsIndicators = showsIndicators - self.loadingViewBackgroundColor = loadingViewBackgroundColor - self.threshold = threshold - self.onRefresh = onRefresh - self.progress = progress - } - - public func body(content: Content) -> some View { - RefreshableScrollView(showsIndicators: showsIndicators, - loadingViewBackgroundColor: loadingViewBackgroundColor, - threshold: threshold, - onRefresh: onRefresh, - progress: progress) { - content - } - } -} - -#if compiler(>=5.5) -@available(iOS 15.0, *) -public extension List { - @ViewBuilder func refreshableCompat(showsIndicators: Bool = true, - loadingViewBackgroundColor: - Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - onRefresh: @escaping OnRefresh, - @ViewBuilder progress: - @escaping RefreshProgressBuilder) -> some View { - if #available(iOS 15.0, macOS 12.0, *) { - self.refreshable { - await withCheckedContinuation { cont in - onRefresh { - cont.resume() - } - } - } - } else { - self.modifier(RefreshableCompat(showsIndicators: showsIndicators, - loadingViewBackgroundColor: loadingViewBackgroundColor, - threshold: threshold, - onRefresh: onRefresh, - progress: progress)) - } - } -} -#endif - -public extension View { - @ViewBuilder func refreshableCompat(showsIndicators: Bool = true, - loadingViewBackgroundColor: - Color = defaultLoadingViewBackgroundColor, - threshold: CGFloat = defaultRefreshThreshold, - onRefresh: @escaping OnRefresh, - @ViewBuilder progress: - @escaping RefreshProgressBuilder) -> some View { - self.modifier(RefreshableCompat(showsIndicators: showsIndicators, - loadingViewBackgroundColor: loadingViewBackgroundColor, - threshold: threshold, - onRefresh: onRefresh, - progress: progress)) - } -} - -struct ActivityIndicator: UIViewRepresentable { - public typealias UIView = UIActivityIndicatorView - public var isAnimating: Bool = true - public var configuration = { (indicator: UIView) in } - - public init(isAnimating: Bool, configuration: ((UIView) -> Void)? = nil) { - self.isAnimating = isAnimating - if let configuration = configuration { - self.configuration = configuration - } - } - - public func makeUIView(context: UIViewRepresentableContext) -> UIView { - let uiView = UIView() - uiView.startAnimating() - return uiView - } - - public func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext) { -// isAnimating ? uiView.startAnimating() : uiView.stopAnimating() - configuration(uiView) - } - } diff --git a/Core/Core/View/Base/RefreshableScrollViewCompat.swift b/Core/Core/View/Base/RefreshableScrollViewCompat.swift deleted file mode 100644 index 768aa08b9..000000000 --- a/Core/Core/View/Base/RefreshableScrollViewCompat.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// RefreshableScrollViewCompat.swift -// Core -// -// Created by  Stepanok Ivan on 15.09.2023. -// - -import SwiftUI - -public struct RefreshableScrollViewCompat: View where Content: View { - private let content: () -> Content - private let action: () async -> Void - - public init(action: @escaping () async -> Void, @ViewBuilder content: @escaping () -> Content) { - self.action = action - self.content = content - } - - public var body: some View { - if #available(iOS 16.0, *) { - return ScrollView { - content() - }.refreshable { - Task { - await action() - } - } - } else { - return RefreshableScrollView(onRefresh: { done in - Task { - await action() - done() - } - }) { - content() - } - } - } -} diff --git a/Core/Core/View/Base/RegistrationTextField.swift b/Core/Core/View/Base/RegistrationTextField.swift index 9ed039763..7cf4788d4 100644 --- a/Core/Core/View/Base/RegistrationTextField.swift +++ b/Core/Core/View/Base/RegistrationTextField.swift @@ -48,7 +48,7 @@ public struct RegistrationTextField: View { .padding(.vertical, 4) .foregroundColor(Theme.Colors.textInputTextColor) .frame(height: 100) - .hideScrollContentBackground() + .scrollContentBackground(.hidden) .background( Theme.Shapes.textInputShape .fill(Theme.Colors.textInputBackground) @@ -60,7 +60,7 @@ public struct RegistrationTextField: View { .fill( config.error == "" ? Theme.Colors.textInputStroke - : Theme.Colors.alert + : Theme.Colors.irreversibleAlert ) ) .shake($config.shake) @@ -83,7 +83,7 @@ public struct RegistrationTextField: View { .fill( config.error == "" ? Theme.Colors.textInputStroke - : Theme.Colors.alert + : Theme.Colors.irreversibleAlert ) ) .shake($config.shake) @@ -107,7 +107,7 @@ public struct RegistrationTextField: View { .fill( config.error == "" ? Theme.Colors.textInputStroke - : Theme.Colors.alert + : Theme.Colors.irreversibleAlert ) ) .shake($config.shake) @@ -119,7 +119,7 @@ public struct RegistrationTextField: View { .font(Theme.Fonts.bodySmall) .foregroundColor(config.error == "" ? Theme.Colors.textSecondaryLight - : Theme.Colors.alert) + : Theme.Colors.irreversibleAlert) .accessibilityIdentifier("\(config.field.name)_instructions_text") } } diff --git a/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift b/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift index 5f09777a7..ef300e279 100644 --- a/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift +++ b/Core/Core/View/Base/ScrollSlidingTabBar/ScrollSlidingTabBar.swift @@ -49,6 +49,9 @@ public struct ScrollSlidingTabBar: View { } .onTapGesture {} // Fix button tapable area bug – https://forums.developer.apple.com/forums/thread/745059 + .onAppear { + proxy.scrollTo(selection, anchor: .center) + } .onChange(of: selection) { newValue in withAnimation { proxy.scrollTo(newValue, anchor: .center) @@ -100,7 +103,7 @@ extension ScrollSlidingTabBar { } .accentColor( isSelected(index: obj.offset) - ? Theme.Colors.white + ? Theme.Colors.slidingSelectedTextColor : Theme.Colors.slidingTextColor ) } diff --git a/Core/Core/View/Base/SnackBarView.swift b/Core/Core/View/Base/SnackBarView.swift index 14ed079f4..fc61351de 100644 --- a/Core/Core/View/Base/SnackBarView.swift +++ b/Core/Core/View/Base/SnackBarView.swift @@ -14,7 +14,7 @@ public struct SnackBarView: View { var action: (() -> Void)? private var safeArea: CGFloat { - UIApplication.shared.windows.first { $0.isKeyWindow }?.safeAreaInsets.bottom ?? 0 + UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0 } private let minHeight: CGFloat = 50 diff --git a/Core/Core/View/Base/StyledButton.swift b/Core/Core/View/Base/StyledButton.swift index f76252e44..6aa2962f4 100644 --- a/Core/Core/View/Base/StyledButton.swift +++ b/Core/Core/View/Base/StyledButton.swift @@ -22,6 +22,7 @@ public struct StyledButton: View { private let buttonColor: Color private let textColor: Color private let isActive: Bool + private let horizontalPadding: Bool private let borderColor: Color private let iconImage: Image? private let iconPosition: IconImagePosition @@ -34,7 +35,8 @@ public struct StyledButton: View { borderColor: Color = .clear, iconImage: Image? = nil, iconPosition: IconImagePosition = .none, - isActive: Bool = true) { + isActive: Bool = true, + horizontalPadding: Bool = false) { self.title = title self.action = action self.isTransparent = isTransparent @@ -44,6 +46,7 @@ public struct StyledButton: View { self.isActive = isActive self.iconImage = iconImage self.iconPosition = iconPosition + self.horizontalPadding = horizontalPadding } public var body: some View { @@ -69,6 +72,7 @@ public struct StyledButton: View { } Spacer() } + .padding(.horizontal, horizontalPadding ? 20 : 0) } .disabled(!isActive) .frame(maxWidth: idiom == .pad ? 260: .infinity, minHeight: isTransparent ? 36 : 42) diff --git a/Core/Core/View/Base/UnitButtonView.swift b/Core/Core/View/Base/UnitButtonView.swift index 6e39ff997..37c94eeb4 100644 --- a/Core/Core/View/Base/UnitButtonView.swift +++ b/Core/Core/View/Base/UnitButtonView.swift @@ -75,23 +75,23 @@ public struct UnitButtonView: View { case .first: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .font(Theme.Fonts.labelLarge) CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .rotationEffect(Angle.degrees(nextButtonDegrees)) }.padding(.horizontal, 16) case .next, .nextBig: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .padding(.leading, 20) .font(Theme.Fonts.labelLarge) if type != .nextBig { Spacer() } CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .rotationEffect(Angle.degrees(nextButtonDegrees)) .padding(.trailing, 20) } @@ -99,19 +99,19 @@ public struct UnitButtonView: View { HStack { if isVerticalNavigation { Text(type.stringValue()) - .foregroundColor(Theme.Colors.secondaryButtonTextColor) + .foregroundColor(Theme.Colors.accentColor) .font(Theme.Fonts.labelLarge) .padding(.leading, 20) CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) .rotationEffect(Angle.degrees(90)) .padding(.trailing, 20) - .foregroundColor(Theme.Colors.secondaryButtonTextColor) + .foregroundColor(Theme.Colors.accentColor) } else { CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) .padding(.leading, 20) - .foregroundColor(Theme.Colors.secondaryButtonTextColor) + .foregroundColor(Theme.Colors.accentColor) Text(type.stringValue()) - .foregroundColor(Theme.Colors.secondaryButtonTextColor) + .foregroundColor(Theme.Colors.accentColor) .font(Theme.Fonts.labelLarge) .padding(.trailing, 20) } @@ -119,22 +119,22 @@ public struct UnitButtonView: View { case .last: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .padding(.leading, 8) .font(Theme.Fonts.labelLarge) .scaledToFit() Spacer() CoreAssets.check.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .padding(.trailing, 8) } case .finish: HStack { Text(type.stringValue()) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) .font(Theme.Fonts.labelLarge) CoreAssets.check.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor(Theme.Colors.primaryButtonTextColor) }.padding(.horizontal, 16) case .reload, .custom: VStack(alignment: .center) { @@ -149,7 +149,11 @@ public struct UnitButtonView: View { .padding(.leading, 20) .font(Theme.Fonts.labelLarge) CoreAssets.arrowLeft.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.styledButtonText) + .foregroundColor( + type == .continueLesson + ? Theme.Colors.accentColor + : Theme.Colors.styledButtonText + ) .rotationEffect(Angle.degrees(180)) .padding(.trailing, 20) } @@ -163,7 +167,7 @@ public struct UnitButtonView: View { Theme.Shapes.buttonShape .fill(type == .previous ? Theme.Colors.background - : Theme.Colors.accentButtonColor) + : Theme.Colors.accentColor) .shadow(color: Color.black.opacity(0.25), radius: 21, y: 4) .overlay( Theme.Shapes.buttonShape @@ -174,8 +178,7 @@ public struct UnitButtonView: View { miterLimit: 1) ) .foregroundColor( - type == .previous ? Theme.Colors.secondaryButtonBorderColor - : Theme.Colors.accentButtonColor + Theme.Colors.accentColor ) ) @@ -199,7 +202,8 @@ public struct UnitButtonView: View { miterLimit: 1 )) .foregroundColor( - type == .continueLesson ? Theme.Colors.accentButtonColor + type == .continueLesson + ? Theme.Colors.accentButtonColor : Theme.Colors.secondaryButtonBorderColor ) ) diff --git a/Core/Core/View/Base/VideoDownloadQualityView.swift b/Core/Core/View/Base/VideoDownloadQualityView.swift index 2401a4f27..2fd29240a 100644 --- a/Core/Core/View/Base/VideoDownloadQualityView.swift +++ b/Core/Core/View/Base/VideoDownloadQualityView.swift @@ -33,11 +33,16 @@ public struct VideoDownloadQualityView: View { @StateObject private var viewModel: VideoDownloadQualityViewModel private var analytics: CoreAnalytics + private var router: BaseRouter + private var isModal: Bool + @Environment(\.isHorizontal) private var isHorizontal public init( downloadQuality: DownloadQuality, didSelect: ((DownloadQuality) -> Void)?, - analytics: CoreAnalytics + analytics: CoreAnalytics, + router: BaseRouter, + isModal: Bool = false ) { self._viewModel = StateObject( wrappedValue: .init( @@ -46,64 +51,99 @@ public struct VideoDownloadQualityView: View { ) ) self.analytics = analytics + self.router = router + self.isModal = isModal } public var body: some View { GeometryReader { proxy in ZStack(alignment: .top) { - // MARK: - Page Body - ScrollView { - VStack(alignment: .leading, spacing: 24) { - ForEach(viewModel.downloadQuality, id: \.self) { quality in - Button(action: { - analytics.videoQualityChanged( - .videoDownloadQualityChanged, - bivalue: .videoDownloadQualityChanged, - value: quality.value ?? "", - oldValue: viewModel.selectedDownloadQuality.value ?? "" + if !isModal { + VStack { + ThemeAssets.headerBackground.swiftUIImage + .resizable() + .edgesIgnoringSafeArea(.top) + } + .frame(maxWidth: .infinity, maxHeight: 200) + .accessibilityIdentifier("auth_bg_image") + } + + // MARK: - Page name + VStack(alignment: .center) { + if !isModal { + ZStack { + HStack { + Text(CoreLocalization.Settings.videoDownloadQualityTitle) + .titleSettings(color: Theme.Colors.loginNavigationText) + .accessibilityIdentifier("manage_account_text") + } + VStack { + BackNavigationButton( + color: Theme.Colors.loginNavigationText, + action: { + router.back() + } ) + .backViewStyle() + .padding(.leading, isHorizontal ? 48 : 0) + .accessibilityIdentifier("back_button") - viewModel.selectedDownloadQuality = quality - }, label: { - HStack { - SettingsCell( - title: quality.title, - description: quality.description + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + } + } + // MARK: - Page Body + ScrollView { + VStack(alignment: .leading, spacing: 24) { + ForEach(viewModel.downloadQuality, id: \.self) { quality in + Button(action: { + analytics.videoQualityChanged( + .videoDownloadQualityChanged, + bivalue: .videoDownloadQualityChanged, + value: quality.value ?? "", + oldValue: viewModel.selectedDownloadQuality.value ?? "" ) - .accessibilityElement(children: .ignore) - .accessibilityLabel("\(quality.title) \(quality.description ?? "")") - Spacer() - CoreAssets.checkmark.swiftUIImage - .renderingMode(.template) - .foregroundColor(Theme.Colors.accentXColor) - .opacity(quality == viewModel.selectedDownloadQuality ? 1 : 0) - .accessibilityIdentifier("checkmark_image") - } - .foregroundColor(Theme.Colors.textPrimary) - }) - .accessibilityIdentifier("select_quality_button") - Divider() + viewModel.selectedDownloadQuality = quality + }, label: { + HStack { + SettingsCell( + title: quality.title, + description: quality.description + ) + .accessibilityElement(children: .ignore) + .accessibilityLabel("\(quality.title) \(quality.description ?? "")") + Spacer() + CoreAssets.checkmark.swiftUIImage + .renderingMode(.template) + .foregroundColor(Theme.Colors.accentXColor) + .opacity(quality == viewModel.selectedDownloadQuality ? 1 : 0) + .accessibilityIdentifier("checkmark_image") + + } + .foregroundColor(Theme.Colors.textPrimary) + }) + .accessibilityIdentifier("select_quality_button") + Divider() + } } + .frameLimit(width: proxy.size.width) + .padding(.horizontal, 24) + .padding(.top, 24) } - .frame( - minWidth: 0, - maxWidth: .infinity, - alignment: .topLeading - ) - .padding(.horizontal, 24) - .frameLimit(width: proxy.size.width) + .roundedBackground(Theme.Colors.background) } - .padding(.top, 8) } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(CoreLocalization.Settings.videoDownloadQualityTitle) - .background( - Theme.Colors.background - .ignoresSafeArea() - ) } + .navigationBarHidden(!isModal) + .navigationBarBackButtonHidden(!isModal) + .navigationTitle(CoreLocalization.Settings.videoDownloadQualityTitle) + .ignoresSafeArea(.all, edges: .horizontal) + .background( + Theme.Colors.background + .ignoresSafeArea() + ) } } @@ -177,3 +217,17 @@ public extension DownloadQuality { } } } + +#if DEBUG +struct VideoDownloadQualityView_Previews: PreviewProvider { + static var previews: some View { + VideoDownloadQualityView( + downloadQuality: .auto, + didSelect: nil, + analytics: CoreAnalyticsMock(), + router: BaseRouterMock(), + isModal: true + ) + } +} +#endif diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift index 9d6a113ac..cd61dcdb6 100644 --- a/Core/Core/View/Base/WebBrowser.swift +++ b/Core/Core/View/Base/WebBrowser.swift @@ -17,11 +17,18 @@ public struct WebBrowser: View { private var url: String private var pageTitle: String private var showProgress: Bool + private let connectivity: ConnectivityProtocol - public init(url: String, pageTitle: String, showProgress: Bool = false) { + public init( + url: String, + pageTitle: String, + showProgress: Bool = false, + connectivity: ConnectivityProtocol + ) { self.url = url self.pageTitle = pageTitle self.showProgress = showProgress + self.connectivity = connectivity } public var body: some View { @@ -57,10 +64,13 @@ public struct WebBrowser: View { viewModel: .init( url: url, baseURL: "", + openFile: {_ in}, injections: [.colorInversionCss, .readability, .accessibility] ), isLoading: $isLoading, - refreshCookies: {} + refreshCookies: { + }, + connectivity: connectivity ) .accessibilityIdentifier("web_browser") } @@ -71,6 +81,6 @@ public struct WebBrowser: View { struct WebBrowser_Previews: PreviewProvider { static var previews: some View { - WebBrowser(url: "", pageTitle: "") + WebBrowser(url: "", pageTitle: "", connectivity: Connectivity()) } } diff --git a/Core/Core/View/Base/WebUnitView.swift b/Core/Core/View/Base/WebUnitView.swift index a00b3a4b2..fda1e5040 100644 --- a/Core/Core/View/Base/WebUnitView.swift +++ b/Core/Core/View/Base/WebUnitView.swift @@ -10,23 +10,38 @@ import SwiftUI import Theme public struct WebUnitView: View { - + @StateObject private var viewModel: WebUnitViewModel @State private var isWebViewLoading = false - + private var url: String private var injections: [WebviewInjection]? - + private let connectivity: ConnectivityProtocol + private var blockID: String + @State private var isFileOpen: Bool = false + @State private var dataUrl: String? + @State private var fileUrl: String = "" + public init( url: String, + dataUrl: String?, viewModel: WebUnitViewModel, - injections: [WebviewInjection]? + connectivity: ConnectivityProtocol, + injections: [WebviewInjection]?, + blockID: String ) { self._viewModel = .init( wrappedValue: viewModel ) self.url = url + self.dataUrl = dataUrl + self.connectivity = connectivity self.injections = injections + self.blockID = blockID + + if !self.connectivity.isInternetAvaliable, let dataUrl { + self.url = dataUrl + } } @ViewBuilder @@ -62,11 +77,14 @@ public struct WebUnitView: View { ZStack(alignment: .center) { GeometryReader { reader in ScrollView { - if viewModel.cookiesReady { + if viewModel.cookiesReady || dataUrl != nil { WebView( viewModel: .init( url: url, baseURL: viewModel.config.baseURL.absoluteString, + openFile: { file in + self.fileUrl = file + }, injections: injections ), isLoading: $isWebViewLoading, @@ -74,6 +92,10 @@ public struct WebUnitView: View { await viewModel.updateCookies( force: true ) + }, + connectivity: connectivity, + message: { message in + viewModel.syncManager.handleMessage(message: message, blockID: blockID) } ) .frame( @@ -82,8 +104,32 @@ public struct WebUnitView: View { ) } } - .introspect(.scrollView, on: .iOS(.v15...), customize: { scrollView in - scrollView.isScrollEnabled = false + .scrollDisabled(true) + .onChange(of: self.fileUrl, perform: { file in + if file != "" { + self.isFileOpen = true + } + }) + .sheet(isPresented: $isFileOpen, onDismiss: { self.fileUrl = ""; isFileOpen = false }, content: { + GeometryReader { reader2 in + ZStack(alignment: .topTrailing) { + ScrollView { + FileWebView(viewModel: FileWebView.ViewModel(url: fileUrl)) + .frame(width: reader2.size.width, height: reader2.size.height) + } + Button(action: { + isFileOpen = false + }, label: { + ZStack { + Circle().frame(width: 32, height: 32) + .foregroundColor(.white) + .shadow(color: .black.opacity(0.2), radius: 12) + Image(systemName: "xmark").renderingMode(.template) + .foregroundColor(.black) + }.padding(16) + }) + } + } }) if viewModel.updatingCookies || isWebViewLoading { VStack { @@ -94,7 +140,9 @@ public struct WebUnitView: View { } }.onFirstAppear { Task { - await viewModel.updateCookies() + if dataUrl == nil { + await viewModel.updateCookies() + } } } } diff --git a/Core/Core/View/Base/WebUnitViewModel.swift b/Core/Core/View/Base/WebUnitViewModel.swift index 6a76a6ee2..e8bc585c3 100644 --- a/Core/Core/View/Base/WebUnitViewModel.swift +++ b/Core/Core/View/Base/WebUnitViewModel.swift @@ -12,6 +12,7 @@ public class WebUnitViewModel: ObservableObject, WebviewCookiesUpdateProtocol { public let authInteractor: AuthInteractorProtocol let config: ConfigProtocol + let syncManager: OfflineSyncManagerProtocol @Published public var updatingCookies: Bool = false @Published public var cookiesReady: Bool = false @@ -26,8 +27,13 @@ public class WebUnitViewModel: ObservableObject, WebviewCookiesUpdateProtocol { } } - public init(authInteractor: AuthInteractorProtocol, config: ConfigProtocol) { + public init( + authInteractor: AuthInteractorProtocol, + config: ConfigProtocol, + syncManager: OfflineSyncManagerProtocol + ) { self.authInteractor = authInteractor self.config = config + self.syncManager = syncManager } } diff --git a/Core/Core/View/Base/Webview/WebView.swift b/Core/Core/View/Base/Webview/WebView.swift index ce757b1c9..c5b21e7ef 100644 --- a/Core/Core/View/Base/Webview/WebView.swift +++ b/Core/Core/View/Base/Webview/WebView.swift @@ -17,6 +17,8 @@ public protocol WebViewNavigationDelegate: AnyObject { shouldLoad request: URLRequest, navigationAction: WKNavigationAction ) async -> Bool + + func showWebViewError() } public struct WebView: UIViewRepresentable { @@ -26,10 +28,17 @@ public struct WebView: UIViewRepresentable { @Published var url: String let baseURL: String let injections: [WebviewInjection]? + var openFile: (String) -> Void - public init(url: String, baseURL: String, injections: [WebviewInjection]? = nil) { + public init( + url: String, + baseURL: String, + openFile: @escaping (String) -> Void, + injections: [WebviewInjection]? = nil + ) { self.url = url self.baseURL = baseURL + self.openFile = openFile self.injections = injections } } @@ -37,19 +46,29 @@ public struct WebView: UIViewRepresentable { @ObservedObject var viewModel: ViewModel @Binding public var isLoading: Bool var webViewNavDelegate: WebViewNavigationDelegate? + let connectivity: ConnectivityProtocol + var message: ((WKScriptMessage) -> Void) var refreshCookies: () async -> Void + var webViewType: String? + private let userContentControllerName = "IOSBridge" public init( viewModel: ViewModel, isLoading: Binding, refreshCookies: @escaping () async -> Void, - navigationDelegate: WebViewNavigationDelegate? = nil + navigationDelegate: WebViewNavigationDelegate? = nil, + connectivity: ConnectivityProtocol, + message: @escaping ((WKScriptMessage) -> Void) = { _ in }, + webViewType: String? = nil ) { self.viewModel = viewModel self._isLoading = isLoading self.refreshCookies = refreshCookies self.webViewNavDelegate = navigationDelegate + self.connectivity = connectivity + self.message = message + self.webViewType = webViewType } public class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler { @@ -70,6 +89,10 @@ public struct WebView: UIViewRepresentable { public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { webView.isHidden = false + DispatchQueue.main.async { + self.parent.isLoading = false + self.parent.webViewNavDelegate?.showWebViewError() + } } public func webView( @@ -78,6 +101,10 @@ public struct WebView: UIViewRepresentable { withError error: Error ) { webView.isHidden = false + DispatchQueue.main.async { + self.parent.isLoading = false + self.parent.webViewNavDelegate?.showWebViewError() + } } public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { @@ -109,6 +136,17 @@ public struct WebView: UIViewRepresentable { handler: { _ in completionHandler(false) })) + + if let presenter = alertController.popoverPresentationController { + let view = UIApplication.topViewController()?.view + presenter.sourceView = view + presenter.sourceRect = CGRect( + x: view?.bounds.midX ?? 0, + y: view?.bounds.midY ?? 0, + width: 0, + height: 0 + ) + } UIApplication.topViewController()?.present(alertController, animated: true, completion: nil) } @@ -120,6 +158,13 @@ public struct WebView: UIViewRepresentable { guard let url = navigationAction.request.url else { return .cancel } + if url.absoluteString.starts(with: "file:///") { + if url.pathExtension == "pdf" { + await parent.viewModel.openFile(url.absoluteString) + return .cancel + } + } + let isWebViewDelegateHandled = await ( parent.webViewNavDelegate?.webView( webView, @@ -132,38 +177,43 @@ public struct WebView: UIViewRepresentable { } let baseURL = await parent.viewModel.baseURL - if !baseURL.isEmpty, !url.absoluteString.starts(with: baseURL) { - if navigationAction.navigationType == .other { - return .allow - } else if navigationAction.navigationType == .linkActivated { - await MainActor.run { + switch navigationAction.navigationType { + case .other, .formSubmitted, .formResubmitted: + return .allow + case .linkActivated: + await MainActor.run { + if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url, options: [:]) } - } else if navigationAction.navigationType == .formSubmitted { - return .allow } return .cancel + default: + if !baseURL.isEmpty, !url.absoluteString.starts(with: baseURL) { + return .cancel + } else { + return .allow + } } - - return .allow } public func webView( _ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse ) async -> WKNavigationResponsePolicy { - guard let response = (navigationResponse.response as? HTTPURLResponse), - let url = response.url else { - return .cancel - } - let baseURL = await parent.viewModel.baseURL - - if (401...404).contains(response.statusCode) || url.absoluteString.hasPrefix(baseURL + "/login") { - await parent.refreshCookies() - DispatchQueue.main.async { - if let url = webView.url { - let request = URLRequest(url: url) - webView.load(request) + if parent.connectivity.isInternetAvaliable { + guard let response = (navigationResponse.response as? HTTPURLResponse), + let url = response.url else { + return .cancel + } + let baseURL = await parent.viewModel.baseURL + + if (401...404).contains(response.statusCode) || url.absoluteString.hasPrefix(baseURL + "/login") { + await parent.refreshCookies() + DispatchQueue.main.async { + if let url = webView.url { + let request = URLRequest(url: url) + webView.load(request) + } } } } @@ -172,7 +222,7 @@ public struct WebView: UIViewRepresentable { private func addObservers() { cancellables.removeAll() - NotificationCenter.default.publisher(for: .webviewReloadNotification, object: nil) + NotificationCenter.default.publisher(for: Notification.Name(parent.webViewType ?? ""), object: nil) .sink { [weak self] _ in self?.reload() } @@ -188,17 +238,30 @@ public struct WebView: UIViewRepresentable { fileprivate var webview: WKWebView? @objc private func reload() { - parent.isLoading = true - webview?.reload() + DispatchQueue.main.async { + self.parent.isLoading = true + } + if webview?.url?.absoluteString.isEmpty ?? true, + let url = URL(string: parent.viewModel.url) { + let request = URLRequest(url: url) + webview?.load(request) + } else { + webview?.reload() + } } public func userContentController( _ userContentController: WKUserContentController, didReceive message: WKScriptMessage ) { + self.parent.message(message) parent.viewModel.injections?.handle(message: message) } } + + public func webView(_ webView: WKWebView, shouldPreviewElement elementInfo: WKContextMenuElementInfo) -> Bool { + return true + } private var userAgent: String { let info = Bundle.main.infoDictionary @@ -217,12 +280,13 @@ public struct WebView: UIViewRepresentable { public func makeUIView(context: UIViewRepresentableContext) -> WKWebView { let webViewConfig = WKWebViewConfiguration() + webViewConfig.userContentController.add(context.coordinator, name: userContentControllerName) + webViewConfig.defaultWebpagePreferences.allowsContentJavaScript = true + webViewConfig.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs") let webView = WKWebView(frame: .zero, configuration: webViewConfig) #if DEBUG - if #available(iOS 16.4, *) { - webView.isInspectable = true - } + webView.isInspectable = true #endif webView.navigationDelegate = context.coordinator webView.uiDelegate = context.coordinator @@ -239,7 +303,6 @@ public struct WebView: UIViewRepresentable { webView.scrollView.backgroundColor = Theme.Colors.background.uiColor() webView.scrollView.alwaysBounceVertical = false webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 200, right: 0) - // To add ability to change font size with webkitTextSizeAdjust need to set mode to mobile webView.configuration.defaultWebpagePreferences.preferredContentMode = .mobile webView.applyInjections(viewModel.injections, toHandler: context.coordinator) @@ -302,8 +365,8 @@ extension WKWebView { extension Array where Element == WebviewInjection { func handle(message: WKScriptMessage) { - let messages = compactMap{ $0.messages } - .flatMap{ $0 } + let messages = compactMap { $0.messages } + .flatMap { $0 } if let currentMessage = messages.first(where: { $0.name == message.name }) { currentMessage.handler(message.body, message.webView) } diff --git a/Core/Core/View/Base/Webview/WebViewHTML.swift b/Core/Core/View/Base/Webview/WebViewHTML.swift index 4db4d2abb..1d40cc082 100644 --- a/Core/Core/View/Base/Webview/WebViewHTML.swift +++ b/Core/Core/View/Base/Webview/WebViewHTML.swift @@ -36,9 +36,7 @@ public struct WebViewHtml: UIViewRepresentable { context.coordinator.webview = webView #if DEBUG - if #available(iOS 16.4, *) { - webView.isInspectable = true - } + webView.isInspectable = true #endif return webView } diff --git a/Core/Core/en.lproj/Localizable.strings b/Core/Core/en.lproj/Localizable.strings index 27196487a..97d00ae26 100644 --- a/Core/Core/en.lproj/Localizable.strings +++ b/Core/Core/en.lproj/Localizable.strings @@ -11,6 +11,7 @@ "MAINSCREEN.IN_DEVELOPING" = "In developing"; "MAINSCREEN.PROGRAMS" = "Programs"; "MAINSCREEN.PROFILE" = "Profile"; +"MAINSCREEN.LEARN" = "Learn"; "VIEW.SNACKBAR.TRY_AGAIN_BTN" = "Try Again"; @@ -44,10 +45,20 @@ "ERROR.RELOAD" = "Reload"; +"DATE.COURSE_STARTS" = "Course Starts"; +"DATE.COURSE_ENDS" = "Course Ends"; +"DATE.COURSE_ENDED" = "Course Ended"; + "DATE.ENDED" = "Ended"; "DATE.START" = "Start"; "DATE.STARTED" = "Started"; "DATE.JUST_NOW" = "Just now"; +"DATE.TODAY" = "Today"; +"DATE.NEXT" = "Next %@"; +"DATE.DAYS_AGO" = "%@ Days Ago"; +"DATE.DUE" = "Due "; +"DATE.DUE_IN" = "Due in "; +"DATE.DUE_IN_DAYS" = "Due in %@ Days"; "ALERT.ACCEPT" = "ACCEPT"; "ALERT.CANCEL" = "CANCEL"; @@ -65,6 +76,7 @@ "DATE_FORMAT.MMMM_DD" = "MMMM dd"; "DATE_FORMAT.MMM_DD_YYYY" = "MMM dd, yyyy"; +"DATE_FORMAT.MMMM_DD_YYYY" = "MMMM dd, yyyy"; "DOWNLOAD_MANAGER.DOWNLOAD" = "Download"; "DOWNLOAD_MANAGER.DOWNLOADED" = "Downloaded"; @@ -80,7 +92,10 @@ "SETTINGS.DOWNLOAD_QUALITY_720_DESCRIPTION" = "Best quality"; "DONE" = "Done"; -"VIEW " = "View"; +"VIEW" = "View"; +"BACK" = "Back"; +"OK" = "Ok"; +"CLOSE" = "Close"; "PICKER.SEARCH" = "Search"; "PICKER.ACCEPT" = "Accept"; @@ -108,9 +123,36 @@ "SOCIAL_SIGN_CANCELED" = "The user canceled the sign-in flow."; "SIGN_IN.LOG_IN_BTN" = "Sign in"; -"REGISTER" = "Register"; +"SIGN_IN.REGISTER_BTN" = "Register"; +"SIGN_IN.LOG_IN_WITH_SSO_BTN" = "Sign in with SSO"; "TOMORROW" = "Tomorrow"; "YESTERDAY" = "Yesterday"; "OPEN_IN_BROWSER"="View in Safari"; + +"COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.BODY" = "Don't worry - shift our suggested schedule to complete past due assignments without losing any progress."; +"COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.BUTTON" = "Shift due dates"; +"COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.HEADER" = "Missed some deadlines?"; + +"COURSE_DATES.RESET_DATE.TAB_INFO_BANNER.BODY" = "We built a suggested schedule to help you stay on track. But don’t worry – it’s flexible so you can learn at your own pace. If you happen to fall behind, you’ll be able to adjust the dates to keep yourself on track."; +"COURSE_DATES.RESET_DATE.TAB_INFO_BANNER.HEADER" = ""; + +"COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.BODY" = "To complete graded assignments as part of this course, you can upgrade today."; +"COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.BUTTON" = ""; +"COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.HEADER" = ""; + +"COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.BODY" = "You are auditing this course, which means that you are unable to participate in graded assignments. It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today."; +"COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.BUTTON" = ""; +"COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.HEADER" = ""; + +"COURSE_DATES.RESET_DATE.ERROR_MESSAGE" = "Your dates could not be shifted. Please try again."; +"COURSE_DATES.RESET_DATE.SUCCESS_MESSAGE" = "Your dates have been successfully shifted."; +"COURSE_DATES.RESET_DATE.TITLE" = "Course Dates"; + +"COURSE_DATES.TODAY" = "Today"; +"COURSE_DATES.COMPLETED" = "Completed"; +"COURSE_DATES.PAST_DUE" = "Past due"; +"COURSE_DATES.THIS_WEEK" = "This week"; +"COURSE_DATES.NEXT_WEEK" = "Next week"; +"COURSE_DATES.UPCOMING" = "Upcoming"; diff --git a/Core/Core/uk.lproj/Localizable.strings b/Core/Core/uk.lproj/Localizable.strings deleted file mode 100644 index 1da07ab04..000000000 --- a/Core/Core/uk.lproj/Localizable.strings +++ /dev/null @@ -1,115 +0,0 @@ -/* - Localizable.strings - Core - - Created by Vladimir Chekyrta on 13.09.2022. - -*/ - -"MAINSCREEN.DISCOVERY" = "Всі курси"; -"MAINSCREEN.DASHBOARD" = "Мої курси"; -"MAINSCREEN.IN_DEVELOPING" = "В розробці"; -"MAINSCREEN.PROGRAMS" = "Програми"; -"MAINSCREEN.PROFILE" = "Профіль"; - -"VIEW.SNACKBAR.TRY_AGAIN_BTN" = "Спробувати ще"; - -"ERROR.INVALID_CREDENTIALS" = "Недійсні дані авторизації"; -"ERROR.SLOW_OR_NO_INTERNET_CONNECTION" = "Повільне або відсутнє з’єднання з Інтернетом"; -"ERROR.NO_CACHED_DATA" = "Немає збережених даних для автономного режиму"; -"ERROR.USER_NOT_ACTIVE" = "Обліковий запис користувача не активовано. Будь ласка, спочатку активуйте свій обліковий запис."; -"ERROR.UNKNOWN_ERROR" = "Щось пішло не так"; -"ERROR.WIFI" = "Завантажувати файли можна лише через Wi-Fi. Ви можете змінити це в налаштуваннях."; - -"ERROR.INTERNET.NO_INTERNET_TITLE" = "Немає підключення до Інтернету"; -"ERROR.INTERNET.NO_INTERNET_DESCRIPTION" = "Будь ласка, підключіться до Інтернету, щоб переглянути цей вміст."; - -"COURSEWARE.COURSE_CONTENT" = "Зміст курсу"; -"COURSEWARE.COURSE_CONTENT_NOT_AVAILABLE" = "This interactive component isn't yet available on mobile."; -"COURSEWARE.COURSE_UNITS" = "Модулі"; -"COURSEWARE.NEXT" = "Далі"; -"COURSEWARE.PREVIOUS" = "Назад"; -"COURSEWARE.FINISH" = "Завершити"; -"COURSEWARE.GOOD_WORK" = "Гарна робота!"; -"COURSEWARE.BACK_TO_OUTLINE" = "Повернутись до модуля"; -"COURSEWARE.SECTION_COMPLETED" = "Ви завершили “%@”."; -"COURSEWARE.CONTINUE" = "Продовжити"; -"COURSEWARE.RESUME" = "Resume"; -"COURSEWARE.RESUME_WITH" = "Продовжити далі:"; -"COURSEWARE.NEXT_SECTION" = "Наступний розділ"; - -"COURSEWARE.NEXT_SECTION_DESCRIPTION_FIRST" = "Щоб перейти до “"; -"COURSEWARE.NEXT_SECTION_DESCRIPTION_LAST" = "” натисніть “Наступний розділ”."; - -"ERROR.RELOAD" = "Перезавантажити"; - -"DATE.ENDED" = "Кінець"; -"DATE.START" = "Початок"; -"DATE.STARTED" = "Почався"; -"DATE.JUST_NOW" = "Прямо зараз"; - -"ALERT.ACCEPT" = "ТАК"; -"ALERT.CANCEL" = "СКАСУВАТИ"; -"ALERT.LOGOUT" = "Вийти"; -"ALERT.LEAVE" = "Покинути"; -"ALERT.KEEP_EDITING" = "Залишитись"; -"ALERT.ADD" = "Add"; -"ALERT.REMOVE" = "Remove"; -"ALERT.CALENDAR_SHIFT_PROMPT_REMOVE_COURSE_CALENDAR"="Remove course calendar"; - -"NO_INTERNET.OFFLINE" = "Офлайн режим"; -"NO_INTERNET.DISMISS" = "Сховати"; -"NO_INTERNET.RELOAD" = "Перезавантажити"; - -"DATE_FORMAT.MMMM_dd" = "dd MMMM"; -"DATE_FORMAT.MMM_DD_YYYY" = "dd MMMM yyyy"; - -"DOWNLOAD_MANAGER.DOWNLOAD" = "Скачати"; -"DOWNLOAD_MANAGER.DOWNLOADED" = "Скачано"; -"DOWNLOAD_MANAGER.COMPLETED" = "Завершено"; - -"SETTINGS.VIDEO_DOWNLOAD_QUALITY_TITLE" = "Video download quality"; -"SETTINGS.DOWNLOAD_QUALITY_AUTO_TITLE" = "Auto"; -"SETTINGS.DOWNLOAD_QUALITY_AUTO_DESCRIPTION" = "Recommended"; -"SETTINGS.DOWNLOAD_QUALITY_360_TITLE" = "360p"; -"SETTINGS.DOWNLOAD_QUALITY_360_DESCRIPTION" = "Lower data usage"; -"SETTINGS.DOWNLOAD_QUALITY_540_TITLE" = "540p"; -"SETTINGS.DOWNLOAD_QUALITY_720_TITLE" = "720p"; -"SETTINGS.DOWNLOAD_QUALITY_720_DESCRIPTION" = "Best quality"; - -"DONE" = "Зберегти"; - -"PICKER.SEARCH" = "Знайти"; -"PICKER.ACCEPT" = "Прийняти"; - -"WEBVIEW.ALERT.OK" = "Так"; -"WEBVIEW.ALERT.CANCEL" = "Скасувати"; -"WEBVIEW.ALERT.CONTINUE" = "Continue"; - - -"REVIEW.VOTE_TITLE" = "Вам подобається Open edX?"; -"REVIEW.VOTE_DESCRIPTION" = "Ваш відгук важливий для нас. Можливо, ви візьмете хвилинку, щоб оцінити додаток, натиснувши на зірку нижче? Дякуємо за вашу підтримку!"; -"REVIEW.FEEDBACK_TITLE" = "Залиште відгук"; -"REVIEW.FEEDBACK_DESCRIPTION" = "Нам шкода чути, що ваше навчання мало деякі проблеми. Ми вдячні за будь-який відгук."; -"REVIEW.THANKS_FOR_VOTE_TITLE" = "Дякуємо"; -"REVIEW.THANKS_FOR_VOTE_DESCRIPTION" = "Дякуємо, що поділилися своїми враженнями з нами. Бажаєте залишити свій відгук про цей додаток для інших користувачів в магазині додатків?"; -"REVIEW.THANKS_FOR_FEEDBACK_TITLE" = "Дякуємо"; -"REVIEW.THANKS_FOR_FEEDBACK_DESCRIPTION" = "Ми отримали ваш відгук і використовуватимемо його для покращення вашого навчального досвіду в майбутньому!"; -"REVIEW.BETTER" = "Що можна було б зробити краще?"; -"REVIEW.NOT_NOW" = "Не зараз"; - -"REVIEW.BUTTON.SUBMIT" = "Надіслати"; -"REVIEW.BUTTON.SHARE_FEEDBACK" = "Поділитися відгуком"; -"REVIEW.BUTTON.RATE_US" = "Оцінити нас"; -"REVIEW.EMAIL.TITLE" = "Виберіть поштового клієнта:"; - -"SOCIAL_SIGN_CANCELED" = "The user canceled the sign-in flow."; -"AUTHORIZATION_FAILED" = "Authorization failed."; - -"SIGN_IN.LOG_IN_BTN" = "Увійти"; -"REGISTER" = "Реєстрація"; - -"TOMORROW" = "Tomorrow"; -"YESTERDAY" = "Yesterday"; - -"OPEN_IN_BROWSER"="View in Safari"; diff --git a/Core/CoreTests/Configuration/ConfigTests.swift b/Core/CoreTests/Configuration/ConfigTests.swift index 35a90c2b7..c408e02a5 100644 --- a/Core/CoreTests/Configuration/ConfigTests.swift +++ b/Core/CoreTests/Configuration/ConfigTests.swift @@ -140,11 +140,4 @@ class ConfigTests: XCTestCase { XCTAssertTrue(config.branch.enabled) XCTAssertEqual(config.branch.key, "testBranchKey") } - - func testSegmentConfigInitialization() { - let config = Config(properties: properties) - - XCTAssertTrue(config.segment.enabled) - XCTAssertEqual(config.segment.writeKey, "testSegmentKey") - } } diff --git a/Core/CoreTests/CoreMock.generated.swift b/Core/CoreTests/CoreMock.generated.swift new file mode 100644 index 000000000..261c6be74 --- /dev/null +++ b/Core/CoreTests/CoreMock.generated.swift @@ -0,0 +1,4508 @@ +// Generated using Sourcery 2.1.2 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + + +// Generated with SwiftyMocky 4.2.0 +// Required Sourcery: 1.8.0 + + +import SwiftyMocky +import XCTest +import Core +import Foundation +import SwiftUI +import Combine + + +// MARK: - AuthInteractorProtocol + +open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + @discardableResult + open func login(username: String, password: String) throws -> User { + addInvocation(.m_login__username_usernamepassword_password(Parameter.value(`username`), Parameter.value(`password`))) + let perform = methodPerformValue(.m_login__username_usernamepassword_password(Parameter.value(`username`), Parameter.value(`password`))) as? (String, String) -> Void + perform?(`username`, `password`) + var __value: User + do { + __value = try methodReturnValue(.m_login__username_usernamepassword_password(Parameter.value(`username`), Parameter.value(`password`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(username: String, password: String). Use given") + Failure("Stub return value not specified for login(username: String, password: String). Use given") + } catch { + throw error + } + return __value + } + + @discardableResult + open func login(externalToken: String, backend: String) throws -> User { + addInvocation(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) + let perform = methodPerformValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))) as? (String, String) -> Void + perform?(`externalToken`, `backend`) + var __value: User + do { + __value = try methodReturnValue(.m_login__externalToken_externalTokenbackend_backend(Parameter.value(`externalToken`), Parameter.value(`backend`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + Failure("Stub return value not specified for login(externalToken: String, backend: String). Use given") + } catch { + throw error + } + return __value + } + + open func login(ssoToken: String) throws -> User { + addInvocation(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) + let perform = methodPerformValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) as? (String) -> Void + perform?(`ssoToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(ssoToken: String). Use given") + Failure("Stub return value not specified for login(ssoToken: String). Use given") + } catch { + throw error + } + return __value + } + + open func resetPassword(email: String) throws -> ResetPassword { + addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) + let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void + perform?(`email`) + var __value: ResetPassword + do { + __value = try methodReturnValue(.m_resetPassword__email_email(Parameter.value(`email`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for resetPassword(email: String). Use given") + Failure("Stub return value not specified for resetPassword(email: String). Use given") + } catch { + throw error + } + return __value + } + + open func getCookies(force: Bool) throws { + addInvocation(.m_getCookies__force_force(Parameter.value(`force`))) + let perform = methodPerformValue(.m_getCookies__force_force(Parameter.value(`force`))) as? (Bool) -> Void + perform?(`force`) + do { + _ = try methodReturnValue(.m_getCookies__force_force(Parameter.value(`force`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func getRegistrationFields() throws -> [PickerFields] { + addInvocation(.m_getRegistrationFields) + let perform = methodPerformValue(.m_getRegistrationFields) as? () -> Void + perform?() + var __value: [PickerFields] + do { + __value = try methodReturnValue(.m_getRegistrationFields).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getRegistrationFields(). Use given") + Failure("Stub return value not specified for getRegistrationFields(). Use given") + } catch { + throw error + } + return __value + } + + open func registerUser(fields: [String: String], isSocial: Bool) throws -> User { + addInvocation(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) + let perform = methodPerformValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))) as? ([String: String], Bool) -> Void + perform?(`fields`, `isSocial`) + var __value: User + do { + __value = try methodReturnValue(.m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>.value(`fields`), Parameter.value(`isSocial`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + Failure("Stub return value not specified for registerUser(fields: [String: String], isSocial: Bool). Use given") + } catch { + throw error + } + return __value + } + + open func validateRegistrationFields(fields: [String: String]) throws -> [String: String] { + addInvocation(.m_validateRegistrationFields__fields_fields(Parameter<[String: String]>.value(`fields`))) + let perform = methodPerformValue(.m_validateRegistrationFields__fields_fields(Parameter<[String: String]>.value(`fields`))) as? ([String: String]) -> Void + perform?(`fields`) + var __value: [String: String] + do { + __value = try methodReturnValue(.m_validateRegistrationFields__fields_fields(Parameter<[String: String]>.value(`fields`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for validateRegistrationFields(fields: [String: String]). Use given") + Failure("Stub return value not specified for validateRegistrationFields(fields: [String: String]). Use given") + } catch { + throw error + } + return __value + } + + + fileprivate enum MethodType { + case m_login__username_usernamepassword_password(Parameter, Parameter) + case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) + case m_login__ssoToken_ssoToken(Parameter) + case m_resetPassword__email_email(Parameter) + case m_getCookies__force_force(Parameter) + case m_getRegistrationFields + case m_registerUser__fields_fieldsisSocial_isSocial(Parameter<[String: String]>, Parameter) + case m_validateRegistrationFields__fields_fields(Parameter<[String: String]>) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_login__username_usernamepassword_password(let lhsUsername, let lhsPassword), .m_login__username_usernamepassword_password(let rhsUsername, let rhsPassword)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUsername, rhs: rhsUsername, with: matcher), lhsUsername, rhsUsername, "username")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) + return Matcher.ComparisonResult(results) + + case (.m_login__externalToken_externalTokenbackend_backend(let lhsExternaltoken, let lhsBackend), .m_login__externalToken_externalTokenbackend_backend(let rhsExternaltoken, let rhsBackend)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsExternaltoken, rhs: rhsExternaltoken, with: matcher), lhsExternaltoken, rhsExternaltoken, "externalToken")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) + return Matcher.ComparisonResult(results) + + case (.m_login__ssoToken_ssoToken(let lhsSsotoken), .m_login__ssoToken_ssoToken(let rhsSsotoken)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSsotoken, rhs: rhsSsotoken, with: matcher), lhsSsotoken, rhsSsotoken, "ssoToken")) + return Matcher.ComparisonResult(results) + + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) + return Matcher.ComparisonResult(results) + + case (.m_getCookies__force_force(let lhsForce), .m_getCookies__force_force(let rhsForce)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) + return Matcher.ComparisonResult(results) + + case (.m_getRegistrationFields, .m_getRegistrationFields): return .match + + case (.m_registerUser__fields_fieldsisSocial_isSocial(let lhsFields, let lhsIssocial), .m_registerUser__fields_fieldsisSocial_isSocial(let rhsFields, let rhsIssocial)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsIssocial, rhs: rhsIssocial, with: matcher), lhsIssocial, rhsIssocial, "isSocial")) + return Matcher.ComparisonResult(results) + + case (.m_validateRegistrationFields__fields_fields(let lhsFields), .m_validateRegistrationFields__fields_fields(let rhsFields)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFields, rhs: rhsFields, with: matcher), lhsFields, rhsFields, "fields")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue + case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue + case let .m_login__ssoToken_ssoToken(p0): return p0.intValue + case let .m_resetPassword__email_email(p0): return p0.intValue + case let .m_getCookies__force_force(p0): return p0.intValue + case .m_getRegistrationFields: return 0 + case let .m_registerUser__fields_fieldsisSocial_isSocial(p0, p1): return p0.intValue + p1.intValue + case let .m_validateRegistrationFields__fields_fields(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_login__username_usernamepassword_password: return ".login(username:password:)" + case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" + case .m_login__ssoToken_ssoToken: return ".login(ssoToken:)" + case .m_resetPassword__email_email: return ".resetPassword(email:)" + case .m_getCookies__force_force: return ".getCookies(force:)" + case .m_getRegistrationFields: return ".getRegistrationFields()" + case .m_registerUser__fields_fieldsisSocial_isSocial: return ".registerUser(fields:isSocial:)" + case .m_validateRegistrationFields__fields_fields: return ".validateRegistrationFields(fields:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + @discardableResult + public static func login(username: Parameter, password: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func login(ssoToken: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { + return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getRegistrationFields(willReturn: [PickerFields]...) -> MethodStub { + return Given(method: .m_getRegistrationFields, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func validateRegistrationFields(fields: Parameter<[String: String]>, willReturn: [String: String]...) -> MethodStub { + return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + @discardableResult + public static func login(username: Parameter, password: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(username: Parameter, password: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__username_usernamepassword_password(`username`, `password`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } + public static func login(ssoToken: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func login(ssoToken: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } + public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func resetPassword(email: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (ResetPassword).self) + willProduce(stubber) + return given + } + public static func getCookies(force: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getCookies__force_force(`force`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getCookies(force: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getCookies__force_force(`force`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func getRegistrationFields(willThrow: Error...) -> MethodStub { + return Given(method: .m_getRegistrationFields, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getRegistrationFields(willProduce: (StubberThrows<[PickerFields]>) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getRegistrationFields, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: ([PickerFields]).self) + willProduce(stubber) + return given + } + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } + public static func validateRegistrationFields(fields: Parameter<[String: String]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func validateRegistrationFields(fields: Parameter<[String: String]>, willProduce: (StubberThrows<[String: String]>) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_validateRegistrationFields__fields_fields(`fields`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: ([String: String]).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + @discardableResult + public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} + public static func login(ssoToken: Parameter) -> Verify { return Verify(method: .m_login__ssoToken_ssoToken(`ssoToken`))} + public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} + public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} + public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter) -> Verify { return Verify(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`))} + public static func validateRegistrationFields(fields: Parameter<[String: String]>) -> Verify { return Verify(method: .m_validateRegistrationFields__fields_fields(`fields`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + @discardableResult + public static func login(username: Parameter, password: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__username_usernamepassword_password(`username`, `password`), performs: perform) + } + @discardableResult + public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) + } + public static func login(ssoToken: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_login__ssoToken_ssoToken(`ssoToken`), performs: perform) + } + public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) + } + public static func getCookies(force: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_getCookies__force_force(`force`), performs: perform) + } + public static func getRegistrationFields(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getRegistrationFields, performs: perform) + } + public static func registerUser(fields: Parameter<[String: String]>, isSocial: Parameter, perform: @escaping ([String: String], Bool) -> Void) -> Perform { + return Perform(method: .m_registerUser__fields_fieldsisSocial_isSocial(`fields`, `isSocial`), performs: perform) + } + public static func validateRegistrationFields(fields: Parameter<[String: String]>, perform: @escaping ([String: String]) -> Void) -> Perform { + return Perform(method: .m_validateRegistrationFields__fields_fields(`fields`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - BaseRouter + +open class BaseRouterMock: BaseRouter, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func backToRoot(animated: Bool) { + addInvocation(.m_backToRoot__animated_animated(Parameter.value(`animated`))) + let perform = methodPerformValue(.m_backToRoot__animated_animated(Parameter.value(`animated`))) as? (Bool) -> Void + perform?(`animated`) + } + + open func back(animated: Bool) { + addInvocation(.m_back__animated_animated(Parameter.value(`animated`))) + let perform = methodPerformValue(.m_back__animated_animated(Parameter.value(`animated`))) as? (Bool) -> Void + perform?(`animated`) + } + + open func backWithFade() { + addInvocation(.m_backWithFade) + let perform = methodPerformValue(.m_backWithFade) as? () -> Void + perform?() + } + + open func dismiss(animated: Bool) { + addInvocation(.m_dismiss__animated_animated(Parameter.value(`animated`))) + let perform = methodPerformValue(.m_dismiss__animated_animated(Parameter.value(`animated`))) as? (Bool) -> Void + perform?(`animated`) + } + + open func removeLastView(controllers: Int) { + addInvocation(.m_removeLastView__controllers_controllers(Parameter.value(`controllers`))) + let perform = methodPerformValue(.m_removeLastView__controllers_controllers(Parameter.value(`controllers`))) as? (Int) -> Void + perform?(`controllers`) + } + + open func showMainOrWhatsNewScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showStartupScreen() { + addInvocation(.m_showStartupScreen) + let perform = methodPerformValue(.m_showStartupScreen) as? () -> Void + perform?() + } + + open func showLoginScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showLoginScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showRegisterScreen(sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showRegisterScreen__sourceScreen_sourceScreen(Parameter.value(`sourceScreen`))) as? (LogistrationSourceScreen) -> Void + perform?(`sourceScreen`) + } + + open func showForgotPasswordScreen() { + addInvocation(.m_showForgotPasswordScreen) + let perform = methodPerformValue(.m_showForgotPasswordScreen) as? () -> Void + perform?() + } + + open func showDiscoveryScreen(searchQuery: String?, sourceScreen: LogistrationSourceScreen) { + addInvocation(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) + let perform = methodPerformValue(.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter.value(`searchQuery`), Parameter.value(`sourceScreen`))) as? (String?, LogistrationSourceScreen) -> Void + perform?(`searchQuery`, `sourceScreen`) + } + + open func showWebBrowser(title: String, url: URL) { + addInvocation(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) + let perform = methodPerformValue(.m_showWebBrowser__title_titleurl_url(Parameter.value(`title`), Parameter.value(`url`))) as? (String, URL) -> Void + perform?(`title`, `url`) + } + + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void + perform?(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`) + } + + open func presentAlert(alertTitle: String, alertMessage: String, nextSectionName: String?, action: String, image: SwiftUI.Image, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, nextSectionTapped: @escaping () -> Void) { + addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) + let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`nextSectionName`), Parameter.value(`action`), Parameter.value(`image`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter<() -> Void>.value(`nextSectionTapped`))) as? (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void + perform?(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`) + } + + open func presentView(transitionStyle: UIModalTransitionStyle, view: any View, completion: (() -> Void)?) { + addInvocation(.m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter.value(`transitionStyle`), Parameter.value(`view`), Parameter<(() -> Void)?>.value(`completion`))) + let perform = methodPerformValue(.m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter.value(`transitionStyle`), Parameter.value(`view`), Parameter<(() -> Void)?>.value(`completion`))) as? (UIModalTransitionStyle, any View, (() -> Void)?) -> Void + perform?(`transitionStyle`, `view`, `completion`) + } + + open func presentView(transitionStyle: UIModalTransitionStyle, animated: Bool, content: () -> any View) { + addInvocation(.m_presentView__transitionStyle_transitionStyleanimated_animatedcontent_content(Parameter.value(`transitionStyle`), Parameter.value(`animated`), Parameter<() -> any View>.any)) + let perform = methodPerformValue(.m_presentView__transitionStyle_transitionStyleanimated_animatedcontent_content(Parameter.value(`transitionStyle`), Parameter.value(`animated`), Parameter<() -> any View>.any)) as? (UIModalTransitionStyle, Bool, () -> any View) -> Void + perform?(`transitionStyle`, `animated`, `content`) + } + + + fileprivate enum MethodType { + case m_backToRoot__animated_animated(Parameter) + case m_back__animated_animated(Parameter) + case m_backWithFade + case m_dismiss__animated_animated(Parameter) + case m_removeLastView__controllers_controllers(Parameter) + case m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(Parameter) + case m_showStartupScreen + case m_showLoginScreen__sourceScreen_sourceScreen(Parameter) + case m_showRegisterScreen__sourceScreen_sourceScreen(Parameter) + case m_showForgotPasswordScreen + case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) + case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showSSOWebBrowser__title_title(Parameter) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) + case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) + case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) + case m_presentView__transitionStyle_transitionStyleanimated_animatedcontent_content(Parameter, Parameter, Parameter<() -> any View>) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_backToRoot__animated_animated(let lhsAnimated), .m_backToRoot__animated_animated(let rhsAnimated)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAnimated, rhs: rhsAnimated, with: matcher), lhsAnimated, rhsAnimated, "animated")) + return Matcher.ComparisonResult(results) + + case (.m_back__animated_animated(let lhsAnimated), .m_back__animated_animated(let rhsAnimated)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAnimated, rhs: rhsAnimated, with: matcher), lhsAnimated, rhsAnimated, "animated")) + return Matcher.ComparisonResult(results) + + case (.m_backWithFade, .m_backWithFade): return .match + + case (.m_dismiss__animated_animated(let lhsAnimated), .m_dismiss__animated_animated(let rhsAnimated)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAnimated, rhs: rhsAnimated, with: matcher), lhsAnimated, rhsAnimated, "animated")) + return Matcher.ComparisonResult(results) + + case (.m_removeLastView__controllers_controllers(let lhsControllers), .m_removeLastView__controllers_controllers(let rhsControllers)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsControllers, rhs: rhsControllers, with: matcher), lhsControllers, rhsControllers, "controllers")) + return Matcher.ComparisonResult(results) + + case (.m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showStartupScreen, .m_showStartupScreen): return .match + + case (.m_showLoginScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showLoginScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showRegisterScreen__sourceScreen_sourceScreen(let lhsSourcescreen), .m_showRegisterScreen__sourceScreen_sourceScreen(let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showForgotPasswordScreen, .m_showForgotPasswordScreen): return .match + + case (.m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let lhsSearchquery, let lhsSourcescreen), .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(let rhsSearchquery, let rhsSourcescreen)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSearchquery, rhs: rhsSearchquery, with: matcher), lhsSearchquery, rhsSearchquery, "searchQuery")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSourcescreen, rhs: rhsSourcescreen, with: matcher), lhsSourcescreen, rhsSourcescreen, "sourceScreen")) + return Matcher.ComparisonResult(results) + + case (.m_showWebBrowser__title_titleurl_url(let lhsTitle, let lhsUrl), .m_showWebBrowser__title_titleurl_url(let rhsTitle, let rhsUrl)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) + return Matcher.ComparisonResult(results) + + case (.m_showSSOWebBrowser__title_title(let lhsTitle), .m_showSSOWebBrowser__title_title(let rhsTitle)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + return Matcher.ComparisonResult(results) + + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPositiveaction, rhs: rhsPositiveaction, with: matcher), lhsPositiveaction, rhsPositiveaction, "positiveAction")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsType, rhs: rhsType, with: matcher), lhsType, rhsType, "type")) + return Matcher.ComparisonResult(results) + + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let lhsAlerttitle, let lhsAlertmessage, let lhsNextsectionname, let lhsAction, let lhsImage, let lhsOnclosetapped, let lhsOktapped, let lhsNextsectiontapped), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(let rhsAlerttitle, let rhsAlertmessage, let rhsNextsectionname, let rhsAction, let rhsImage, let rhsOnclosetapped, let rhsOktapped, let rhsNextsectiontapped)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlertmessage, rhs: rhsAlertmessage, with: matcher), lhsAlertmessage, rhsAlertmessage, "alertMessage")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectionname, rhs: rhsNextsectionname, with: matcher), lhsNextsectionname, rhsNextsectionname, "nextSectionName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsImage, rhs: rhsImage, with: matcher), lhsImage, rhsImage, "image")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOnclosetapped, rhs: rhsOnclosetapped, with: matcher), lhsOnclosetapped, rhsOnclosetapped, "onCloseTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOktapped, rhs: rhsOktapped, with: matcher), lhsOktapped, rhsOktapped, "okTapped")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsNextsectiontapped, rhs: rhsNextsectiontapped, with: matcher), lhsNextsectiontapped, rhsNextsectiontapped, "nextSectionTapped")) + return Matcher.ComparisonResult(results) + + case (.m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(let lhsTransitionstyle, let lhsView, let lhsCompletion), .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(let rhsTransitionstyle, let rhsView, let rhsCompletion)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTransitionstyle, rhs: rhsTransitionstyle, with: matcher), lhsTransitionstyle, rhsTransitionstyle, "transitionStyle")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsView, rhs: rhsView, with: matcher), lhsView, rhsView, "view")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCompletion, rhs: rhsCompletion, with: matcher), lhsCompletion, rhsCompletion, "completion")) + return Matcher.ComparisonResult(results) + + case (.m_presentView__transitionStyle_transitionStyleanimated_animatedcontent_content(let lhsTransitionstyle, let lhsAnimated, let lhsContent), .m_presentView__transitionStyle_transitionStyleanimated_animatedcontent_content(let rhsTransitionstyle, let rhsAnimated, let rhsContent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTransitionstyle, rhs: rhsTransitionstyle, with: matcher), lhsTransitionstyle, rhsTransitionstyle, "transitionStyle")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAnimated, rhs: rhsAnimated, with: matcher), lhsAnimated, rhsAnimated, "animated")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsContent, rhs: rhsContent, with: matcher), lhsContent, rhsContent, "content")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_backToRoot__animated_animated(p0): return p0.intValue + case let .m_back__animated_animated(p0): return p0.intValue + case .m_backWithFade: return 0 + case let .m_dismiss__animated_animated(p0): return p0.intValue + case let .m_removeLastView__controllers_controllers(p0): return p0.intValue + case let .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showStartupScreen: return 0 + case let .m_showLoginScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case let .m_showRegisterScreen__sourceScreen_sourceScreen(p0): return p0.intValue + case .m_showForgotPasswordScreen: return 0 + case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue + case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showSSOWebBrowser__title_title(p0): return p0.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue + case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_presentView__transitionStyle_transitionStyleanimated_animatedcontent_content(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + } + } + func assertionName() -> String { + switch self { + case .m_backToRoot__animated_animated: return ".backToRoot(animated:)" + case .m_back__animated_animated: return ".back(animated:)" + case .m_backWithFade: return ".backWithFade()" + case .m_dismiss__animated_animated: return ".dismiss(animated:)" + case .m_removeLastView__controllers_controllers: return ".removeLastView(controllers:)" + case .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen: return ".showMainOrWhatsNewScreen(sourceScreen:)" + case .m_showStartupScreen: return ".showStartupScreen()" + case .m_showLoginScreen__sourceScreen_sourceScreen: return ".showLoginScreen(sourceScreen:)" + case .m_showRegisterScreen__sourceScreen_sourceScreen: return ".showRegisterScreen(sourceScreen:)" + case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" + case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" + case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showSSOWebBrowser__title_title: return ".showSSOWebBrowser(title:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" + case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" + case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" + case .m_presentView__transitionStyle_transitionStyleanimated_animatedcontent_content: return ".presentView(transitionStyle:animated:content:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func backToRoot(animated: Parameter) -> Verify { return Verify(method: .m_backToRoot__animated_animated(`animated`))} + public static func back(animated: Parameter) -> Verify { return Verify(method: .m_back__animated_animated(`animated`))} + public static func backWithFade() -> Verify { return Verify(method: .m_backWithFade)} + public static func dismiss(animated: Parameter) -> Verify { return Verify(method: .m_dismiss__animated_animated(`animated`))} + public static func removeLastView(controllers: Parameter) -> Verify { return Verify(method: .m_removeLastView__controllers_controllers(`controllers`))} + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showStartupScreen() -> Verify { return Verify(method: .m_showStartupScreen)} + public static func showLoginScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showRegisterScreen(sourceScreen: Parameter) -> Verify { return Verify(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`))} + public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} + public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} + public static func showSSOWebBrowser(title: Parameter) -> Verify { return Verify(method: .m_showSSOWebBrowser__title_title(`title`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} + public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`))} + public static func presentView(transitionStyle: Parameter, animated: Parameter, content: Parameter<() -> any View>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleanimated_animatedcontent_content(`transitionStyle`, `animated`, `content`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func backToRoot(animated: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_backToRoot__animated_animated(`animated`), performs: perform) + } + public static func back(animated: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_back__animated_animated(`animated`), performs: perform) + } + public static func backWithFade(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_backWithFade, performs: perform) + } + public static func dismiss(animated: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_dismiss__animated_animated(`animated`), performs: perform) + } + public static func removeLastView(controllers: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_removeLastView__controllers_controllers(`controllers`), performs: perform) + } + public static func showMainOrWhatsNewScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showMainOrWhatsNewScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showStartupScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showStartupScreen, performs: perform) + } + public static func showLoginScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showLoginScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showRegisterScreen(sourceScreen: Parameter, perform: @escaping (LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showRegisterScreen__sourceScreen_sourceScreen(`sourceScreen`), performs: perform) + } + public static func showForgotPasswordScreen(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showForgotPasswordScreen, performs: perform) + } + public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter, perform: @escaping (String?, LogistrationSourceScreen) -> Void) -> Perform { + return Perform(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`), performs: perform) + } + public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { + return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) + } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showSSOWebBrowser__title_title(`title`), performs: perform) + } + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) + } + public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>, perform: @escaping (String, String, String?, String, SwiftUI.Image, @escaping () -> Void, @escaping () -> Void, @escaping () -> Void) -> Void) -> Perform { + return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`), performs: perform) + } + public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>, perform: @escaping (UIModalTransitionStyle, any View, (() -> Void)?) -> Void) -> Perform { + return Perform(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`), performs: perform) + } + public static func presentView(transitionStyle: Parameter, animated: Parameter, content: Parameter<() -> any View>, perform: @escaping (UIModalTransitionStyle, Bool, () -> any View) -> Void) -> Perform { + return Perform(method: .m_presentView__transitionStyle_transitionStyleanimated_animatedcontent_content(`transitionStyle`, `animated`, `content`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CalendarManagerProtocol + +open class CalendarManagerProtocolMock: CalendarManagerProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func createCalendarIfNeeded() { + addInvocation(.m_createCalendarIfNeeded) + let perform = methodPerformValue(.m_createCalendarIfNeeded) as? () -> Void + perform?() + } + + open func filterCoursesBySelected(fetchedCourses: [CourseForSync]) -> [CourseForSync] { + addInvocation(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) + let perform = methodPerformValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) as? ([CourseForSync]) -> Void + perform?(`fetchedCourses`) + var __value: [CourseForSync] + do { + __value = try methodReturnValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))).casted() + } catch { + onFatalFailure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + Failure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + } + return __value + } + + open func removeOldCalendar() { + addInvocation(.m_removeOldCalendar) + let perform = methodPerformValue(.m_removeOldCalendar) as? () -> Void + perform?() + } + + open func removeOutdatedEvents(courseID: String) { + addInvocation(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + } + + open func syncCourse(courseID: String, courseName: String, dates: CourseDates) { + addInvocation(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) + let perform = methodPerformValue(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) as? (String, String, CourseDates) -> Void + perform?(`courseID`, `courseName`, `dates`) + } + + open func requestAccess() -> Bool { + addInvocation(.m_requestAccess) + let perform = methodPerformValue(.m_requestAccess) as? () -> Void + perform?() + var __value: Bool + do { + __value = try methodReturnValue(.m_requestAccess).casted() + } catch { + onFatalFailure("Stub return value not specified for requestAccess(). Use given") + Failure("Stub return value not specified for requestAccess(). Use given") + } + return __value + } + + open func courseStatus(courseID: String) -> SyncStatus { + addInvocation(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: SyncStatus + do { + __value = try methodReturnValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch { + onFatalFailure("Stub return value not specified for courseStatus(courseID: String). Use given") + Failure("Stub return value not specified for courseStatus(courseID: String). Use given") + } + return __value + } + + open func clearAllData(removeCalendar: Bool) { + addInvocation(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) + let perform = methodPerformValue(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) as? (Bool) -> Void + perform?(`removeCalendar`) + } + + open func isDatesChanged(courseID: String, checksum: String) -> Bool { + addInvocation(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) + let perform = methodPerformValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) as? (String, String) -> Void + perform?(`courseID`, `checksum`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + Failure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + } + return __value + } + + + fileprivate enum MethodType { + case m_createCalendarIfNeeded + case m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>) + case m_removeOldCalendar + case m_removeOutdatedEvents__courseID_courseID(Parameter) + case m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter, Parameter, Parameter) + case m_requestAccess + case m_courseStatus__courseID_courseID(Parameter) + case m_clearAllData__removeCalendar_removeCalendar(Parameter) + case m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_createCalendarIfNeeded, .m_createCalendarIfNeeded): return .match + + case (.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let lhsFetchedcourses), .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let rhsFetchedcourses)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFetchedcourses, rhs: rhsFetchedcourses, with: matcher), lhsFetchedcourses, rhsFetchedcourses, "fetchedCourses")) + return Matcher.ComparisonResult(results) + + case (.m_removeOldCalendar, .m_removeOldCalendar): return .match + + case (.m_removeOutdatedEvents__courseID_courseID(let lhsCourseid), .m_removeOutdatedEvents__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let lhsCourseid, let lhsCoursename, let lhsDates), .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let rhsCourseid, let rhsCoursename, let rhsDates)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDates, rhs: rhsDates, with: matcher), lhsDates, rhsDates, "dates")) + return Matcher.ComparisonResult(results) + + case (.m_requestAccess, .m_requestAccess): return .match + + case (.m_courseStatus__courseID_courseID(let lhsCourseid), .m_courseStatus__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_clearAllData__removeCalendar_removeCalendar(let lhsRemovecalendar), .m_clearAllData__removeCalendar_removeCalendar(let rhsRemovecalendar)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRemovecalendar, rhs: rhsRemovecalendar, with: matcher), lhsRemovecalendar, rhsRemovecalendar, "removeCalendar")) + return Matcher.ComparisonResult(results) + + case (.m_isDatesChanged__courseID_courseIDchecksum_checksum(let lhsCourseid, let lhsChecksum), .m_isDatesChanged__courseID_courseIDchecksum_checksum(let rhsCourseid, let rhsChecksum)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsChecksum, rhs: rhsChecksum, with: matcher), lhsChecksum, rhsChecksum, "checksum")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .m_createCalendarIfNeeded: return 0 + case let .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(p0): return p0.intValue + case .m_removeOldCalendar: return 0 + case let .m_removeOutdatedEvents__courseID_courseID(p0): return p0.intValue + case let .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case .m_requestAccess: return 0 + case let .m_courseStatus__courseID_courseID(p0): return p0.intValue + case let .m_clearAllData__removeCalendar_removeCalendar(p0): return p0.intValue + case let .m_isDatesChanged__courseID_courseIDchecksum_checksum(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_createCalendarIfNeeded: return ".createCalendarIfNeeded()" + case .m_filterCoursesBySelected__fetchedCourses_fetchedCourses: return ".filterCoursesBySelected(fetchedCourses:)" + case .m_removeOldCalendar: return ".removeOldCalendar()" + case .m_removeOutdatedEvents__courseID_courseID: return ".removeOutdatedEvents(courseID:)" + case .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates: return ".syncCourse(courseID:courseName:dates:)" + case .m_requestAccess: return ".requestAccess()" + case .m_courseStatus__courseID_courseID: return ".courseStatus(courseID:)" + case .m_clearAllData__removeCalendar_removeCalendar: return ".clearAllData(removeCalendar:)" + case .m_isDatesChanged__courseID_courseIDchecksum_checksum: return ".isDatesChanged(courseID:checksum:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willReturn: [CourseForSync]...) -> MethodStub { + return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func requestAccess(willReturn: Bool...) -> MethodStub { + return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func courseStatus(courseID: Parameter, willReturn: SyncStatus...) -> MethodStub { + return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willProduce: (Stubber<[CourseForSync]>) -> Void) -> MethodStub { + let willReturn: [[CourseForSync]] = [] + let given: Given = { return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseForSync]).self) + willProduce(stubber) + return given + } + public static func requestAccess(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given + } + public static func courseStatus(courseID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [SyncStatus] = [] + let given: Given = { return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (SyncStatus).self) + willProduce(stubber) + return given + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func createCalendarIfNeeded() -> Verify { return Verify(method: .m_createCalendarIfNeeded)} + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>) -> Verify { return Verify(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`))} + public static func removeOldCalendar() -> Verify { return Verify(method: .m_removeOldCalendar)} + public static func removeOutdatedEvents(courseID: Parameter) -> Verify { return Verify(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`))} + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter) -> Verify { return Verify(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`))} + public static func requestAccess() -> Verify { return Verify(method: .m_requestAccess)} + public static func courseStatus(courseID: Parameter) -> Verify { return Verify(method: .m_courseStatus__courseID_courseID(`courseID`))} + public static func clearAllData(removeCalendar: Parameter) -> Verify { return Verify(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`))} + public static func isDatesChanged(courseID: Parameter, checksum: Parameter) -> Verify { return Verify(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func createCalendarIfNeeded(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_createCalendarIfNeeded, performs: perform) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, perform: @escaping ([CourseForSync]) -> Void) -> Perform { + return Perform(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), performs: perform) + } + public static func removeOldCalendar(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeOldCalendar, performs: perform) + } + public static func removeOutdatedEvents(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`), performs: perform) + } + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter, perform: @escaping (String, String, CourseDates) -> Void) -> Perform { + return Perform(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`), performs: perform) + } + public static func requestAccess(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_requestAccess, performs: perform) + } + public static func courseStatus(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_courseStatus__courseID_courseID(`courseID`), performs: perform) + } + public static func clearAllData(removeCalendar: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`), performs: perform) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - ConfigProtocol + +open class ConfigProtocolMock: ConfigProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var baseURL: URL { + get { invocations.append(.p_baseURL_get); return __p_baseURL ?? givenGetterValue(.p_baseURL_get, "ConfigProtocolMock - stub value for baseURL was not defined") } + } + private var __p_baseURL: (URL)? + + public var baseSSOURL: URL { + get { invocations.append(.p_baseSSOURL_get); return __p_baseSSOURL ?? givenGetterValue(.p_baseSSOURL_get, "ConfigProtocolMock - stub value for baseSSOURL was not defined") } + } + private var __p_baseSSOURL: (URL)? + + public var ssoFinishedURL: URL { + get { invocations.append(.p_ssoFinishedURL_get); return __p_ssoFinishedURL ?? givenGetterValue(.p_ssoFinishedURL_get, "ConfigProtocolMock - stub value for ssoFinishedURL was not defined") } + } + private var __p_ssoFinishedURL: (URL)? + + public var ssoButtonTitle: [String: Any] { + get { invocations.append(.p_ssoButtonTitle_get); return __p_ssoButtonTitle ?? givenGetterValue(.p_ssoButtonTitle_get, "ConfigProtocolMock - stub value for ssoButtonTitle was not defined") } + } + private var __p_ssoButtonTitle: ([String: Any])? + + public var oAuthClientId: String { + get { invocations.append(.p_oAuthClientId_get); return __p_oAuthClientId ?? givenGetterValue(.p_oAuthClientId_get, "ConfigProtocolMock - stub value for oAuthClientId was not defined") } + } + private var __p_oAuthClientId: (String)? + + public var tokenType: TokenType { + get { invocations.append(.p_tokenType_get); return __p_tokenType ?? givenGetterValue(.p_tokenType_get, "ConfigProtocolMock - stub value for tokenType was not defined") } + } + private var __p_tokenType: (TokenType)? + + public var feedbackEmail: String { + get { invocations.append(.p_feedbackEmail_get); return __p_feedbackEmail ?? givenGetterValue(.p_feedbackEmail_get, "ConfigProtocolMock - stub value for feedbackEmail was not defined") } + } + private var __p_feedbackEmail: (String)? + + public var appStoreLink: String { + get { invocations.append(.p_appStoreLink_get); return __p_appStoreLink ?? givenGetterValue(.p_appStoreLink_get, "ConfigProtocolMock - stub value for appStoreLink was not defined") } + } + private var __p_appStoreLink: (String)? + + public var faq: URL? { + get { invocations.append(.p_faq_get); return __p_faq ?? optionalGivenGetterValue(.p_faq_get, "ConfigProtocolMock - stub value for faq was not defined") } + } + private var __p_faq: (URL)? + + public var platformName: String { + get { invocations.append(.p_platformName_get); return __p_platformName ?? givenGetterValue(.p_platformName_get, "ConfigProtocolMock - stub value for platformName was not defined") } + } + private var __p_platformName: (String)? + + public var agreement: AgreementConfig { + get { invocations.append(.p_agreement_get); return __p_agreement ?? givenGetterValue(.p_agreement_get, "ConfigProtocolMock - stub value for agreement was not defined") } + } + private var __p_agreement: (AgreementConfig)? + + public var firebase: FirebaseConfig { + get { invocations.append(.p_firebase_get); return __p_firebase ?? givenGetterValue(.p_firebase_get, "ConfigProtocolMock - stub value for firebase was not defined") } + } + private var __p_firebase: (FirebaseConfig)? + + public var facebook: FacebookConfig { + get { invocations.append(.p_facebook_get); return __p_facebook ?? givenGetterValue(.p_facebook_get, "ConfigProtocolMock - stub value for facebook was not defined") } + } + private var __p_facebook: (FacebookConfig)? + + public var microsoft: MicrosoftConfig { + get { invocations.append(.p_microsoft_get); return __p_microsoft ?? givenGetterValue(.p_microsoft_get, "ConfigProtocolMock - stub value for microsoft was not defined") } + } + private var __p_microsoft: (MicrosoftConfig)? + + public var google: GoogleConfig { + get { invocations.append(.p_google_get); return __p_google ?? givenGetterValue(.p_google_get, "ConfigProtocolMock - stub value for google was not defined") } + } + private var __p_google: (GoogleConfig)? + + public var appleSignIn: AppleSignInConfig { + get { invocations.append(.p_appleSignIn_get); return __p_appleSignIn ?? givenGetterValue(.p_appleSignIn_get, "ConfigProtocolMock - stub value for appleSignIn was not defined") } + } + private var __p_appleSignIn: (AppleSignInConfig)? + + public var features: FeaturesConfig { + get { invocations.append(.p_features_get); return __p_features ?? givenGetterValue(.p_features_get, "ConfigProtocolMock - stub value for features was not defined") } + } + private var __p_features: (FeaturesConfig)? + + public var theme: ThemeConfig { + get { invocations.append(.p_theme_get); return __p_theme ?? givenGetterValue(.p_theme_get, "ConfigProtocolMock - stub value for theme was not defined") } + } + private var __p_theme: (ThemeConfig)? + + public var uiComponents: UIComponentsConfig { + get { invocations.append(.p_uiComponents_get); return __p_uiComponents ?? givenGetterValue(.p_uiComponents_get, "ConfigProtocolMock - stub value for uiComponents was not defined") } + } + private var __p_uiComponents: (UIComponentsConfig)? + + public var discovery: DiscoveryConfig { + get { invocations.append(.p_discovery_get); return __p_discovery ?? givenGetterValue(.p_discovery_get, "ConfigProtocolMock - stub value for discovery was not defined") } + } + private var __p_discovery: (DiscoveryConfig)? + + public var dashboard: DashboardConfig { + get { invocations.append(.p_dashboard_get); return __p_dashboard ?? givenGetterValue(.p_dashboard_get, "ConfigProtocolMock - stub value for dashboard was not defined") } + } + private var __p_dashboard: (DashboardConfig)? + + public var braze: BrazeConfig { + get { invocations.append(.p_braze_get); return __p_braze ?? givenGetterValue(.p_braze_get, "ConfigProtocolMock - stub value for braze was not defined") } + } + private var __p_braze: (BrazeConfig)? + + public var branch: BranchConfig { + get { invocations.append(.p_branch_get); return __p_branch ?? givenGetterValue(.p_branch_get, "ConfigProtocolMock - stub value for branch was not defined") } + } + private var __p_branch: (BranchConfig)? + + public var program: DiscoveryConfig { + get { invocations.append(.p_program_get); return __p_program ?? givenGetterValue(.p_program_get, "ConfigProtocolMock - stub value for program was not defined") } + } + private var __p_program: (DiscoveryConfig)? + + public var URIScheme: String { + get { invocations.append(.p_URIScheme_get); return __p_URIScheme ?? givenGetterValue(.p_URIScheme_get, "ConfigProtocolMock - stub value for URIScheme was not defined") } + } + private var __p_URIScheme: (String)? + + + + + + + fileprivate enum MethodType { + case p_baseURL_get + case p_baseSSOURL_get + case p_ssoFinishedURL_get + case p_ssoButtonTitle_get + case p_oAuthClientId_get + case p_tokenType_get + case p_feedbackEmail_get + case p_appStoreLink_get + case p_faq_get + case p_platformName_get + case p_agreement_get + case p_firebase_get + case p_facebook_get + case p_microsoft_get + case p_google_get + case p_appleSignIn_get + case p_features_get + case p_theme_get + case p_uiComponents_get + case p_discovery_get + case p_dashboard_get + case p_braze_get + case p_branch_get + case p_program_get + case p_URIScheme_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_baseURL_get,.p_baseURL_get): return Matcher.ComparisonResult.match + case (.p_baseSSOURL_get,.p_baseSSOURL_get): return Matcher.ComparisonResult.match + case (.p_ssoFinishedURL_get,.p_ssoFinishedURL_get): return Matcher.ComparisonResult.match + case (.p_ssoButtonTitle_get,.p_ssoButtonTitle_get): return Matcher.ComparisonResult.match + case (.p_oAuthClientId_get,.p_oAuthClientId_get): return Matcher.ComparisonResult.match + case (.p_tokenType_get,.p_tokenType_get): return Matcher.ComparisonResult.match + case (.p_feedbackEmail_get,.p_feedbackEmail_get): return Matcher.ComparisonResult.match + case (.p_appStoreLink_get,.p_appStoreLink_get): return Matcher.ComparisonResult.match + case (.p_faq_get,.p_faq_get): return Matcher.ComparisonResult.match + case (.p_platformName_get,.p_platformName_get): return Matcher.ComparisonResult.match + case (.p_agreement_get,.p_agreement_get): return Matcher.ComparisonResult.match + case (.p_firebase_get,.p_firebase_get): return Matcher.ComparisonResult.match + case (.p_facebook_get,.p_facebook_get): return Matcher.ComparisonResult.match + case (.p_microsoft_get,.p_microsoft_get): return Matcher.ComparisonResult.match + case (.p_google_get,.p_google_get): return Matcher.ComparisonResult.match + case (.p_appleSignIn_get,.p_appleSignIn_get): return Matcher.ComparisonResult.match + case (.p_features_get,.p_features_get): return Matcher.ComparisonResult.match + case (.p_theme_get,.p_theme_get): return Matcher.ComparisonResult.match + case (.p_uiComponents_get,.p_uiComponents_get): return Matcher.ComparisonResult.match + case (.p_discovery_get,.p_discovery_get): return Matcher.ComparisonResult.match + case (.p_dashboard_get,.p_dashboard_get): return Matcher.ComparisonResult.match + case (.p_braze_get,.p_braze_get): return Matcher.ComparisonResult.match + case (.p_branch_get,.p_branch_get): return Matcher.ComparisonResult.match + case (.p_program_get,.p_program_get): return Matcher.ComparisonResult.match + case (.p_URIScheme_get,.p_URIScheme_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_baseURL_get: return 0 + case .p_baseSSOURL_get: return 0 + case .p_ssoFinishedURL_get: return 0 + case .p_ssoButtonTitle_get: return 0 + case .p_oAuthClientId_get: return 0 + case .p_tokenType_get: return 0 + case .p_feedbackEmail_get: return 0 + case .p_appStoreLink_get: return 0 + case .p_faq_get: return 0 + case .p_platformName_get: return 0 + case .p_agreement_get: return 0 + case .p_firebase_get: return 0 + case .p_facebook_get: return 0 + case .p_microsoft_get: return 0 + case .p_google_get: return 0 + case .p_appleSignIn_get: return 0 + case .p_features_get: return 0 + case .p_theme_get: return 0 + case .p_uiComponents_get: return 0 + case .p_discovery_get: return 0 + case .p_dashboard_get: return 0 + case .p_braze_get: return 0 + case .p_branch_get: return 0 + case .p_program_get: return 0 + case .p_URIScheme_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_baseURL_get: return "[get] .baseURL" + case .p_baseSSOURL_get: return "[get] .baseSSOURL" + case .p_ssoFinishedURL_get: return "[get] .ssoFinishedURL" + case .p_ssoButtonTitle_get: return "[get] .ssoButtonTitle" + case .p_oAuthClientId_get: return "[get] .oAuthClientId" + case .p_tokenType_get: return "[get] .tokenType" + case .p_feedbackEmail_get: return "[get] .feedbackEmail" + case .p_appStoreLink_get: return "[get] .appStoreLink" + case .p_faq_get: return "[get] .faq" + case .p_platformName_get: return "[get] .platformName" + case .p_agreement_get: return "[get] .agreement" + case .p_firebase_get: return "[get] .firebase" + case .p_facebook_get: return "[get] .facebook" + case .p_microsoft_get: return "[get] .microsoft" + case .p_google_get: return "[get] .google" + case .p_appleSignIn_get: return "[get] .appleSignIn" + case .p_features_get: return "[get] .features" + case .p_theme_get: return "[get] .theme" + case .p_uiComponents_get: return "[get] .uiComponents" + case .p_discovery_get: return "[get] .discovery" + case .p_dashboard_get: return "[get] .dashboard" + case .p_braze_get: return "[get] .braze" + case .p_branch_get: return "[get] .branch" + case .p_program_get: return "[get] .program" + case .p_URIScheme_get: return "[get] .URIScheme" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func baseURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func baseSSOURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseSSOURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoFinishedURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_ssoFinishedURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoButtonTitle(getter defaultValue: [String: Any]...) -> PropertyStub { + return Given(method: .p_ssoButtonTitle_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func oAuthClientId(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_oAuthClientId_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func tokenType(getter defaultValue: TokenType...) -> PropertyStub { + return Given(method: .p_tokenType_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func feedbackEmail(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_feedbackEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appStoreLink(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_appStoreLink_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func faq(getter defaultValue: URL?...) -> PropertyStub { + return Given(method: .p_faq_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func platformName(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_platformName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func agreement(getter defaultValue: AgreementConfig...) -> PropertyStub { + return Given(method: .p_agreement_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func firebase(getter defaultValue: FirebaseConfig...) -> PropertyStub { + return Given(method: .p_firebase_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func facebook(getter defaultValue: FacebookConfig...) -> PropertyStub { + return Given(method: .p_facebook_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func microsoft(getter defaultValue: MicrosoftConfig...) -> PropertyStub { + return Given(method: .p_microsoft_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func google(getter defaultValue: GoogleConfig...) -> PropertyStub { + return Given(method: .p_google_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignIn(getter defaultValue: AppleSignInConfig...) -> PropertyStub { + return Given(method: .p_appleSignIn_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func features(getter defaultValue: FeaturesConfig...) -> PropertyStub { + return Given(method: .p_features_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func theme(getter defaultValue: ThemeConfig...) -> PropertyStub { + return Given(method: .p_theme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func uiComponents(getter defaultValue: UIComponentsConfig...) -> PropertyStub { + return Given(method: .p_uiComponents_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func discovery(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_discovery_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func dashboard(getter defaultValue: DashboardConfig...) -> PropertyStub { + return Given(method: .p_dashboard_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func braze(getter defaultValue: BrazeConfig...) -> PropertyStub { + return Given(method: .p_braze_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func branch(getter defaultValue: BranchConfig...) -> PropertyStub { + return Given(method: .p_branch_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func program(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_program_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func URIScheme(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_URIScheme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var baseURL: Verify { return Verify(method: .p_baseURL_get) } + public static var baseSSOURL: Verify { return Verify(method: .p_baseSSOURL_get) } + public static var ssoFinishedURL: Verify { return Verify(method: .p_ssoFinishedURL_get) } + public static var ssoButtonTitle: Verify { return Verify(method: .p_ssoButtonTitle_get) } + public static var oAuthClientId: Verify { return Verify(method: .p_oAuthClientId_get) } + public static var tokenType: Verify { return Verify(method: .p_tokenType_get) } + public static var feedbackEmail: Verify { return Verify(method: .p_feedbackEmail_get) } + public static var appStoreLink: Verify { return Verify(method: .p_appStoreLink_get) } + public static var faq: Verify { return Verify(method: .p_faq_get) } + public static var platformName: Verify { return Verify(method: .p_platformName_get) } + public static var agreement: Verify { return Verify(method: .p_agreement_get) } + public static var firebase: Verify { return Verify(method: .p_firebase_get) } + public static var facebook: Verify { return Verify(method: .p_facebook_get) } + public static var microsoft: Verify { return Verify(method: .p_microsoft_get) } + public static var google: Verify { return Verify(method: .p_google_get) } + public static var appleSignIn: Verify { return Verify(method: .p_appleSignIn_get) } + public static var features: Verify { return Verify(method: .p_features_get) } + public static var theme: Verify { return Verify(method: .p_theme_get) } + public static var uiComponents: Verify { return Verify(method: .p_uiComponents_get) } + public static var discovery: Verify { return Verify(method: .p_discovery_get) } + public static var dashboard: Verify { return Verify(method: .p_dashboard_get) } + public static var braze: Verify { return Verify(method: .p_braze_get) } + public static var branch: Verify { return Verify(method: .p_branch_get) } + public static var program: Verify { return Verify(method: .p_program_get) } + public static var URIScheme: Verify { return Verify(method: .p_URIScheme_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - ConnectivityProtocol + +open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var isInternetAvaliable: Bool { + get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } + } + private var __p_isInternetAvaliable: (Bool)? + + public var isMobileData: Bool { + get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } + } + private var __p_isMobileData: (Bool)? + + public var internetReachableSubject: CurrentValueSubject { + get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } + } + private var __p_internetReachableSubject: (CurrentValueSubject)? + + + + + + + fileprivate enum MethodType { + case p_isInternetAvaliable_get + case p_isMobileData_get + case p_internetReachableSubject_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match + case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match + case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_isInternetAvaliable_get: return 0 + case .p_isMobileData_get: return 0 + case .p_internetReachableSubject_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" + case .p_isMobileData_get: return "[get] .isMobileData" + case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { + return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } + public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } + public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreAnalytics + +open class CoreAnalyticsMock: CoreAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { + addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) + let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void + perform?(`event`, `biValue`, `action`, `rating`) + } + + open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { + addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) + let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void + perform?(`event`, `bivalue`, `value`, `oldValue`) + } + + open func trackEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + + fileprivate enum MethodType { + case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) + case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) + case m_trackEvent__event(Parameter) + case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) + return Matcher.ComparisonResult(results) + + case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_trackEvent__event(p0): return p0.intValue + case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" + case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" + case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" + case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" + case .m_trackEvent__event: return ".trackEvent(_:)" + case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} + public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { + return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) + } + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { + return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) + } + public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackEvent__event(`event`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CorePersistenceProtocol + +open class CorePersistenceProtocolMock: CorePersistenceProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func set(userId: Int) { + addInvocation(.m_set__userId_userId(Parameter.value(`userId`))) + let perform = methodPerformValue(.m_set__userId_userId(Parameter.value(`userId`))) as? (Int) -> Void + perform?(`userId`) + } + + open func getUserID() -> Int? { + addInvocation(.m_getUserID) + let perform = methodPerformValue(.m_getUserID) as? () -> Void + perform?() + var __value: Int? = nil + do { + __value = try methodReturnValue(.m_getUserID).casted() + } catch { + // do nothing + } + return __value + } + + open func publisher() -> AnyPublisher { + addInvocation(.m_publisher) + let perform = methodPerformValue(.m_publisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_publisher).casted() + } catch { + onFatalFailure("Stub return value not specified for publisher(). Use given") + Failure("Stub return value not specified for publisher(). Use given") + } + return __value + } + + open func addToDownloadQueue(tasks: [DownloadDataTask]) { + addInvocation(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) as? ([DownloadDataTask]) -> Void + perform?(`tasks`) + } + + open func saveOfflineProgress(progress: OfflineProgress) { + addInvocation(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) + let perform = methodPerformValue(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) as? (OfflineProgress) -> Void + perform?(`progress`) + } + + open func loadProgress(for blockID: String) -> OfflineProgress? { + addInvocation(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + var __value: OfflineProgress? = nil + do { + __value = try methodReturnValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))).casted() + } catch { + // do nothing + } + return __value + } + + open func loadAllOfflineProgress() -> [OfflineProgress] { + addInvocation(.m_loadAllOfflineProgress) + let perform = methodPerformValue(.m_loadAllOfflineProgress) as? () -> Void + perform?() + var __value: [OfflineProgress] + do { + __value = try methodReturnValue(.m_loadAllOfflineProgress).casted() + } catch { + onFatalFailure("Stub return value not specified for loadAllOfflineProgress(). Use given") + Failure("Stub return value not specified for loadAllOfflineProgress(). Use given") + } + return __value + } + + open func deleteProgress(for blockID: String) { + addInvocation(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + } + + open func deleteAllProgress() { + addInvocation(.m_deleteAllProgress) + let perform = methodPerformValue(.m_deleteAllProgress) as? () -> Void + perform?() + } + + open func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) { + addInvocation(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) as? ([CourseBlock], DownloadQuality) -> Void + perform?(`blocks`, `downloadQuality`) + } + + open func nextBlockForDownloading() -> DownloadDataTask? { + addInvocation(.m_nextBlockForDownloading) + let perform = methodPerformValue(.m_nextBlockForDownloading) as? () -> Void + perform?() + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_nextBlockForDownloading).casted() + } catch { + // do nothing + } + return __value + } + + open func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) { + addInvocation(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) + let perform = methodPerformValue(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) as? (String, DownloadState, Data?) -> Void + perform?(`id`, `state`, `resumeData`) + } + + open func deleteDownloadDataTask(id: String) throws { + addInvocation(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) + let perform = methodPerformValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) as? (String) -> Void + perform?(`id`) + do { + _ = try methodReturnValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func saveDownloadDataTask(_ task: DownloadDataTask) { + addInvocation(.m_saveDownloadDataTask__task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_saveDownloadDataTask__task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) + } + + open func downloadDataTask(for blockId: String) -> DownloadDataTask? { + addInvocation(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) + let perform = methodPerformValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void + perform?(`blockId`) + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))).casted() + } catch { + // do nothing + } + return __value + } + + open func getDownloadDataTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasks) + let perform = methodPerformValue(.m_getDownloadDataTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasks(). Use given") + Failure("Stub return value not specified for getDownloadDataTasks(). Use given") + } + return __value + } + + open func getDownloadDataTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + } + return __value + } + + + fileprivate enum MethodType { + case m_set__userId_userId(Parameter) + case m_getUserID + case m_publisher + case m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>) + case m_saveOfflineProgress__progress_progress(Parameter) + case m_loadProgress__for_blockID(Parameter) + case m_loadAllOfflineProgress + case m_deleteProgress__for_blockID(Parameter) + case m_deleteAllProgress + case m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>, Parameter) + case m_nextBlockForDownloading + case m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter, Parameter, Parameter) + case m_deleteDownloadDataTask__id_id(Parameter) + case m_saveDownloadDataTask__task(Parameter) + case m_downloadDataTask__for_blockId(Parameter) + case m_getDownloadDataTasks + case m_getDownloadDataTasksForCourse__courseId(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_set__userId_userId(let lhsUserid), .m_set__userId_userId(let rhsUserid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUserid, rhs: rhsUserid, with: matcher), lhsUserid, rhsUserid, "userId")) + return Matcher.ComparisonResult(results) + + case (.m_getUserID, .m_getUserID): return .match + + case (.m_publisher, .m_publisher): return .match + + case (.m_addToDownloadQueue__tasks_tasks(let lhsTasks), .m_addToDownloadQueue__tasks_tasks(let rhsTasks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTasks, rhs: rhsTasks, with: matcher), lhsTasks, rhsTasks, "tasks")) + return Matcher.ComparisonResult(results) + + case (.m_saveOfflineProgress__progress_progress(let lhsProgress), .m_saveOfflineProgress__progress_progress(let rhsProgress)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsProgress, rhs: rhsProgress, with: matcher), lhsProgress, rhsProgress, "progress")) + return Matcher.ComparisonResult(results) + + case (.m_loadProgress__for_blockID(let lhsBlockid), .m_loadProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_loadAllOfflineProgress, .m_loadAllOfflineProgress): return .match + + case (.m_deleteProgress__for_blockID(let lhsBlockid), .m_deleteProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_deleteAllProgress, .m_deleteAllProgress): return .match + + case (.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let lhsBlocks, let lhsDownloadquality), .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let rhsBlocks, let rhsDownloadquality)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDownloadquality, rhs: rhsDownloadquality, with: matcher), lhsDownloadquality, rhsDownloadquality, "downloadQuality")) + return Matcher.ComparisonResult(results) + + case (.m_nextBlockForDownloading, .m_nextBlockForDownloading): return .match + + case (.m_updateDownloadState__id_idstate_stateresumeData_resumeData(let lhsId, let lhsState, let lhsResumedata), .m_updateDownloadState__id_idstate_stateresumeData_resumeData(let rhsId, let rhsState, let rhsResumedata)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsState, rhs: rhsState, with: matcher), lhsState, rhsState, "state")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResumedata, rhs: rhsResumedata, with: matcher), lhsResumedata, rhsResumedata, "resumeData")) + return Matcher.ComparisonResult(results) + + case (.m_deleteDownloadDataTask__id_id(let lhsId), .m_deleteDownloadDataTask__id_id(let rhsId)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + return Matcher.ComparisonResult(results) + + case (.m_saveDownloadDataTask__task(let lhsTask), .m_saveDownloadDataTask__task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "_ task")) + return Matcher.ComparisonResult(results) + + case (.m_downloadDataTask__for_blockId(let lhsBlockid), .m_downloadDataTask__for_blockId(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) + return Matcher.ComparisonResult(results) + + case (.m_getDownloadDataTasks, .m_getDownloadDataTasks): return .match + + case (.m_getDownloadDataTasksForCourse__courseId(let lhsCourseid), .m_getDownloadDataTasksForCourse__courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_set__userId_userId(p0): return p0.intValue + case .m_getUserID: return 0 + case .m_publisher: return 0 + case let .m_addToDownloadQueue__tasks_tasks(p0): return p0.intValue + case let .m_saveOfflineProgress__progress_progress(p0): return p0.intValue + case let .m_loadProgress__for_blockID(p0): return p0.intValue + case .m_loadAllOfflineProgress: return 0 + case let .m_deleteProgress__for_blockID(p0): return p0.intValue + case .m_deleteAllProgress: return 0 + case let .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(p0, p1): return p0.intValue + p1.intValue + case .m_nextBlockForDownloading: return 0 + case let .m_updateDownloadState__id_idstate_stateresumeData_resumeData(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_deleteDownloadDataTask__id_id(p0): return p0.intValue + case let .m_saveDownloadDataTask__task(p0): return p0.intValue + case let .m_downloadDataTask__for_blockId(p0): return p0.intValue + case .m_getDownloadDataTasks: return 0 + case let .m_getDownloadDataTasksForCourse__courseId(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_set__userId_userId: return ".set(userId:)" + case .m_getUserID: return ".getUserID()" + case .m_publisher: return ".publisher()" + case .m_addToDownloadQueue__tasks_tasks: return ".addToDownloadQueue(tasks:)" + case .m_saveOfflineProgress__progress_progress: return ".saveOfflineProgress(progress:)" + case .m_loadProgress__for_blockID: return ".loadProgress(for:)" + case .m_loadAllOfflineProgress: return ".loadAllOfflineProgress()" + case .m_deleteProgress__for_blockID: return ".deleteProgress(for:)" + case .m_deleteAllProgress: return ".deleteAllProgress()" + case .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality: return ".addToDownloadQueue(blocks:downloadQuality:)" + case .m_nextBlockForDownloading: return ".nextBlockForDownloading()" + case .m_updateDownloadState__id_idstate_stateresumeData_resumeData: return ".updateDownloadState(id:state:resumeData:)" + case .m_deleteDownloadDataTask__id_id: return ".deleteDownloadDataTask(id:)" + case .m_saveDownloadDataTask__task: return ".saveDownloadDataTask(_:)" + case .m_downloadDataTask__for_blockId: return ".downloadDataTask(for:)" + case .m_getDownloadDataTasks: return ".getDownloadDataTasks()" + case .m_getDownloadDataTasksForCourse__courseId: return ".getDownloadDataTasksForCourse(_:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func getUserID(willReturn: Int?...) -> MethodStub { + return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func publisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadProgress(for blockID: Parameter, willReturn: OfflineProgress?...) -> MethodStub { + return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadAllOfflineProgress(willReturn: [OfflineProgress]...) -> MethodStub { + return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func nextBlockForDownloading(willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func downloadDataTask(for blockId: Parameter, willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getUserID(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Int?] = [] + let given: Given = { return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Int?).self) + willProduce(stubber) + return given + } + public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func loadProgress(for blockID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [OfflineProgress?] = [] + let given: Given = { return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (OfflineProgress?).self) + willProduce(stubber) + return given + } + public static func loadAllOfflineProgress(willProduce: (Stubber<[OfflineProgress]>) -> Void) -> MethodStub { + let willReturn: [[OfflineProgress]] = [] + let given: Given = { return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([OfflineProgress]).self) + willProduce(stubber) + return given + } + public static func nextBlockForDownloading(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func downloadDataTask(for blockId: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func deleteDownloadDataTask(id: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func deleteDownloadDataTask(id: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func set(userId: Parameter) -> Verify { return Verify(method: .m_set__userId_userId(`userId`))} + public static func getUserID() -> Verify { return Verify(method: .m_getUserID)} + public static func publisher() -> Verify { return Verify(method: .m_publisher)} + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>) -> Verify { return Verify(method: .m_addToDownloadQueue__tasks_tasks(`tasks`))} + public static func saveOfflineProgress(progress: Parameter) -> Verify { return Verify(method: .m_saveOfflineProgress__progress_progress(`progress`))} + public static func loadProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_loadProgress__for_blockID(`blockID`))} + public static func loadAllOfflineProgress() -> Verify { return Verify(method: .m_loadAllOfflineProgress)} + public static func deleteProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_deleteProgress__for_blockID(`blockID`))} + public static func deleteAllProgress() -> Verify { return Verify(method: .m_deleteAllProgress)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`))} + public static func nextBlockForDownloading() -> Verify { return Verify(method: .m_nextBlockForDownloading)} + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter) -> Verify { return Verify(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`))} + public static func deleteDownloadDataTask(id: Parameter) -> Verify { return Verify(method: .m_deleteDownloadDataTask__id_id(`id`))} + public static func saveDownloadDataTask(_ task: Parameter) -> Verify { return Verify(method: .m_saveDownloadDataTask__task(`task`))} + public static func downloadDataTask(for blockId: Parameter) -> Verify { return Verify(method: .m_downloadDataTask__for_blockId(`blockId`))} + public static func getDownloadDataTasks() -> Verify { return Verify(method: .m_getDownloadDataTasks)} + public static func getDownloadDataTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func set(userId: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_set__userId_userId(`userId`), performs: perform) + } + public static func getUserID(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getUserID, performs: perform) + } + public static func publisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_publisher, performs: perform) + } + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>, perform: @escaping ([DownloadDataTask]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__tasks_tasks(`tasks`), performs: perform) + } + public static func saveOfflineProgress(progress: Parameter, perform: @escaping (OfflineProgress) -> Void) -> Perform { + return Perform(method: .m_saveOfflineProgress__progress_progress(`progress`), performs: perform) + } + public static func loadProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_loadProgress__for_blockID(`blockID`), performs: perform) + } + public static func loadAllOfflineProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_loadAllOfflineProgress, performs: perform) + } + public static func deleteProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteProgress__for_blockID(`blockID`), performs: perform) + } + public static func deleteAllProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteAllProgress, performs: perform) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter, perform: @escaping ([CourseBlock], DownloadQuality) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`), performs: perform) + } + public static func nextBlockForDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_nextBlockForDownloading, performs: perform) + } + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter, perform: @escaping (String, DownloadState, Data?) -> Void) -> Perform { + return Perform(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`), performs: perform) + } + public static func deleteDownloadDataTask(id: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteDownloadDataTask__id_id(`id`), performs: perform) + } + public static func saveDownloadDataTask(_ task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_saveDownloadDataTask__task(`task`), performs: perform) + } + public static func downloadDataTask(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_downloadDataTask__for_blockId(`blockId`), performs: perform) + } + public static func getDownloadDataTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasks, performs: perform) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreStorage + +open class CoreStorageMock: CoreStorage, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var accessToken: String? { + get { invocations.append(.p_accessToken_get); return __p_accessToken ?? optionalGivenGetterValue(.p_accessToken_get, "CoreStorageMock - stub value for accessToken was not defined") } + set { invocations.append(.p_accessToken_set(.value(newValue))); __p_accessToken = newValue } + } + private var __p_accessToken: (String)? + + public var refreshToken: String? { + get { invocations.append(.p_refreshToken_get); return __p_refreshToken ?? optionalGivenGetterValue(.p_refreshToken_get, "CoreStorageMock - stub value for refreshToken was not defined") } + set { invocations.append(.p_refreshToken_set(.value(newValue))); __p_refreshToken = newValue } + } + private var __p_refreshToken: (String)? + + public var pushToken: String? { + get { invocations.append(.p_pushToken_get); return __p_pushToken ?? optionalGivenGetterValue(.p_pushToken_get, "CoreStorageMock - stub value for pushToken was not defined") } + set { invocations.append(.p_pushToken_set(.value(newValue))); __p_pushToken = newValue } + } + private var __p_pushToken: (String)? + + public var appleSignFullName: String? { + get { invocations.append(.p_appleSignFullName_get); return __p_appleSignFullName ?? optionalGivenGetterValue(.p_appleSignFullName_get, "CoreStorageMock - stub value for appleSignFullName was not defined") } + set { invocations.append(.p_appleSignFullName_set(.value(newValue))); __p_appleSignFullName = newValue } + } + private var __p_appleSignFullName: (String)? + + public var appleSignEmail: String? { + get { invocations.append(.p_appleSignEmail_get); return __p_appleSignEmail ?? optionalGivenGetterValue(.p_appleSignEmail_get, "CoreStorageMock - stub value for appleSignEmail was not defined") } + set { invocations.append(.p_appleSignEmail_set(.value(newValue))); __p_appleSignEmail = newValue } + } + private var __p_appleSignEmail: (String)? + + public var cookiesDate: Date? { + get { invocations.append(.p_cookiesDate_get); return __p_cookiesDate ?? optionalGivenGetterValue(.p_cookiesDate_get, "CoreStorageMock - stub value for cookiesDate was not defined") } + set { invocations.append(.p_cookiesDate_set(.value(newValue))); __p_cookiesDate = newValue } + } + private var __p_cookiesDate: (Date)? + + public var reviewLastShownVersion: String? { + get { invocations.append(.p_reviewLastShownVersion_get); return __p_reviewLastShownVersion ?? optionalGivenGetterValue(.p_reviewLastShownVersion_get, "CoreStorageMock - stub value for reviewLastShownVersion was not defined") } + set { invocations.append(.p_reviewLastShownVersion_set(.value(newValue))); __p_reviewLastShownVersion = newValue } + } + private var __p_reviewLastShownVersion: (String)? + + public var lastReviewDate: Date? { + get { invocations.append(.p_lastReviewDate_get); return __p_lastReviewDate ?? optionalGivenGetterValue(.p_lastReviewDate_get, "CoreStorageMock - stub value for lastReviewDate was not defined") } + set { invocations.append(.p_lastReviewDate_set(.value(newValue))); __p_lastReviewDate = newValue } + } + private var __p_lastReviewDate: (Date)? + + public var user: DataLayer.User? { + get { invocations.append(.p_user_get); return __p_user ?? optionalGivenGetterValue(.p_user_get, "CoreStorageMock - stub value for user was not defined") } + set { invocations.append(.p_user_set(.value(newValue))); __p_user = newValue } + } + private var __p_user: (DataLayer.User)? + + public var userSettings: UserSettings? { + get { invocations.append(.p_userSettings_get); return __p_userSettings ?? optionalGivenGetterValue(.p_userSettings_get, "CoreStorageMock - stub value for userSettings was not defined") } + set { invocations.append(.p_userSettings_set(.value(newValue))); __p_userSettings = newValue } + } + private var __p_userSettings: (UserSettings)? + + public var resetAppSupportDirectoryUserData: Bool? { + get { invocations.append(.p_resetAppSupportDirectoryUserData_get); return __p_resetAppSupportDirectoryUserData ?? optionalGivenGetterValue(.p_resetAppSupportDirectoryUserData_get, "CoreStorageMock - stub value for resetAppSupportDirectoryUserData was not defined") } + set { invocations.append(.p_resetAppSupportDirectoryUserData_set(.value(newValue))); __p_resetAppSupportDirectoryUserData = newValue } + } + private var __p_resetAppSupportDirectoryUserData: (Bool)? + + public var useRelativeDates: Bool { + get { invocations.append(.p_useRelativeDates_get); return __p_useRelativeDates ?? givenGetterValue(.p_useRelativeDates_get, "CoreStorageMock - stub value for useRelativeDates was not defined") } + set { invocations.append(.p_useRelativeDates_set(.value(newValue))); __p_useRelativeDates = newValue } + } + private var __p_useRelativeDates: (Bool)? + + + + + + open func clear() { + addInvocation(.m_clear) + let perform = methodPerformValue(.m_clear) as? () -> Void + perform?() + } + + + fileprivate enum MethodType { + case m_clear + case p_accessToken_get + case p_accessToken_set(Parameter) + case p_refreshToken_get + case p_refreshToken_set(Parameter) + case p_pushToken_get + case p_pushToken_set(Parameter) + case p_appleSignFullName_get + case p_appleSignFullName_set(Parameter) + case p_appleSignEmail_get + case p_appleSignEmail_set(Parameter) + case p_cookiesDate_get + case p_cookiesDate_set(Parameter) + case p_reviewLastShownVersion_get + case p_reviewLastShownVersion_set(Parameter) + case p_lastReviewDate_get + case p_lastReviewDate_set(Parameter) + case p_user_get + case p_user_set(Parameter) + case p_userSettings_get + case p_userSettings_set(Parameter) + case p_resetAppSupportDirectoryUserData_get + case p_resetAppSupportDirectoryUserData_set(Parameter) + case p_useRelativeDates_get + case p_useRelativeDates_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_clear, .m_clear): return .match + case (.p_accessToken_get,.p_accessToken_get): return Matcher.ComparisonResult.match + case (.p_accessToken_set(let left),.p_accessToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_refreshToken_get,.p_refreshToken_get): return Matcher.ComparisonResult.match + case (.p_refreshToken_set(let left),.p_refreshToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_pushToken_get,.p_pushToken_get): return Matcher.ComparisonResult.match + case (.p_pushToken_set(let left),.p_pushToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignFullName_get,.p_appleSignFullName_get): return Matcher.ComparisonResult.match + case (.p_appleSignFullName_set(let left),.p_appleSignFullName_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignEmail_get,.p_appleSignEmail_get): return Matcher.ComparisonResult.match + case (.p_appleSignEmail_set(let left),.p_appleSignEmail_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_cookiesDate_get,.p_cookiesDate_get): return Matcher.ComparisonResult.match + case (.p_cookiesDate_set(let left),.p_cookiesDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_reviewLastShownVersion_get,.p_reviewLastShownVersion_get): return Matcher.ComparisonResult.match + case (.p_reviewLastShownVersion_set(let left),.p_reviewLastShownVersion_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_lastReviewDate_get,.p_lastReviewDate_get): return Matcher.ComparisonResult.match + case (.p_lastReviewDate_set(let left),.p_lastReviewDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_user_get,.p_user_get): return Matcher.ComparisonResult.match + case (.p_user_set(let left),.p_user_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_userSettings_get,.p_userSettings_get): return Matcher.ComparisonResult.match + case (.p_userSettings_set(let left),.p_userSettings_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_resetAppSupportDirectoryUserData_get,.p_resetAppSupportDirectoryUserData_get): return Matcher.ComparisonResult.match + case (.p_resetAppSupportDirectoryUserData_set(let left),.p_resetAppSupportDirectoryUserData_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_useRelativeDates_get,.p_useRelativeDates_get): return Matcher.ComparisonResult.match + case (.p_useRelativeDates_set(let left),.p_useRelativeDates_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .m_clear: return 0 + case .p_accessToken_get: return 0 + case .p_accessToken_set(let newValue): return newValue.intValue + case .p_refreshToken_get: return 0 + case .p_refreshToken_set(let newValue): return newValue.intValue + case .p_pushToken_get: return 0 + case .p_pushToken_set(let newValue): return newValue.intValue + case .p_appleSignFullName_get: return 0 + case .p_appleSignFullName_set(let newValue): return newValue.intValue + case .p_appleSignEmail_get: return 0 + case .p_appleSignEmail_set(let newValue): return newValue.intValue + case .p_cookiesDate_get: return 0 + case .p_cookiesDate_set(let newValue): return newValue.intValue + case .p_reviewLastShownVersion_get: return 0 + case .p_reviewLastShownVersion_set(let newValue): return newValue.intValue + case .p_lastReviewDate_get: return 0 + case .p_lastReviewDate_set(let newValue): return newValue.intValue + case .p_user_get: return 0 + case .p_user_set(let newValue): return newValue.intValue + case .p_userSettings_get: return 0 + case .p_userSettings_set(let newValue): return newValue.intValue + case .p_resetAppSupportDirectoryUserData_get: return 0 + case .p_resetAppSupportDirectoryUserData_set(let newValue): return newValue.intValue + case .p_useRelativeDates_get: return 0 + case .p_useRelativeDates_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .m_clear: return ".clear()" + case .p_accessToken_get: return "[get] .accessToken" + case .p_accessToken_set: return "[set] .accessToken" + case .p_refreshToken_get: return "[get] .refreshToken" + case .p_refreshToken_set: return "[set] .refreshToken" + case .p_pushToken_get: return "[get] .pushToken" + case .p_pushToken_set: return "[set] .pushToken" + case .p_appleSignFullName_get: return "[get] .appleSignFullName" + case .p_appleSignFullName_set: return "[set] .appleSignFullName" + case .p_appleSignEmail_get: return "[get] .appleSignEmail" + case .p_appleSignEmail_set: return "[set] .appleSignEmail" + case .p_cookiesDate_get: return "[get] .cookiesDate" + case .p_cookiesDate_set: return "[set] .cookiesDate" + case .p_reviewLastShownVersion_get: return "[get] .reviewLastShownVersion" + case .p_reviewLastShownVersion_set: return "[set] .reviewLastShownVersion" + case .p_lastReviewDate_get: return "[get] .lastReviewDate" + case .p_lastReviewDate_set: return "[set] .lastReviewDate" + case .p_user_get: return "[get] .user" + case .p_user_set: return "[set] .user" + case .p_userSettings_get: return "[get] .userSettings" + case .p_userSettings_set: return "[set] .userSettings" + case .p_resetAppSupportDirectoryUserData_get: return "[get] .resetAppSupportDirectoryUserData" + case .p_resetAppSupportDirectoryUserData_set: return "[set] .resetAppSupportDirectoryUserData" + case .p_useRelativeDates_get: return "[get] .useRelativeDates" + case .p_useRelativeDates_set: return "[set] .useRelativeDates" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func accessToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_accessToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func refreshToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_refreshToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func pushToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_pushToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignFullName(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignFullName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignEmail(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_cookiesDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func reviewLastShownVersion(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_reviewLastShownVersion_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func lastReviewDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_lastReviewDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func user(getter defaultValue: DataLayer.User?...) -> PropertyStub { + return Given(method: .p_user_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func userSettings(getter defaultValue: UserSettings?...) -> PropertyStub { + return Given(method: .p_userSettings_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func resetAppSupportDirectoryUserData(getter defaultValue: Bool?...) -> PropertyStub { + return Given(method: .p_resetAppSupportDirectoryUserData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func useRelativeDates(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_useRelativeDates_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func clear() -> Verify { return Verify(method: .m_clear)} + public static var accessToken: Verify { return Verify(method: .p_accessToken_get) } + public static func accessToken(set newValue: Parameter) -> Verify { return Verify(method: .p_accessToken_set(newValue)) } + public static var refreshToken: Verify { return Verify(method: .p_refreshToken_get) } + public static func refreshToken(set newValue: Parameter) -> Verify { return Verify(method: .p_refreshToken_set(newValue)) } + public static var pushToken: Verify { return Verify(method: .p_pushToken_get) } + public static func pushToken(set newValue: Parameter) -> Verify { return Verify(method: .p_pushToken_set(newValue)) } + public static var appleSignFullName: Verify { return Verify(method: .p_appleSignFullName_get) } + public static func appleSignFullName(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignFullName_set(newValue)) } + public static var appleSignEmail: Verify { return Verify(method: .p_appleSignEmail_get) } + public static func appleSignEmail(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignEmail_set(newValue)) } + public static var cookiesDate: Verify { return Verify(method: .p_cookiesDate_get) } + public static func cookiesDate(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesDate_set(newValue)) } + public static var reviewLastShownVersion: Verify { return Verify(method: .p_reviewLastShownVersion_get) } + public static func reviewLastShownVersion(set newValue: Parameter) -> Verify { return Verify(method: .p_reviewLastShownVersion_set(newValue)) } + public static var lastReviewDate: Verify { return Verify(method: .p_lastReviewDate_get) } + public static func lastReviewDate(set newValue: Parameter) -> Verify { return Verify(method: .p_lastReviewDate_set(newValue)) } + public static var user: Verify { return Verify(method: .p_user_get) } + public static func user(set newValue: Parameter) -> Verify { return Verify(method: .p_user_set(newValue)) } + public static var userSettings: Verify { return Verify(method: .p_userSettings_get) } + public static func userSettings(set newValue: Parameter) -> Verify { return Verify(method: .p_userSettings_set(newValue)) } + public static var resetAppSupportDirectoryUserData: Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_get) } + public static func resetAppSupportDirectoryUserData(set newValue: Parameter) -> Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_set(newValue)) } + public static var useRelativeDates: Verify { return Verify(method: .p_useRelativeDates_get) } + public static func useRelativeDates(set newValue: Parameter) -> Verify { return Verify(method: .p_useRelativeDates_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func clear(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_clear, performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - DownloadManagerProtocol + +open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var currentDownloadTask: DownloadDataTask? { + get { invocations.append(.p_currentDownloadTask_get); return __p_currentDownloadTask ?? optionalGivenGetterValue(.p_currentDownloadTask_get, "DownloadManagerProtocolMock - stub value for currentDownloadTask was not defined") } + } + private var __p_currentDownloadTask: (DownloadDataTask)? + + + + + + open func publisher() -> AnyPublisher { + addInvocation(.m_publisher) + let perform = methodPerformValue(.m_publisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_publisher).casted() + } catch { + onFatalFailure("Stub return value not specified for publisher(). Use given") + Failure("Stub return value not specified for publisher(). Use given") + } + return __value + } + + open func eventPublisher() -> AnyPublisher { + addInvocation(.m_eventPublisher) + let perform = methodPerformValue(.m_eventPublisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_eventPublisher).casted() + } catch { + onFatalFailure("Stub return value not specified for eventPublisher(). Use given") + Failure("Stub return value not specified for eventPublisher(). Use given") + } + return __value + } + + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func getDownloadTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasks) + let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadTasks(). Use given") + Failure("Stub return value not specified for getDownloadTasks(). Use given") + } + return __value + } + + open func getDownloadTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") + } + return __value + } + + open func cancelDownloading(courseId: String, blocks: [CourseBlock]) throws { + addInvocation(.m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter.value(`courseId`), Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter.value(`courseId`), Parameter<[CourseBlock]>.value(`blocks`))) as? (String, [CourseBlock]) -> Void + perform?(`courseId`, `blocks`) + do { + _ = try methodReturnValue(.m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter.value(`courseId`), Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func cancelDownloading(task: DownloadDataTask) throws { + addInvocation(.m_cancelDownloading__task_task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_cancelDownloading__task_task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) + do { + _ = try methodReturnValue(.m_cancelDownloading__task_task(Parameter.value(`task`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func cancelDownloading(courseId: String) throws { + addInvocation(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + do { + _ = try methodReturnValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func cancelAllDownloading() throws { + addInvocation(.m_cancelAllDownloading) + let perform = methodPerformValue(.m_cancelAllDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_cancelAllDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func deleteFile(blocks: [CourseBlock]) { + addInvocation(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + } + + open func deleteAllFiles() { + addInvocation(.m_deleteAllFiles) + let perform = methodPerformValue(.m_deleteAllFiles) as? () -> Void + perform?() + } + + open func fileUrl(for blockId: String) -> URL? { + addInvocation(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) + let perform = methodPerformValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void + perform?(`blockId`) + var __value: URL? = nil + do { + __value = try methodReturnValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))).casted() + } catch { + // do nothing + } + return __value + } + + open func updateUnzippedFileSize(for sequentials: [CourseSequential]) -> [CourseSequential] { + addInvocation(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) + let perform = methodPerformValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) as? ([CourseSequential]) -> Void + perform?(`sequentials`) + var __value: [CourseSequential] + do { + __value = try methodReturnValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))).casted() + } catch { + onFatalFailure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + Failure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + } + return __value + } + + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { + addInvocation(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + Failure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + } + return __value + } + + open func removeAppSupportDirectoryUnusedContent() { + addInvocation(.m_removeAppSupportDirectoryUnusedContent) + let perform = methodPerformValue(.m_removeAppSupportDirectoryUnusedContent) as? () -> Void + perform?() + } + + + fileprivate enum MethodType { + case m_publisher + case m_eventPublisher + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) + case m_getDownloadTasks + case m_getDownloadTasksForCourse__courseId(Parameter) + case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) + case m_cancelDownloading__task_task(Parameter) + case m_cancelDownloading__courseId_courseId(Parameter) + case m_cancelAllDownloading + case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) + case m_deleteAllFiles + case m_fileUrl__for_blockId(Parameter) + case m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>) + case m_resumeDownloading + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_removeAppSupportDirectoryUnusedContent + case p_currentDownloadTask_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_publisher, .m_publisher): return .match + + case (.m_eventPublisher, .m_eventPublisher): return .match + + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match + + case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) + return Matcher.ComparisonResult(results) + + case (.m_cancelDownloading__courseId_courseIdblocks_blocks(let lhsCourseid, let lhsBlocks), .m_cancelDownloading__courseId_courseIdblocks_blocks(let rhsCourseid, let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_cancelDownloading__task_task(let lhsTask), .m_cancelDownloading__task_task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "task")) + return Matcher.ComparisonResult(results) + + case (.m_cancelDownloading__courseId_courseId(let lhsCourseid), .m_cancelDownloading__courseId_courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + return Matcher.ComparisonResult(results) + + case (.m_cancelAllDownloading, .m_cancelAllDownloading): return .match + + case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_deleteAllFiles, .m_deleteAllFiles): return .match + + case (.m_fileUrl__for_blockId(let lhsBlockid), .m_fileUrl__for_blockId(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) + return Matcher.ComparisonResult(results) + + case (.m_updateUnzippedFileSize__for_sequentials(let lhsSequentials), .m_updateUnzippedFileSize__for_sequentials(let rhsSequentials)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSequentials, rhs: rhsSequentials, with: matcher), lhsSequentials, rhsSequentials, "for sequentials")) + return Matcher.ComparisonResult(results) + + case (.m_resumeDownloading, .m_resumeDownloading): return .match + + case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_removeAppSupportDirectoryUnusedContent, .m_removeAppSupportDirectoryUnusedContent): return .match + case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .m_publisher: return 0 + case .m_eventPublisher: return 0 + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue + case .m_getDownloadTasks: return 0 + case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue + case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue + case let .m_cancelDownloading__task_task(p0): return p0.intValue + case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue + case .m_cancelAllDownloading: return 0 + case let .m_deleteFile__blocks_blocks(p0): return p0.intValue + case .m_deleteAllFiles: return 0 + case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_updateUnzippedFileSize__for_sequentials(p0): return p0.intValue + case .m_resumeDownloading: return 0 + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_removeAppSupportDirectoryUnusedContent: return 0 + case .p_currentDownloadTask_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .m_publisher: return ".publisher()" + case .m_eventPublisher: return ".eventPublisher()" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" + case .m_getDownloadTasks: return ".getDownloadTasks()" + case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" + case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" + case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" + case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" + case .m_cancelAllDownloading: return ".cancelAllDownloading()" + case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" + case .m_deleteAllFiles: return ".deleteAllFiles()" + case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_updateUnzippedFileSize__for_sequentials: return ".updateUnzippedFileSize(for:)" + case .m_resumeDownloading: return ".resumeDownloading()" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_removeAppSupportDirectoryUnusedContent: return ".removeAppSupportDirectoryUnusedContent()" + case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func currentDownloadTask(getter defaultValue: DownloadDataTask?...) -> PropertyStub { + return Given(method: .p_currentDownloadTask_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + public static func publisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func eventPublisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { + return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willReturn: [CourseSequential]...) -> MethodStub { + return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func eventPublisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func getDownloadTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func fileUrl(for blockId: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [URL?] = [] + let given: Given = { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (URL?).self) + willProduce(stubber) + return given + } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willProduce: (Stubber<[CourseSequential]>) -> Void) -> MethodStub { + let willReturn: [[CourseSequential]] = [] + let given: Given = { return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseSequential]).self) + willProduce(stubber) + return given + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(task: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(task: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(courseId: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(courseId: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelAllDownloading(willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelAllDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func resumeDownloading(willThrow: Error...) -> MethodStub { + return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func resumeDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func publisher() -> Verify { return Verify(method: .m_publisher)} + public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} + public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} + public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} + public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} + public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} + public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} + public static func cancelAllDownloading() -> Verify { return Verify(method: .m_cancelAllDownloading)} + public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} + public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} + public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>) -> Verify { return Verify(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`))} + public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func removeAppSupportDirectoryUnusedContent() -> Verify { return Verify(method: .m_removeAppSupportDirectoryUnusedContent)} + public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func publisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_publisher, performs: perform) + } + public static func eventPublisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_eventPublisher, performs: perform) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } + public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadTasks, performs: perform) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadTasksForCourse__courseId(`courseId`), performs: perform) + } + public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, perform: @escaping (String, [CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), performs: perform) + } + public static func cancelDownloading(task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__task_task(`task`), performs: perform) + } + public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) + } + public static func cancelAllDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_cancelAllDownloading, performs: perform) + } + public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) + } + public static func deleteAllFiles(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteAllFiles, performs: perform) + } + public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) + } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, perform: @escaping ([CourseSequential]) -> Void) -> Perform { + return Perform(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), performs: perform) + } + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) + } + public static func removeAppSupportDirectoryUnusedContent(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeAppSupportDirectoryUnusedContent, performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - OfflineSyncInteractorProtocol + +open class OfflineSyncInteractorProtocolMock: OfflineSyncInteractorProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func submitOfflineProgress(courseID: String, blockID: String, data: String) throws -> Bool { + addInvocation(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) + let perform = methodPerformValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) as? (String, String, String) -> Void + perform?(`courseID`, `blockID`, `data`) + var __value: Bool + do { + __value = try methodReturnValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + Failure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + } catch { + throw error + } + return __value + } + + + fileprivate enum MethodType { + case m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter, Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let lhsCourseid, let lhsBlockid, let lhsData), .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let rhsCourseid, let rhsBlockid, let rhsData)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsData, rhs: rhsData, with: matcher), lhsData, rhsData, "data")) + return Matcher.ComparisonResult(results) + } + } + + func intValue() -> Int { + switch self { + case let .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + } + } + func assertionName() -> String { + switch self { + case .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data: return ".submitOfflineProgress(courseID:blockID:data:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Bool).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter) -> Verify { return Verify(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { + return Perform(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - WebviewCookiesUpdateProtocol + +open class WebviewCookiesUpdateProtocolMock: WebviewCookiesUpdateProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var authInteractor: AuthInteractorProtocol { + get { invocations.append(.p_authInteractor_get); return __p_authInteractor ?? givenGetterValue(.p_authInteractor_get, "WebviewCookiesUpdateProtocolMock - stub value for authInteractor was not defined") } + } + private var __p_authInteractor: (AuthInteractorProtocol)? + + public var cookiesReady: Bool { + get { invocations.append(.p_cookiesReady_get); return __p_cookiesReady ?? givenGetterValue(.p_cookiesReady_get, "WebviewCookiesUpdateProtocolMock - stub value for cookiesReady was not defined") } + set { invocations.append(.p_cookiesReady_set(.value(newValue))); __p_cookiesReady = newValue } + } + private var __p_cookiesReady: (Bool)? + + public var updatingCookies: Bool { + get { invocations.append(.p_updatingCookies_get); return __p_updatingCookies ?? givenGetterValue(.p_updatingCookies_get, "WebviewCookiesUpdateProtocolMock - stub value for updatingCookies was not defined") } + set { invocations.append(.p_updatingCookies_set(.value(newValue))); __p_updatingCookies = newValue } + } + private var __p_updatingCookies: (Bool)? + + public var errorMessage: String? { + get { invocations.append(.p_errorMessage_get); return __p_errorMessage ?? optionalGivenGetterValue(.p_errorMessage_get, "WebviewCookiesUpdateProtocolMock - stub value for errorMessage was not defined") } + set { invocations.append(.p_errorMessage_set(.value(newValue))); __p_errorMessage = newValue } + } + private var __p_errorMessage: (String)? + + + + + + open func updateCookies(force: Bool, retryCount: Int) { + addInvocation(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) + let perform = methodPerformValue(.m_updateCookies__force_forceretryCount_retryCount(Parameter.value(`force`), Parameter.value(`retryCount`))) as? (Bool, Int) -> Void + perform?(`force`, `retryCount`) + } + + + fileprivate enum MethodType { + case m_updateCookies__force_forceretryCount_retryCount(Parameter, Parameter) + case p_authInteractor_get + case p_cookiesReady_get + case p_cookiesReady_set(Parameter) + case p_updatingCookies_get + case p_updatingCookies_set(Parameter) + case p_errorMessage_get + case p_errorMessage_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_updateCookies__force_forceretryCount_retryCount(let lhsForce, let lhsRetrycount), .m_updateCookies__force_forceretryCount_retryCount(let rhsForce, let rhsRetrycount)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRetrycount, rhs: rhsRetrycount, with: matcher), lhsRetrycount, rhsRetrycount, "retryCount")) + return Matcher.ComparisonResult(results) + case (.p_authInteractor_get,.p_authInteractor_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_get,.p_cookiesReady_get): return Matcher.ComparisonResult.match + case (.p_cookiesReady_set(let left),.p_cookiesReady_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_updatingCookies_get,.p_updatingCookies_get): return Matcher.ComparisonResult.match + case (.p_updatingCookies_set(let left),.p_updatingCookies_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_errorMessage_get,.p_errorMessage_get): return Matcher.ComparisonResult.match + case (.p_errorMessage_set(let left),.p_errorMessage_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_updateCookies__force_forceretryCount_retryCount(p0, p1): return p0.intValue + p1.intValue + case .p_authInteractor_get: return 0 + case .p_cookiesReady_get: return 0 + case .p_cookiesReady_set(let newValue): return newValue.intValue + case .p_updatingCookies_get: return 0 + case .p_updatingCookies_set(let newValue): return newValue.intValue + case .p_errorMessage_get: return 0 + case .p_errorMessage_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .m_updateCookies__force_forceretryCount_retryCount: return ".updateCookies(force:retryCount:)" + case .p_authInteractor_get: return "[get] .authInteractor" + case .p_cookiesReady_get: return "[get] .cookiesReady" + case .p_cookiesReady_set: return "[set] .cookiesReady" + case .p_updatingCookies_get: return "[get] .updatingCookies" + case .p_updatingCookies_set: return "[set] .updatingCookies" + case .p_errorMessage_get: return "[get] .errorMessage" + case .p_errorMessage_set: return "[set] .errorMessage" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func authInteractor(getter defaultValue: AuthInteractorProtocol...) -> PropertyStub { + return Given(method: .p_authInteractor_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesReady(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_cookiesReady_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func updatingCookies(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_updatingCookies_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func errorMessage(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_errorMessage_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func updateCookies(force: Parameter, retryCount: Parameter) -> Verify { return Verify(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`))} + public static var authInteractor: Verify { return Verify(method: .p_authInteractor_get) } + public static var cookiesReady: Verify { return Verify(method: .p_cookiesReady_get) } + public static func cookiesReady(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesReady_set(newValue)) } + public static var updatingCookies: Verify { return Verify(method: .p_updatingCookies_get) } + public static func updatingCookies(set newValue: Parameter) -> Verify { return Verify(method: .p_updatingCookies_set(newValue)) } + public static var errorMessage: Verify { return Verify(method: .p_errorMessage_get) } + public static func errorMessage(set newValue: Parameter) -> Verify { return Verify(method: .p_errorMessage_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func updateCookies(force: Parameter, retryCount: Parameter, perform: @escaping (Bool, Int) -> Void) -> Perform { + return Perform(method: .m_updateCookies__force_forceretryCount_retryCount(`force`, `retryCount`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + diff --git a/Core/CoreTests/DownloadManager/DownloadManagerTests.swift b/Core/CoreTests/DownloadManager/DownloadManagerTests.swift new file mode 100644 index 000000000..67e0aa644 --- /dev/null +++ b/Core/CoreTests/DownloadManager/DownloadManagerTests.swift @@ -0,0 +1,331 @@ +// +// DownloadManagerTests.swift +// Core +// +// Created by Ivan Stepanok on 22.10.2024. +// + +import XCTest +import SwiftyMocky +@testable import Core + +final class DownloadManagerTests: XCTestCase { + + var persistence: CorePersistenceProtocolMock! + var storage: CoreStorageMock! + var connectivity: ConnectivityProtocolMock! + + override func setUp() { + super.setUp() + persistence = CorePersistenceProtocolMock() + storage = CoreStorageMock() + connectivity = ConnectivityProtocolMock() + } + + // MARK: - Test Add to Queue + + func testAddToDownloadQueue_WhenWiFiOnlyAndOnWiFi_ShouldAddToQueue() async throws { + // Given + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(connectivity, .isMobileData(getter: false)) + + let downloadManager = DownloadManager( + persistence: persistence, + appStorage: storage, + connectivity: connectivity + ) + + Given(storage, .userSettings(getter: UserSettings( + wifiOnly: true, + streamingQuality: .auto, + downloadQuality: .auto + ))) + + let blocks = [createMockCourseBlock()] + + // When + try await downloadManager.addToDownloadQueue(blocks: blocks) + + // Then + Verify(persistence, 1, .addToDownloadQueue(blocks: .value(blocks), downloadQuality: .value(.auto))) + } + + func testAddToDownloadQueue_WhenWiFiOnlyAndOnMobileData_ShouldThrowError() async { + // Given + Given(storage, .userSettings(getter: UserSettings( + wifiOnly: true, + streamingQuality: .auto, + downloadQuality: .auto + ))) + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .isMobileData(getter: true)) + + let downloadManager = DownloadManager( + persistence: persistence, + appStorage: storage, + connectivity: connectivity + ) + + let blocks = [createMockCourseBlock()] + + // When/Then + do { + try await downloadManager.addToDownloadQueue(blocks: blocks) + XCTFail("Should throw NoWiFiError") + } catch is NoWiFiError { + // Success + Verify(persistence, 0, .addToDownloadQueue(blocks: .any, downloadQuality: .value(.auto))) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + // MARK: - Test New Download + + func testNewDownload_WhenTaskAvailable_ShouldStartDownloading() async throws { + // Given + let mockTask = createMockDownloadTask() + Given(persistence, .getDownloadDataTasks(willReturn: [mockTask])) + Given(persistence, .nextBlockForDownloading(willReturn: mockTask)) + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .isMobileData(getter: false)) + + let downloadManager = DownloadManager( + persistence: persistence, + appStorage: storage, + connectivity: connectivity + ) + + // When + try await downloadManager.resumeDownloading() + + // Wait a bit for async operations to complete + try? await Task.sleep(nanoseconds: 100_000_000) + + // Then + Verify(persistence, 2, .nextBlockForDownloading()) + XCTAssertEqual(downloadManager.currentDownloadTask?.id, mockTask.id) + } + + // MARK: - Test Cancel Downloads + + func testCancelDownloading_ForSpecificTask_ShouldRemoveFileAndTask() async throws { + // Given + let task = createMockDownloadTask() + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .isMobileData(getter: false)) + Given(persistence, .deleteDownloadDataTask(id: .value(task.id), willProduce: { _ in })) + + let downloadManager = DownloadManager( + persistence: persistence, + appStorage: storage, + connectivity: connectivity + ) + + // When + try await downloadManager.cancelDownloading(task: task) + + // Then + Verify(persistence, 1, .deleteDownloadDataTask(id: .value(task.id))) + } + + func testCancelDownloading_ForCourse_ShouldCancelAllTasksForCourse() async throws { + // Given + let courseId = "course123" + let task = createMockDownloadTask(courseId: courseId) + let tasks = [task] + + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .isMobileData(getter: false)) + Given(persistence, .getDownloadDataTasksForCourse(.value(courseId), willReturn: tasks)) + Given(persistence, .deleteDownloadDataTask(id: .value(task.id), willProduce: { _ in })) + + let downloadManager = DownloadManager( + persistence: persistence, + appStorage: storage, + connectivity: connectivity + ) + + // When + try await downloadManager.cancelDownloading(courseId: courseId) + + // Then + Verify(persistence, 1, .getDownloadDataTasksForCourse(.value(courseId))) + Verify(persistence, 1, .deleteDownloadDataTask(id: .value(task.id))) + } + + // MARK: - Test File Management + + func testDeleteFile_ShouldRemoveFileAndTask() async { + // Given + let block = createMockCourseBlock() + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .isMobileData(getter: false)) + Given(persistence, .deleteDownloadDataTask(id: .value(block.id), willProduce: { _ in })) + + let downloadManager = DownloadManager( + persistence: persistence, + appStorage: storage, + connectivity: connectivity + ) + + // When + await downloadManager.deleteFile(blocks: [block]) + + // Then + Verify(persistence, 1, .deleteDownloadDataTask(id: .value(block.id))) + } + + func testFileUrl_ForFinishedTask_ShouldReturnCorrectUrl() { + // Given + let task = createMockDownloadTask(state: .finished) + let mockUser = DataLayer.User( + id: 1, + username: "test", + email: "test@test.com", + name: "Test User" + ) + + Given(storage, .user(getter: mockUser)) + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .isMobileData(getter: false)) + Given(persistence, .downloadDataTask(for: .value(task.id), willReturn: task)) + + let downloadManager = DownloadManager( + persistence: persistence, + appStorage: storage, + connectivity: connectivity + ) + + // When + let url = downloadManager.fileUrl(for: task.id) + + // Then + XCTAssertNotNil(url) + Verify(persistence, 1, .downloadDataTask(for: .value(task.id))) + XCTAssertEqual(url?.lastPathComponent, task.fileName) + } + + // MARK: - Test Video Size Calculation + + func testIsLargeVideosSize_WhenOver1GB_ShouldReturnTrue() { + // Given + let blocks = [createMockCourseBlock(videoSize: 1_200_000_000)] // 1.2 GB + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .isMobileData(getter: false)) + + let downloadManager = DownloadManager( + persistence: persistence, + appStorage: storage, + connectivity: connectivity + ) + + // When + let isLarge = downloadManager.isLargeVideosSize(blocks: blocks) + + // Then + XCTAssertTrue(isLarge) + } + + func testIsLargeVideosSize_WhenUnder1GB_ShouldReturnFalse() { + // Given + let blocks = [createMockCourseBlock(videoSize: 500_000_000)] // 500 MB + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .isMobileData(getter: false)) + + let downloadManager = DownloadManager( + persistence: persistence, + appStorage: storage, + connectivity: connectivity + ) + + // When + let isLarge = downloadManager.isLargeVideosSize(blocks: blocks) + + // Then + XCTAssertFalse(isLarge) + } + + // MARK: - Test Download Tasks Retrieval + + func testGetDownloadTasks_ShouldReturnAllTasks() async { + // Given + let expectedTasks = [ + createMockDownloadTask(id: "1"), + createMockDownloadTask(id: "2") + ] + + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(connectivity, .isMobileData(getter: false)) + Given(persistence, .getDownloadDataTasks(willReturn: expectedTasks)) + + let downloadManager = DownloadManager( + persistence: persistence, + appStorage: storage, + connectivity: connectivity + ) + + // When + let tasks = await downloadManager.getDownloadTasks() + + // Then + Verify(persistence, 1, .getDownloadDataTasks()) + XCTAssertEqual(tasks.count, expectedTasks.count) + XCTAssertEqual(tasks[0].id, expectedTasks[0].id) + XCTAssertEqual(tasks[1].id, expectedTasks[1].id) + } + + // MARK: - Helper Methods + + private func createMockDownloadTask( + id: String = "test123", + courseId: String = "course123", + state: DownloadState = .waiting + ) -> DownloadDataTask { + DownloadDataTask( + id: id, + blockId: "block123", + courseId: courseId, + userId: 1, + url: "https://test.com/video.mp4", + fileName: "video.mp4", + displayName: "Test Video", + progress: 0, + resumeData: nil, + state: state, + type: .video, + fileSize: 1000, + lastModified: "2024-01-01" + ) + } + + private func createMockCourseBlock(videoSize: Int = 1000) -> CourseBlock { + CourseBlock( + blockId: "block123", + id: "test123", + courseId: "course123", + graded: false, + due: nil, + completion: 0, + type: .video, + displayName: "Test Video", + studentUrl: "https://test.com", + webUrl: "https://test.com", + encodedVideo: CourseBlockEncodedVideo( + fallback: CourseBlockVideo( + url: "https://test.com/video.mp4", + fileSize: videoSize, + streamPriority: 1 + ), + youtube: nil, + desktopMP4: nil, + mobileHigh: nil, + mobileLow: nil, + hls: nil + ), + multiDevice: true, + offlineDownload: nil + ) + } +} diff --git a/Core/Mockfile b/Core/Mockfile new file mode 100644 index 000000000..86cfe49c3 --- /dev/null +++ b/Core/Mockfile @@ -0,0 +1,16 @@ +sourceryCommand: mint run krzysztofzablocki/Sourcery@2.1.2 sourcery +sourceryTemplate: ../MockTemplate.swifttemplate +unit.tests.mock: + sources: + include: + - ./../Core + - ./Core + exclude: [] + output: ./CoreTests/CoreMock.generated.swift + targets: + - MyAppUnitTests + import: + - Core + - Foundation + - SwiftUI + - Combine \ No newline at end of file diff --git a/Course/Course.xcodeproj/project.pbxproj b/Course/Course.xcodeproj/project.pbxproj index a118f173b..930494938 100644 --- a/Course/Course.xcodeproj/project.pbxproj +++ b/Course/Course.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 02197DC52C1AFC0600CC8FF2 /* CalendarSyncStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EBC7542C19CFCF00BE182C /* CalendarSyncStatusView.swift */; }; + 02228B2F2C221412009A5F28 /* LargestDownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02228B2E2C221412009A5F28 /* LargestDownloadsView.swift */; }; 02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5C294B4FDA0032823A /* CourseCoreModel.xcdatamodeld */; }; 02280F60294B50030032823A /* CoursePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */; }; 022C64D829ACEC48000F532B /* HandoutsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022C64D729ACEC48000F532B /* HandoutsView.swift */; }; @@ -25,7 +27,7 @@ 02454CA02A2618E70043052A /* YouTubeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454C9F2A2618E70043052A /* YouTubeView.swift */; }; 02454CA22A26190A0043052A /* EncodedVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA12A26190A0043052A /* EncodedVideoView.swift */; }; 02454CA42A26193F0043052A /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA32A26193F0043052A /* WebView.swift */; }; - 02454CA62A26196C0043052A /* UnknownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA52A26196C0043052A /* UnknownView.swift */; }; + 02454CA62A26196C0043052A /* NotAvailableOnMobileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA52A26196C0043052A /* NotAvailableOnMobileView.swift */; }; 02454CA82A2619890043052A /* DiscussionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA72A2619890043052A /* DiscussionView.swift */; }; 02454CAA2A2619B40043052A /* LessonProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02454CA92A2619B40043052A /* LessonProgressView.swift */; }; 0248C92729C097EB00DC8402 /* CourseVerticalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */; }; @@ -36,6 +38,7 @@ 0270210328E736E700F54332 /* CourseOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0270210128E736E700F54332 /* CourseOutlineView.swift */; }; 0276D75B29DDA3890004CDF8 /* Data_ResumeBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0276D75A29DDA3890004CDF8 /* Data_ResumeBlock.swift */; }; 0276D75D29DDA3F80004CDF8 /* ResumeBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */; }; + 02868AE52C19FE0B0003E339 /* DownloadActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02868AE42C19FE0B0003E339 /* DownloadActionView.swift */; }; 0289F90228E1C3E10064F8F3 /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = 0289F90128E1C3E00064F8F3 /* swiftgen.yml */; }; 0295B1D9297E6DF8003B0C65 /* CourseUnitViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295B1D8297E6DF8003B0C65 /* CourseUnitViewModelTests.swift */; }; 0295C889299BBE8200ABE571 /* CourseNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295C888299BBE8200ABE571 /* CourseNavigationView.swift */; }; @@ -45,18 +48,31 @@ 02B6B3BC28E1D14F00232911 /* CourseRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3BB28E1D14F00232911 /* CourseRepository.swift */; }; 02B6B3BE28E1D15C00232911 /* CourseEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B6B3BD28E1D15C00232911 /* CourseEndpoint.swift */; }; 02B6B3C928E1E68100232911 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02B6B3C828E1E68100232911 /* Core.framework */; }; + 02BB20182BFCE7B200364948 /* CustomDisclosureGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BB20172BFCE7B200364948 /* CustomDisclosureGroup.swift */; }; + 02C355392C08DCD700501342 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 02C355372C08DCD700501342 /* Localizable.stringsdict */; }; + 02C7B1D82C271A7000D2A7BB /* OfflineContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C7B1D72C271A7000D2A7BB /* OfflineContentView.swift */; }; 02D4FC2E2BBD7C9C00C47748 /* MessageSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D4FC2D2BBD7C9C00C47748 /* MessageSectionView.swift */; }; + 02E3803E2BFF9F0A00815AFA /* CourseProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E3803D2BFF9F0A00815AFA /* CourseProgressView.swift */; }; 02F0144F28F46474002E513D /* CourseContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F0144E28F46474002E513D /* CourseContainerView.swift */; }; 02F0145728F4A2FF002E513D /* CourseContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F0145628F4A2FF002E513D /* CourseContainerViewModel.swift */; }; - 02F066E829DC71750073E13B /* SubtittlesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F066E729DC71750073E13B /* SubtittlesView.swift */; }; - 02F175372A4DAFD20019CD70 /* CourseAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F175362A4DAFD20019CD70 /* CourseAnalytics.swift */; }; 02F3BFDD29252E900051930C /* CourseRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFDC29252E900051930C /* CourseRouter.swift */; }; + 02F71B4A2C1B163B00FF936A /* DownloadErrorAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F71B492C1B163A00FF936A /* DownloadErrorAlertView.swift */; }; + 02F71B4C2C1B200900FF936A /* DeviceStorageFullAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F71B4B2C1B200900FF936A /* DeviceStorageFullAlertView.swift */; }; 02F78AEB29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F78AEA29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift */; }; 02F98A8128F8224200DE94C0 /* Discussion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02F98A8028F8224200DE94C0 /* Discussion.framework */; }; 02FCB2B32BBEB36600373180 /* CourseHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FCB2B22BBEB36600373180 /* CourseHeaderView.swift */; }; + 02FF6FA72C20BFF800E44DD8 /* OfflineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FF6FA62C20BFF800E44DD8 /* OfflineView.swift */; }; + 02FF6FAA2C20D56A00E44DD8 /* TotalDownloadedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FF6FA92C20D56A00E44DD8 /* TotalDownloadedProgressView.swift */; }; 02FFAD0D29E4347300140E46 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FFAD0C29E4347300140E46 /* VideoPlayerViewModel.swift */; }; 060E8BCA2B5FD68C0080C952 /* UnitStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060E8BC92B5FD68C0080C952 /* UnitStack.swift */; }; 065275352BB1B39C0093BCCA /* PlayerViewControllerHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065275342BB1B39C0093BCCA /* PlayerViewControllerHolder.swift */; }; + 067B7B4E2BED339200D1768F /* PlayerTrackerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B7B472BED339200D1768F /* PlayerTrackerProtocol.swift */; }; + 067B7B4F2BED339200D1768F /* PlayerDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B7B482BED339200D1768F /* PlayerDelegateProtocol.swift */; }; + 067B7B502BED339200D1768F /* PlayerControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B7B492BED339200D1768F /* PlayerControllerProtocol.swift */; }; + 067B7B512BED339200D1768F /* PipManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B7B4A2BED339200D1768F /* PipManagerProtocol.swift */; }; + 067B7B522BED339200D1768F /* SubtitlesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B7B4B2BED339200D1768F /* SubtitlesView.swift */; }; + 067B7B532BED339200D1768F /* PlayerServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B7B4C2BED339200D1768F /* PlayerServiceProtocol.swift */; }; + 067B7B542BED339200D1768F /* YoutubePlayerViewControllerHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B7B4D2BED339200D1768F /* YoutubePlayerViewControllerHolder.swift */; }; 068DDA5F2B1E198700FF8CCB /* CourseUnitDropDownList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068DDA5B2B1E198700FF8CCB /* CourseUnitDropDownList.swift */; }; 068DDA602B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068DDA5C2B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift */; }; 068DDA612B1E198700FF8CCB /* CourseUnitDropDownCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068DDA5D2B1E198700FF8CCB /* CourseUnitDropDownCell.swift */; }; @@ -67,29 +83,28 @@ 0766DFCC299AA7A600EBEF6A /* YouTubeVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0766DFCB299AA7A600EBEF6A /* YouTubeVideoPlayer.swift */; }; 0766DFCE299AB26D00EBEF6A /* EncodedVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0766DFCD299AB26D00EBEF6A /* EncodedVideoPlayer.swift */; }; 0766DFD0299AB29000EBEF6A /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0766DFCF299AB29000EBEF6A /* PlayerViewController.swift */; }; + 07DE59862BECB868001CBFBC /* CourseAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07DE59852BECB868001CBFBC /* CourseAnalytics.swift */; }; 197FB8EA8F92F00A8F383D82 /* Pods_App_Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E5E795BD160CDA7D9C120DE6 /* Pods_App_Course.framework */; }; 975F475E2B6151FD00E5B031 /* CourseDatesMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975F475D2B6151FD00E5B031 /* CourseDatesMock.swift */; }; 975F47602B615DA700E5B031 /* CourseStructureMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975F475F2B615DA700E5B031 /* CourseStructureMock.swift */; }; 97C99C362B9A08FE004EEDE2 /* CalendarSyncProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C99C352B9A08FE004EEDE2 /* CalendarSyncProgressView.swift */; }; 97CA95252B875EE200A9EDEA /* DatesSuccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97CA95242B875EE200A9EDEA /* DatesSuccessView.swift */; }; 97E7DF0F2B7C852A00A2A09B /* DatesStatusInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E7DF0E2B7C852A00A2A09B /* DatesStatusInfoView.swift */; }; - 97EA4D862B85034D00663F58 /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97EA4D852B85034D00663F58 /* CalendarManager.swift */; }; B8F50317B6B830A0E520C954 /* Pods_App_Course_CourseTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50E59D2B81E12610964282C5 /* Pods_App_Course_CourseTests.framework */; }; BA58CF5D2B3D804D005B102E /* CourseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF5C2B3D804D005B102E /* CourseStorage.swift */; }; BA58CF612B471041005B102E /* VideoDownloadQualityBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF602B471041005B102E /* VideoDownloadQualityBarView.swift */; }; BA58CF642B471363005B102E /* VideoDownloadQualityContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA58CF632B471363005B102E /* VideoDownloadQualityContainerView.swift */; }; - BAAD62C82AFD00EE000E6103 /* CourseStructureNestedListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAD62C72AFD00EE000E6103 /* CourseStructureNestedListView.swift */; }; BAC0E0D82B32EF03006B68A9 /* DownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC0E0D72B32EF03006B68A9 /* DownloadsView.swift */; }; BAC0E0DB2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC0E0DA2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift */; }; BAC0E0DE2B32F0F3006B68A9 /* DownloadsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAC0E0DD2B32F0F3006B68A9 /* DownloadsViewModel.swift */; }; BAD9CA2D2B2736BB00DE790A /* LessonLineProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA2C2B2736BB00DE790A /* LessonLineProgressView.swift */; }; - BAD9CA442B2C87A200DE790A /* CourseStructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA432B2C87A200DE790A /* CourseStructureView.swift */; }; BAD9CA4A2B2C88E000DE790A /* CourseVideoDownloadBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA492B2C88E000DE790A /* CourseVideoDownloadBarView.swift */; }; + CE7CAF412CC1563500E0AC9D /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CE7CAF402CC1563500E0AC9D /* OEXFoundation */; }; + CEB1E2732CC14EC400921517 /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CEB1E2722CC14EC400921517 /* OEXFoundation */; }; + CEBCA4342CC13CDE00076589 /* YouTubePlayerKit in Frameworks */ = {isa = PBXBuildFile; productRef = CEBCA4332CC13CDE00076589 /* YouTubePlayerKit */; }; DB205BFB2AE81B1200136EC2 /* CourseDateViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */; }; DB7D6EAC2ADFCAC50036BB13 /* CourseDatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */; }; DB7D6EAE2ADFCB4A0036BB13 /* CourseDatesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAD2ADFCB4A0036BB13 /* CourseDatesViewModel.swift */; }; - DB7D6EB02ADFDA0E0036BB13 /* CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EAF2ADFDA0E0036BB13 /* CourseDates.swift */; }; - DB7D6EB22ADFE9510036BB13 /* Data_CourseDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7D6EB12ADFE9510036BB13 /* Data_CourseDates.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -102,7 +117,21 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + CE7CAF432CC1563500E0AC9D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ + 02228B2E2C221412009A5F28 /* LargestDownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargestDownloadsView.swift; sourceTree = ""; }; 02280F5D294B4FDA0032823A /* CourseCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CourseCoreModel.xcdatamodel; sourceTree = ""; }; 02280F5F294B50030032823A /* CoursePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursePersistenceProtocol.swift; sourceTree = ""; }; 022C64D729ACEC48000F532B /* HandoutsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandoutsView.swift; sourceTree = ""; }; @@ -121,7 +150,7 @@ 02454C9F2A2618E70043052A /* YouTubeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeView.swift; sourceTree = ""; }; 02454CA12A26190A0043052A /* EncodedVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodedVideoView.swift; sourceTree = ""; }; 02454CA32A26193F0043052A /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; - 02454CA52A26196C0043052A /* UnknownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnknownView.swift; sourceTree = ""; }; + 02454CA52A26196C0043052A /* NotAvailableOnMobileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotAvailableOnMobileView.swift; sourceTree = ""; }; 02454CA72A2619890043052A /* DiscussionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionView.swift; sourceTree = ""; }; 02454CA92A2619B40043052A /* LessonProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonProgressView.swift; sourceTree = ""; }; 0248C92629C097EB00DC8402 /* CourseVerticalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVerticalViewModel.swift; sourceTree = ""; }; @@ -132,6 +161,7 @@ 0270210128E736E700F54332 /* CourseOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseOutlineView.swift; sourceTree = ""; }; 0276D75A29DDA3890004CDF8 /* Data_ResumeBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_ResumeBlock.swift; sourceTree = ""; }; 0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumeBlock.swift; sourceTree = ""; }; + 02868AE42C19FE0B0003E339 /* DownloadActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadActionView.swift; sourceTree = ""; }; 0289F8EE28E1C3510064F8F3 /* Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Course.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0289F90128E1C3E00064F8F3 /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; 0295B1D8297E6DF8003B0C65 /* CourseUnitViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseUnitViewModelTests.swift; sourceTree = ""; }; @@ -143,19 +173,33 @@ 02B6B3BD28E1D15C00232911 /* CourseEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseEndpoint.swift; sourceTree = ""; }; 02B6B3C228E1DCD100232911 /* CourseDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDetails.swift; sourceTree = ""; }; 02B6B3C828E1E68100232911 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 02BB20172BFCE7B200364948 /* CustomDisclosureGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDisclosureGroup.swift; sourceTree = ""; }; + 02C355382C08DCD700501342 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; + 02C3553A2C08DCE000501342 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; + 02C7B1D72C271A7000D2A7BB /* OfflineContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineContentView.swift; sourceTree = ""; }; 02D4FC2D2BBD7C9C00C47748 /* MessageSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSectionView.swift; sourceTree = ""; }; - 02ED50CF29A64BB6008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + 02E3803D2BFF9F0A00815AFA /* CourseProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseProgressView.swift; sourceTree = ""; }; + 02EBC7542C19CFCF00BE182C /* CalendarSyncStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarSyncStatusView.swift; sourceTree = ""; }; 02F0144E28F46474002E513D /* CourseContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseContainerView.swift; sourceTree = ""; }; 02F0145628F4A2FF002E513D /* CourseContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseContainerViewModel.swift; sourceTree = ""; }; - 02F066E729DC71750073E13B /* SubtittlesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtittlesView.swift; sourceTree = ""; }; - 02F175362A4DAFD20019CD70 /* CourseAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CourseAnalytics.swift; path = ../Presentation/CourseAnalytics.swift; sourceTree = ""; }; 02F3BFDC29252E900051930C /* CourseRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseRouter.swift; sourceTree = ""; }; + 02F71B492C1B163A00FF936A /* DownloadErrorAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadErrorAlertView.swift; sourceTree = ""; }; + 02F71B4B2C1B200900FF936A /* DeviceStorageFullAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStorageFullAlertView.swift; sourceTree = ""; }; 02F78AEA29E6BCA20038DE30 /* VideoPlayerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerViewModelTests.swift; path = CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift; sourceTree = SOURCE_ROOT; }; 02F98A8028F8224200DE94C0 /* Discussion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discussion.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 02FCB2B22BBEB36600373180 /* CourseHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseHeaderView.swift; sourceTree = ""; }; + 02FF6FA62C20BFF800E44DD8 /* OfflineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineView.swift; sourceTree = ""; }; + 02FF6FA92C20D56A00E44DD8 /* TotalDownloadedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalDownloadedProgressView.swift; sourceTree = ""; }; 02FFAD0C29E4347300140E46 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = ""; }; 060E8BC92B5FD68C0080C952 /* UnitStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitStack.swift; sourceTree = ""; }; 065275342BB1B39C0093BCCA /* PlayerViewControllerHolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerViewControllerHolder.swift; sourceTree = ""; }; + 067B7B472BED339200D1768F /* PlayerTrackerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerTrackerProtocol.swift; sourceTree = ""; }; + 067B7B482BED339200D1768F /* PlayerDelegateProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerDelegateProtocol.swift; sourceTree = ""; }; + 067B7B492BED339200D1768F /* PlayerControllerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerControllerProtocol.swift; sourceTree = ""; }; + 067B7B4A2BED339200D1768F /* PipManagerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PipManagerProtocol.swift; sourceTree = ""; }; + 067B7B4B2BED339200D1768F /* SubtitlesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubtitlesView.swift; sourceTree = ""; }; + 067B7B4C2BED339200D1768F /* PlayerServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerServiceProtocol.swift; sourceTree = ""; }; + 067B7B4D2BED339200D1768F /* YoutubePlayerViewControllerHolder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubePlayerViewControllerHolder.swift; sourceTree = ""; }; 068DDA5B2B1E198700FF8CCB /* CourseUnitDropDownList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseUnitDropDownList.swift; sourceTree = ""; }; 068DDA5C2B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseUnitVerticalsDropdownView.swift; sourceTree = ""; }; 068DDA5D2B1E198700FF8CCB /* CourseUnitDropDownCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseUnitDropDownCell.swift; sourceTree = ""; }; @@ -166,6 +210,7 @@ 0766DFCB299AA7A600EBEF6A /* YouTubeVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YouTubeVideoPlayer.swift; sourceTree = ""; }; 0766DFCD299AB26D00EBEF6A /* EncodedVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodedVideoPlayer.swift; sourceTree = ""; }; 0766DFCF299AB29000EBEF6A /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; + 07DE59852BECB868001CBFBC /* CourseAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseAnalytics.swift; sourceTree = ""; }; 2A444220A08C5035164B071F /* Pods-App-Course-CourseTests.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course-CourseTests.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests.releasedev.xcconfig"; sourceTree = ""; }; 3A55620C6018088BFF77F9AE /* Pods-App-CourseDetails.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-CourseDetails.debug.xcconfig"; path = "Target Support Files/Pods-App-CourseDetails/Pods-App-CourseDetails.debug.xcconfig"; sourceTree = ""; }; 3D506212980347A9D5A70E20 /* Pods-App-Course.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Course/Pods-App-Course.debugstage.xcconfig"; sourceTree = ""; }; @@ -182,7 +227,6 @@ 97C99C352B9A08FE004EEDE2 /* CalendarSyncProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarSyncProgressView.swift; sourceTree = ""; }; 97CA95242B875EE200A9EDEA /* DatesSuccessView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatesSuccessView.swift; sourceTree = ""; }; 97E7DF0E2B7C852A00A2A09B /* DatesStatusInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatesStatusInfoView.swift; sourceTree = ""; }; - 97EA4D852B85034D00663F58 /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = ""; }; 99AEF08FD75F1509863D3302 /* Pods-App-CourseDetails.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-CourseDetails.debugprod.xcconfig"; path = "Target Support Files/Pods-App-CourseDetails/Pods-App-CourseDetails.debugprod.xcconfig"; sourceTree = ""; }; 9B5D3D31A9CFA08B6C4347BD /* Pods-App-CourseDetails.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-CourseDetails.releasedev.xcconfig"; path = "Target Support Files/Pods-App-CourseDetails/Pods-App-CourseDetails.releasedev.xcconfig"; sourceTree = ""; }; A47C63D9EB0D866F303D4588 /* Pods-App-Course.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Course/Pods-App-Course.releasestage.xcconfig"; sourceTree = ""; }; @@ -191,18 +235,14 @@ BA58CF5C2B3D804D005B102E /* CourseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStorage.swift; sourceTree = ""; }; BA58CF602B471041005B102E /* VideoDownloadQualityBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadQualityBarView.swift; sourceTree = ""; }; BA58CF632B471363005B102E /* VideoDownloadQualityContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDownloadQualityContainerView.swift; sourceTree = ""; }; - BAAD62C72AFD00EE000E6103 /* CourseStructureNestedListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStructureNestedListView.swift; sourceTree = ""; }; BAC0E0D72B32EF03006B68A9 /* DownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsView.swift; sourceTree = ""; }; BAC0E0DA2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVideoDownloadBarViewModel.swift; sourceTree = ""; }; BAC0E0DD2B32F0F3006B68A9 /* DownloadsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsViewModel.swift; sourceTree = ""; }; BAD9CA2C2B2736BB00DE790A /* LessonLineProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LessonLineProgressView.swift; sourceTree = ""; }; - BAD9CA432B2C87A200DE790A /* CourseStructureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStructureView.swift; sourceTree = ""; }; BAD9CA492B2C88E000DE790A /* CourseVideoDownloadBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseVideoDownloadBarView.swift; sourceTree = ""; }; DB205BFA2AE81B1200136EC2 /* CourseDateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDateViewModelTests.swift; sourceTree = ""; }; DB7D6EAB2ADFCAC40036BB13 /* CourseDatesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CourseDatesView.swift; sourceTree = ""; }; DB7D6EAD2ADFCB4A0036BB13 /* CourseDatesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDatesViewModel.swift; sourceTree = ""; }; - DB7D6EAF2ADFDA0E0036BB13 /* CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDates.swift; sourceTree = ""; }; - DB7D6EB12ADFE9510036BB13 /* Data_CourseDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data_CourseDates.swift; sourceTree = ""; }; DBE05972CB5115D4535C6B8A /* Pods-App-Course-CourseTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course-CourseTests.debug.xcconfig"; path = "Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests.debug.xcconfig"; sourceTree = ""; }; E5E795BD160CDA7D9C120DE6 /* Pods_App_Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Course.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E6BDAE887ED8A46860B3F6D3 /* Pods-App-Course-CourseTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Course-CourseTests.release.xcconfig"; path = "Target Support Files/Pods-App-Course-CourseTests/Pods-App-Course-CourseTests.release.xcconfig"; sourceTree = ""; }; @@ -219,6 +259,7 @@ buildActionMask = 2147483647; files = ( 023812E8297AC8EB0087098F /* Course.framework in Frameworks */, + CE7CAF412CC1563500E0AC9D /* OEXFoundation in Frameworks */, B8F50317B6B830A0E520C954 /* Pods_App_Course_CourseTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -229,7 +270,9 @@ files = ( 02B6B3C928E1E68100232911 /* Core.framework in Frameworks */, 197FB8EA8F92F00A8F383D82 /* Pods_App_Course.framework in Frameworks */, + CEBCA4342CC13CDE00076589 /* YouTubePlayerKit in Frameworks */, 02F98A8128F8224200DE94C0 /* Discussion.framework in Frameworks */, + CEB1E2732CC14EC400921517 /* OEXFoundation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -261,15 +304,26 @@ 02454C9F2A2618E70043052A /* YouTubeView.swift */, 02454CA12A26190A0043052A /* EncodedVideoView.swift */, 02454CA32A26193F0043052A /* WebView.swift */, - 02454CA52A26196C0043052A /* UnknownView.swift */, + 02454CA52A26196C0043052A /* NotAvailableOnMobileView.swift */, 02454CA72A2619890043052A /* DiscussionView.swift */, 02454CA92A2619B40043052A /* LessonProgressView.swift */, BAD9CA2C2B2736BB00DE790A /* LessonLineProgressView.swift */, 060E8BC92B5FD68C0080C952 /* UnitStack.swift */, + 02C7B1D72C271A7000D2A7BB /* OfflineContentView.swift */, ); path = Subviews; sourceTree = ""; }; + 02868AE32C19FDF10003E339 /* ActionViews */ = { + isa = PBXGroup; + children = ( + 02868AE42C19FE0B0003E339 /* DownloadActionView.swift */, + 02F71B492C1B163A00FF936A /* DownloadErrorAlertView.swift */, + 02F71B4B2C1B200900FF936A /* DeviceStorageFullAlertView.swift */, + ); + path = ActionViews; + sourceTree = ""; + }; 0289F8E428E1C3510064F8F3 = { isa = PBXGroup; children = ( @@ -294,14 +348,12 @@ 0289F8F028E1C3510064F8F3 /* Course */ = { isa = PBXGroup; children = ( - 979A6AB92BC3FFF8001B0DE3 /* Analytics */, 02B6B3AD28E1C47100232911 /* SwiftGen */, 02B6B3B828E1D12900232911 /* Data */, 02B6B3B528E1D10700232911 /* Domain */, 02EAE2CA28E1F0A700529644 /* Presentation */, - 97EA4D822B84EFA900663F58 /* Managers */, - 97CA95212B875EA200A9EDEA /* Views */, 02B6B3B428E1C49400232911 /* Localizable.strings */, + 02C355372C08DCD700501342 /* Localizable.stringsdict */, ); path = Course; sourceTree = ""; @@ -351,7 +403,6 @@ 027020FB28E7362100F54332 /* Data_CourseOutlineResponse.swift */, 022C64DB29ACFDEE000F532B /* Data_HandoutsResponse.swift */, 022C64DF29ADEA9B000F532B /* Data_UpdatesResponse.swift */, - DB7D6EB12ADFE9510036BB13 /* Data_CourseDates.swift */, ); path = Model; sourceTree = ""; @@ -388,6 +439,7 @@ 02EAE2CA28E1F0A700529644 /* Presentation */ = { isa = PBXGroup; children = ( + 02FF6FA52C20BFE100E44DD8 /* Offline */, DB7D6EAA2ADFCAA00036BB13 /* Dates */, 070019A828F6F33600D5FC78 /* Container */, 070019A728F6F2D600D5FC78 /* Outline */, @@ -397,10 +449,29 @@ BAC0E0DC2B32F0EA006B68A9 /* Downloads */, BAD9CA482B2C88D500DE790A /* Subviews */, 02F3BFDC29252E900051930C /* CourseRouter.swift */, + 07DE59852BECB868001CBFBC /* CourseAnalytics.swift */, ); path = Presentation; sourceTree = ""; }; + 02FF6FA52C20BFE100E44DD8 /* Offline */ = { + isa = PBXGroup; + children = ( + 02FF6FA62C20BFF800E44DD8 /* OfflineView.swift */, + 02FF6FA82C20D53C00E44DD8 /* Subviews */, + ); + path = Offline; + sourceTree = ""; + }; + 02FF6FA82C20D53C00E44DD8 /* Subviews */ = { + isa = PBXGroup; + children = ( + 02FF6FA92C20D56A00E44DD8 /* TotalDownloadedProgressView.swift */, + 02228B2E2C221412009A5F28 /* LargestDownloadsView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; 068DDA5A2B1E198700FF8CCB /* DropdownList */ = { isa = PBXGroup; children = ( @@ -419,7 +490,6 @@ 02B6B3C228E1DCD100232911 /* CourseDetails.swift */, 0276D75C29DDA3F80004CDF8 /* ResumeBlock.swift */, 022C64E129ADEB83000F532B /* CourseUpdate.swift */, - DB7D6EAF2ADFDA0E0036BB13 /* CourseDates.swift */, ); path = Model; sourceTree = ""; @@ -428,7 +498,6 @@ isa = PBXGroup; children = ( BAD9CA462B2C888600DE790A /* CourseVertical */, - BAD9CA472B2C88AA00DE790A /* CourseStructure */, 02635AC62A24F181008062F2 /* ContinueWithView.swift */, 0270210128E736E700F54332 /* CourseOutlineView.swift */, ); @@ -459,8 +528,14 @@ 070019AA28F6F79E00D5FC78 /* Video */ = { isa = PBXGroup; children = ( + 067B7B4A2BED339200D1768F /* PipManagerProtocol.swift */, + 067B7B492BED339200D1768F /* PlayerControllerProtocol.swift */, + 067B7B482BED339200D1768F /* PlayerDelegateProtocol.swift */, + 067B7B4C2BED339200D1768F /* PlayerServiceProtocol.swift */, + 067B7B472BED339200D1768F /* PlayerTrackerProtocol.swift */, + 067B7B4B2BED339200D1768F /* SubtitlesView.swift */, + 067B7B4D2BED339200D1768F /* YoutubePlayerViewControllerHolder.swift */, 065275342BB1B39C0093BCCA /* PlayerViewControllerHolder.swift */, - 02F066E729DC71750073E13B /* SubtittlesView.swift */, 0766DFCB299AA7A600EBEF6A /* YouTubeVideoPlayer.swift */, 022F8E152A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift */, 0766DFCD299AB26D00EBEF6A /* EncodedVideoPlayer.swift */, @@ -508,29 +583,20 @@ path = Mock; sourceTree = ""; }; - 979A6AB92BC3FFF8001B0DE3 /* Analytics */ = { - isa = PBXGroup; - children = ( - 02F175362A4DAFD20019CD70 /* CourseAnalytics.swift */, - ); - path = Analytics; - sourceTree = ""; - }; - 97CA95212B875EA200A9EDEA /* Views */ = { + 9784D4752BF39EEF00AFEFFF /* CalendarSyncProgressView */ = { isa = PBXGroup; children = ( 97C99C352B9A08FE004EEDE2 /* CalendarSyncProgressView.swift */, - 97CA95242B875EE200A9EDEA /* DatesSuccessView.swift */, ); - path = Views; + path = CalendarSyncProgressView; sourceTree = ""; }; - 97EA4D822B84EFA900663F58 /* Managers */ = { + 9784D4762BF39EFD00AFEFFF /* DatesSuccessView */ = { isa = PBXGroup; children = ( - 97EA4D852B85034D00663F58 /* CalendarManager.swift */, + 97CA95242B875EE200A9EDEA /* DatesSuccessView.swift */, ); - path = Managers; + path = DatesSuccessView; sourceTree = ""; }; BA58CF622B471047005B102E /* VideoDownloadQualityBarView */ = { @@ -570,22 +636,19 @@ path = CourseVertical; sourceTree = ""; }; - BAD9CA472B2C88AA00DE790A /* CourseStructure */ = { - isa = PBXGroup; - children = ( - BAD9CA432B2C87A200DE790A /* CourseStructureView.swift */, - BAAD62C72AFD00EE000E6103 /* CourseStructureNestedListView.swift */, - ); - path = CourseStructure; - sourceTree = ""; - }; BAD9CA482B2C88D500DE790A /* Subviews */ = { isa = PBXGroup; children = ( + 02868AE32C19FDF10003E339 /* ActionViews */, + 9784D4762BF39EFD00AFEFFF /* DatesSuccessView */, + 9784D4752BF39EEF00AFEFFF /* CalendarSyncProgressView */, 02D4FC2C2BBD7C7500C47748 /* MessageSectionView */, BAC0E0D92B32F0A2006B68A9 /* CourseVideoDownloadBarView */, BA58CF622B471047005B102E /* VideoDownloadQualityBarView */, 02FCB2B22BBEB36600373180 /* CourseHeaderView.swift */, + 02BB20172BFCE7B200364948 /* CustomDisclosureGroup.swift */, + 02E3803D2BFF9F0A00815AFA /* CourseProgressView.swift */, + 02EBC7542C19CFCF00BE182C /* CalendarSyncStatusView.swift */, ); path = Subviews; sourceTree = ""; @@ -652,6 +715,7 @@ 023812E1297AC8EA0087098F /* Frameworks */, 023812E2297AC8EA0087098F /* Resources */, 92C3B3183886DDECE1CBAC22 /* [CP] Copy Pods Resources */, + CE7CAF432CC1563500E0AC9D /* Embed Frameworks */, ); buildRules = ( ); @@ -712,6 +776,10 @@ uk, ); mainGroup = 0289F8E428E1C3510064F8F3; + packageReferences = ( + CEBCA4322CC13CDE00076589 /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */, + CEB1E2712CC14EC400921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */, + ); productRefGroup = 0289F8EF28E1C3510064F8F3 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -736,6 +804,7 @@ files = ( 0289F90228E1C3E10064F8F3 /* swiftgen.yml in Resources */, 02B6B3B228E1C49400232911 /* Localizable.strings in Resources */, + 02C355392C08DCD700501342 /* Localizable.stringsdict in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -842,6 +911,7 @@ buildActionMask = 2147483647; files = ( 06FD7EE32B1F3FF6008D632B /* DropdownAnimationModifier.swift in Sources */, + 067B7B542BED339200D1768F /* YoutubePlayerViewControllerHolder.swift in Sources */, 02FFAD0D29E4347300140E46 /* VideoPlayerViewModel.swift in Sources */, 02454CA42A26193F0043052A /* WebView.swift in Sources */, 022C64DA29ACEC50000F532B /* HandoutsViewModel.swift in Sources */, @@ -850,8 +920,10 @@ BAD9CA4A2B2C88E000DE790A /* CourseVideoDownloadBarView.swift in Sources */, 022C64DE29AD167A000F532B /* HandoutsUpdatesDetailView.swift in Sources */, BA58CF612B471041005B102E /* VideoDownloadQualityBarView.swift in Sources */, + 02FF6FA72C20BFF800E44DD8 /* OfflineView.swift in Sources */, 0270210328E736E700F54332 /* CourseOutlineView.swift in Sources */, 068DDA602B1E198700FF8CCB /* CourseUnitVerticalsDropdownView.swift in Sources */, + 067B7B512BED339200D1768F /* PipManagerProtocol.swift in Sources */, 022C64E029ADEA9B000F532B /* Data_UpdatesResponse.swift in Sources */, 02D4FC2E2BBD7C9C00C47748 /* MessageSectionView.swift in Sources */, 02454CA02A2618E70043052A /* YouTubeView.swift in Sources */, @@ -863,39 +935,47 @@ 02280F60294B50030032823A /* CoursePersistenceProtocol.swift in Sources */, 02454CAA2A2619B40043052A /* LessonProgressView.swift in Sources */, 975F475E2B6151FD00E5B031 /* CourseDatesMock.swift in Sources */, - DB7D6EB22ADFE9510036BB13 /* Data_CourseDates.swift in Sources */, + 02FF6FAA2C20D56A00E44DD8 /* TotalDownloadedProgressView.swift in Sources */, 975F47602B615DA700E5B031 /* CourseStructureMock.swift in Sources */, 02280F5E294B4FDA0032823A /* CourseCoreModel.xcdatamodeld in Sources */, 0766DFCE299AB26D00EBEF6A /* EncodedVideoPlayer.swift in Sources */, + 02C7B1D82C271A7000D2A7BB /* OfflineContentView.swift in Sources */, 0276D75B29DDA3890004CDF8 /* Data_ResumeBlock.swift in Sources */, 0276D75D29DDA3F80004CDF8 /* ResumeBlock.swift in Sources */, 02F3BFDD29252E900051930C /* CourseRouter.swift in Sources */, 068DDA5F2B1E198700FF8CCB /* CourseUnitDropDownList.swift in Sources */, 022F8E182A1E2642008EFAB9 /* EncodedVideoPlayerViewModel.swift in Sources */, 0248C92729C097EB00DC8402 /* CourseVerticalViewModel.swift in Sources */, - BAD9CA442B2C87A200DE790A /* CourseStructureView.swift in Sources */, 02F0145728F4A2FF002E513D /* CourseContainerViewModel.swift in Sources */, 02B6B3B728E1D11E00232911 /* CourseInteractor.swift in Sources */, 073512E229C0E400005CFA41 /* BaseCourseViewModel.swift in Sources */, 0231124F28EDA811002588FB /* CourseUnitViewModel.swift in Sources */, 02F0144F28F46474002E513D /* CourseContainerView.swift in Sources */, - BAAD62C82AFD00EE000E6103 /* CourseStructureNestedListView.swift in Sources */, 02A8076829474831007F53AB /* CourseVerticalView.swift in Sources */, 97E7DF0F2B7C852A00A2A09B /* DatesStatusInfoView.swift in Sources */, + 067B7B4F2BED339200D1768F /* PlayerDelegateProtocol.swift in Sources */, 0231124D28EDA804002588FB /* CourseUnitView.swift in Sources */, 027020FC28E7362100F54332 /* Data_CourseOutlineResponse.swift in Sources */, - DB7D6EB02ADFDA0E0036BB13 /* CourseDates.swift in Sources */, + 02F71B4A2C1B163B00FF936A /* DownloadErrorAlertView.swift in Sources */, + 067B7B532BED339200D1768F /* PlayerServiceProtocol.swift in Sources */, BAD9CA2D2B2736BB00DE790A /* LessonLineProgressView.swift in Sources */, + 02197DC52C1AFC0600CC8FF2 /* CalendarSyncStatusView.swift in Sources */, 060E8BCA2B5FD68C0080C952 /* UnitStack.swift in Sources */, + 02228B2F2C221412009A5F28 /* LargestDownloadsView.swift in Sources */, 0295C889299BBE8200ABE571 /* CourseNavigationView.swift in Sources */, 06FD7EDF2B1F29F3008D632B /* CourseVerticalImageView.swift in Sources */, BAC0E0DB2B32F0AE006B68A9 /* CourseVideoDownloadBarViewModel.swift in Sources */, DB7D6EAE2ADFCB4A0036BB13 /* CourseDatesViewModel.swift in Sources */, - 02F066E829DC71750073E13B /* SubtittlesView.swift in Sources */, + 067B7B522BED339200D1768F /* SubtitlesView.swift in Sources */, + 02868AE52C19FE0B0003E339 /* DownloadActionView.swift in Sources */, + 07DE59862BECB868001CBFBC /* CourseAnalytics.swift in Sources */, + 02BB20182BFCE7B200364948 /* CustomDisclosureGroup.swift in Sources */, 022C64E229ADEB83000F532B /* CourseUpdate.swift in Sources */, BA58CF642B471363005B102E /* VideoDownloadQualityContainerView.swift in Sources */, BA58CF5D2B3D804D005B102E /* CourseStorage.swift in Sources */, - 02454CA62A26196C0043052A /* UnknownView.swift in Sources */, + 067B7B502BED339200D1768F /* PlayerControllerProtocol.swift in Sources */, + 067B7B4E2BED339200D1768F /* PlayerTrackerProtocol.swift in Sources */, + 02454CA62A26196C0043052A /* NotAvailableOnMobileView.swift in Sources */, 0766DFD0299AB29000EBEF6A /* PlayerViewController.swift in Sources */, 022C64DC29ACFDEE000F532B /* Data_HandoutsResponse.swift in Sources */, 022C64D829ACEC48000F532B /* HandoutsView.swift in Sources */, @@ -907,10 +987,10 @@ 02FCB2B32BBEB36600373180 /* CourseHeaderView.swift in Sources */, DB7D6EAC2ADFCAC50036BB13 /* CourseDatesView.swift in Sources */, 0766DFCC299AA7A600EBEF6A /* YouTubeVideoPlayer.swift in Sources */, + 02E3803E2BFF9F0A00815AFA /* CourseProgressView.swift in Sources */, 022F8E162A1DFBC6008EFAB9 /* YouTubeVideoPlayerViewModel.swift in Sources */, - 02F175372A4DAFD20019CD70 /* CourseAnalytics.swift in Sources */, 02B6B3BE28E1D15C00232911 /* CourseEndpoint.swift in Sources */, - 97EA4D862B85034D00663F58 /* CalendarManager.swift in Sources */, + 02F71B4C2C1B200900FF936A /* DeviceStorageFullAlertView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -930,11 +1010,19 @@ isa = PBXVariantGroup; children = ( 02B6B3B328E1C49400232911 /* en */, - 02ED50CF29A64BB6008341CD /* uk */, ); name = Localizable.strings; sourceTree = ""; }; + 02C355372C08DCD700501342 /* Localizable.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + 02C355382C08DCD700501342 /* en */, + 02C3553A2C08DCE000501342 /* uk */, + ); + name = Localizable.stringsdict; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -946,7 +1034,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -967,7 +1055,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -988,7 +1076,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1009,7 +1097,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1030,7 +1118,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1051,7 +1139,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1194,14 +1282,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1229,14 +1317,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1327,14 +1415,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1426,14 +1514,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1519,14 +1607,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1611,14 +1699,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1709,14 +1797,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1744,7 +1832,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1823,14 +1911,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1857,7 +1945,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.CourseTests; @@ -1920,6 +2008,43 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + CEB1E2712CC14EC400921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; + requirement = { + kind = exactVersion; + version = 1.0.0; + }; + }; + CEBCA4322CC13CDE00076589 /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SvenTiigi/YouTubePlayerKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.9.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + CE7CAF402CC1563500E0AC9D /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E2712CC14EC400921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; + CEB1E2722CC14EC400921517 /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E2712CC14EC400921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; + CEBCA4332CC13CDE00076589 /* YouTubePlayerKit */ = { + isa = XCSwiftPackageProductDependency; + package = CEBCA4322CC13CDE00076589 /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */; + productName = YouTubePlayerKit; + }; +/* End XCSwiftPackageProductDependency section */ + /* Begin XCVersionGroup section */ 02280F5C294B4FDA0032823A /* CourseCoreModel.xcdatamodeld */ = { isa = XCVersionGroup; diff --git a/Course/Course/Data/CourseRepository.swift b/Course/Course/Data/CourseRepository.swift index 19073d2de..7210a9f76 100644 --- a/Course/Course/Data/CourseRepository.swift +++ b/Course/Course/Data/CourseRepository.swift @@ -7,10 +7,11 @@ import Foundation import Core +import OEXFoundation public protocol CourseRepositoryProtocol { func getCourseBlocks(courseID: String) async throws -> CourseStructure - func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure + func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure func blockCompletionRequest(courseID: String, blockID: String) async throws func getHandouts(courseID: String) async throws -> String? func getUpdates(courseID: String) async throws -> [CourseUpdate] @@ -50,8 +51,8 @@ public class CourseRepository: CourseRepositoryProtocol { return parsedStructure } - public func getLoadedCourseBlocks(courseID: String) throws -> CourseStructure { - let localData = try persistence.loadCourseStructure(courseID: courseID) + public func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure { + let localData = try await persistence.loadCourseStructure(courseID: courseID) return parseCourseStructure(course: localData) } @@ -85,7 +86,7 @@ public class CourseRepository: CourseRepositoryProtocol { } public func getSubtitles(url: String, selectedLanguage: String) async throws -> String { - if let subtitlesOffline = persistence.loadSubtitles(url: url + selectedLanguage) { + if let subtitlesOffline = await persistence.loadSubtitles(url: url + selectedLanguage) { return subtitlesOffline } else { let result = try await api.requestData(CourseEndpoint.getSubtitles( @@ -101,7 +102,7 @@ public class CourseRepository: CourseRepositoryProtocol { public func getCourseDates(courseID: String) async throws -> CourseDates { let courseDates = try await api.requestData( CourseEndpoint.getCourseDates(courseID: courseID) - ).mapResponse(DataLayer.CourseDates.self).domain + ).mapResponse(DataLayer.CourseDates.self).domain(useRelativeDates: coreStorage.useRelativeDates) persistence.saveCourseDates(courseID: courseID, courseDates: courseDates) return courseDates } @@ -139,7 +140,11 @@ public class CourseRepository: CourseRepositoryProtocol { media: course.media, certificate: course.certificate?.domain, org: course.org ?? "", - isSelfPaced: course.isSelfPaced + isSelfPaced: course.isSelfPaced, + courseProgress: course.courseProgress == nil ? nil : CourseProgress( + totalAssignmentsCount: course.courseProgress?.totalAssignmentsCount ?? 0, + assignmentsCompleted: course.courseProgress?.assignmentsCompleted ?? 0 + ) ) } @@ -173,7 +178,13 @@ public class CourseRepository: CourseRepositoryProtocol { displayName: sequential.displayName, type: BlockType(rawValue: sequential.type) ?? .unknown, completion: sequential.completion ?? 0, - childs: childs + childs: childs, + sequentialProgress: SequentialProgress( + assignmentType: sequential.assignmentProgress?.assignmentType, + numPointsEarned: Int(sequential.assignmentProgress?.numPointsEarned ?? 0), + numPointsPossible: Int(sequential.assignmentProgress?.numPointsPossible ?? 0) + ), + due: sequential.due == nil ? nil : Date(iso8601: sequential.due!) ) } @@ -192,7 +203,8 @@ public class CourseRepository: CourseRepositoryProtocol { displayName: sequential.displayName, type: BlockType(rawValue: sequential.type) ?? .unknown, completion: sequential.completion ?? 0, - childs: childs + childs: childs, + webUrl: sequential.webUrl ) } @@ -204,6 +216,20 @@ public class CourseRepository: CourseRepositoryProtocol { .replacingOccurrences(of: "?lang=\($0.key)", with: "") return SubtitleUrl(language: $0.key, url: url) } + + var offlineDownload: OfflineDownload? + + if let offlineData = block.offlineDownload, + let fileUrl = offlineData.fileUrl, + let lastModified = offlineData.lastModified, + let fileSize = offlineData.fileSize { + let fullUrl = fileUrl.starts(with: "http") ? fileUrl : config.baseURL.absoluteString + fileUrl + offlineDownload = OfflineDownload( + fileUrl: fullUrl, + lastModified: lastModified, + fileSize: fileSize + ) + } return CourseBlock( blockId: block.blockId, @@ -211,6 +237,7 @@ public class CourseRepository: CourseRepositoryProtocol { courseId: courseId, topicId: block.userViewData?.topicID, graded: block.graded, + due: block.due == nil ? nil : Date(iso8601: block.due!), completion: block.completion ?? 0, type: BlockType(rawValue: block.type) ?? .unknown, displayName: block.displayName, @@ -225,12 +252,13 @@ public class CourseRepository: CourseRepositoryProtocol { mobileLow: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.mobileLow), hls: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.hls) ), - multiDevice: block.multiDevice + multiDevice: block.multiDevice, + offlineDownload: offlineDownload ) } private func parseVideo(encodedVideo: DataLayer.EncodedVideoData?) -> CourseBlockVideo? { - guard let encodedVideo else { + guard let encodedVideo, encodedVideo.url?.isEmpty == false else { return nil } return .init( @@ -265,7 +293,7 @@ class CourseRepositoryMock: CourseRepositoryProtocol { do { let courseDates = try CourseRepository.courseDatesJSON.data(using: .utf8)!.mapResponse(DataLayer.CourseDates.self) - return courseDates.domain + return courseDates.domain(useRelativeDates: true) } catch { throw error } @@ -350,7 +378,11 @@ And there are various ways of describing it-- call it oral poetry or media: course.media, certificate: course.certificate?.domain, org: course.org ?? "", - isSelfPaced: course.isSelfPaced + isSelfPaced: course.isSelfPaced, + courseProgress: course.courseProgress == nil ? nil : CourseProgress( + totalAssignmentsCount: course.courseProgress?.totalAssignmentsCount ?? 0, + assignmentsCompleted: course.courseProgress?.assignmentsCompleted ?? 0 + ) ) } @@ -385,7 +417,13 @@ And there are various ways of describing it-- call it oral poetry or displayName: sequential.displayName, type: BlockType(rawValue: sequential.type) ?? .unknown, completion: sequential.completion ?? 0, - childs: childs + childs: childs, + sequentialProgress: SequentialProgress( + assignmentType: sequential.assignmentProgress?.assignmentType, + numPointsEarned: Int(sequential.assignmentProgress?.numPointsEarned ?? 0), + numPointsPossible: Int(sequential.assignmentProgress?.numPointsPossible ?? 0) + ), + due: sequential.due == nil ? nil : Date(iso8601: sequential.due!) ) } @@ -404,7 +442,8 @@ And there are various ways of describing it-- call it oral poetry or displayName: sequential.displayName, type: BlockType(rawValue: sequential.type) ?? .unknown, completion: sequential.completion ?? 0, - childs: childs + childs: childs, + webUrl: sequential.webUrl ) } @@ -414,6 +453,19 @@ And there are various ways of describing it-- call it oral poetry or let url = $0.value return SubtitleUrl(language: $0.key, url: url) } + + var offlineDownload: OfflineDownload? + + if let offlineData = block.offlineDownload, + let fileUrl = offlineData.fileUrl, + let lastModified = offlineData.lastModified, + let fileSize = offlineData.fileSize { + offlineDownload = OfflineDownload( + fileUrl: fileUrl, + lastModified: lastModified, + fileSize: fileSize + ) + } return CourseBlock( blockId: block.blockId, @@ -421,6 +473,7 @@ And there are various ways of describing it-- call it oral poetry or courseId: courseId, topicId: block.userViewData?.topicID, graded: block.graded, + due: block.due == nil ? nil : Date(iso8601: block.due!), completion: block.completion ?? 0, type: BlockType(rawValue: block.type) ?? .unknown, displayName: block.displayName, @@ -435,7 +488,8 @@ And there are various ways of describing it-- call it oral poetry or mobileLow: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.mobileLow), hls: parseVideo(encodedVideo: block.userViewData?.encodedVideo?.hls) ), - multiDevice: block.multiDevice + multiDevice: block.multiDevice, + offlineDownload: offlineDownload ) } diff --git a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift index f2a060e13..a4fe30b96 100644 --- a/Course/Course/Data/Model/Data_CourseOutlineResponse.swift +++ b/Course/Course/Data/Model/Data_CourseOutlineResponse.swift @@ -21,6 +21,7 @@ public extension DataLayer { public let certificate: Certificate? public let org: String? public let isSelfPaced: Bool + public let courseProgress: CourseProgress? enum CodingKeys: String, CodingKey { case blocks @@ -30,6 +31,7 @@ public extension DataLayer { case certificate case org case isSelfPaced = "is_self_paced" + case courseProgress = "course_progress" } public init( @@ -39,7 +41,8 @@ public extension DataLayer { media: DataLayer.CourseMedia, certificate: Certificate?, org: String?, - isSelfPaced: Bool + isSelfPaced: Bool, + courseProgress: CourseProgress? ) { self.rootItem = rootItem self.dict = dict @@ -48,6 +51,7 @@ public extension DataLayer { self.certificate = certificate self.org = org self.isSelfPaced = isSelfPaced + self.courseProgress = courseProgress } public init(from decoder: Decoder) throws { @@ -60,6 +64,7 @@ public extension DataLayer { certificate = try values.decode(Certificate.self, forKey: .certificate) org = try values.decode(String.self, forKey: .org) isSelfPaced = try values.decode(Bool.self, forKey: .isSelfPaced) + courseProgress = try? values.decode(DataLayer.CourseProgress.self, forKey: .courseProgress) } } } @@ -68,6 +73,7 @@ public extension DataLayer { public let blockId: String public let id: String public let graded: Bool + public let due: String? public let completion: Double? public let studentUrl: String public let webUrl: String @@ -77,11 +83,14 @@ public extension DataLayer { public let allSources: [String]? public let userViewData: CourseDetailUserViewData? public let multiDevice: Bool? + public let assignmentProgress: AssignmentProgress? + public let offlineDownload: OfflineDownload? public init( blockId: String, id: String, graded: Bool, + due: String?, completion: Double?, studentUrl: String, webUrl: String, @@ -90,11 +99,14 @@ public extension DataLayer { descendants: [String]?, allSources: [String]?, userViewData: CourseDetailUserViewData?, - multiDevice: Bool? + multiDevice: Bool?, + assignmentProgress: AssignmentProgress?, + offlineDownload: OfflineDownload? ) { self.blockId = blockId self.id = id self.graded = graded + self.due = due self.completion = completion self.studentUrl = studentUrl self.webUrl = webUrl @@ -104,10 +116,12 @@ public extension DataLayer { self.allSources = allSources self.userViewData = userViewData self.multiDevice = multiDevice + self.assignmentProgress = assignmentProgress + self.offlineDownload = offlineDownload } public enum CodingKeys: String, CodingKey { - case id, type, descendants, graded, completion + case id, type, descendants, graded, completion, due case blockId = "block_id" case studentUrl = "student_view_url" case webUrl = "lms_web_url" @@ -115,9 +129,47 @@ public extension DataLayer { case userViewData = "student_view_data" case allSources = "all_sources" case multiDevice = "student_view_multi_device" + case assignmentProgress = "assignment_progress" + case offlineDownload = "offline_download" } } + + struct AssignmentProgress: Codable { + public let assignmentType: String? + public let numPointsEarned: Double? + public let numPointsPossible: Double? + + public enum CodingKeys: String, CodingKey { + case assignmentType = "assignment_type" + case numPointsEarned = "num_points_earned" + case numPointsPossible = "num_points_possible" + } + + public init(assignmentType: String?, numPointsEarned: Double?, numPointsPossible: Double?) { + self.assignmentType = assignmentType + self.numPointsEarned = numPointsEarned + self.numPointsPossible = numPointsPossible + } + } + + struct OfflineDownload: Codable { + public let fileUrl: String? + public let lastModified: String? + public let fileSize: Int? + public enum CodingKeys: String, CodingKey { + case fileUrl = "file_url" + case lastModified = "last_modified" + case fileSize = "file_size" + } + + public init(fileUrl: String?, lastModified: String?, fileSize: Int?) { + self.fileUrl = fileUrl + self.lastModified = lastModified + self.fileSize = fileSize + } + } + struct Transcripts: Codable { public let en: String? @@ -202,6 +254,5 @@ public extension DataLayer { case fileSize = "file_size" case streamPriority = "stream_priority" } - } } diff --git a/Course/Course/Data/Network/CourseEndpoint.swift b/Course/Course/Data/Network/CourseEndpoint.swift index 2abafa14a..62531a042 100644 --- a/Course/Course/Data/Network/CourseEndpoint.swift +++ b/Course/Course/Data/Network/CourseEndpoint.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import Alamofire enum CourseEndpoint: EndPointType { @@ -24,7 +25,7 @@ enum CourseEndpoint: EndPointType { var path: String { switch self { case .getCourseBlocks: - return "/api/mobile/v3/course_info/blocks/" + return "/api/mobile/v4/course_info/blocks/" case .pageHTML(let url): return "/xblock/\(url)" case .blockCompletionRequest: @@ -82,11 +83,11 @@ enum CourseEndpoint: EndPointType { "username": userName, "course_id": courseID, "depth": "all", - "student_view_data": "video,discussion,html", + "student_view_data": "video,discussion,html,problem", "nav_depth": "4", "requested_fields": """ contains_gated_content,show_gated_sections,special_exam_info,graded, - format,student_view_multi_device,due,completion + format,student_view_multi_device,due,completion,assignment_progress """, "block_counts": "video" ] diff --git a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents index dda82f3ae..1c6c4c93f 100644 --- a/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents +++ b/Course/Course/Data/Persistence/CourseCoreModel.xcdatamodeld/CourseCoreModel.xcdatamodel/contents @@ -1,15 +1,22 @@ - + + + + + + + + @@ -61,12 +68,13 @@ + + - @@ -77,6 +85,7 @@ + @@ -85,6 +94,7 @@ + @@ -101,4 +111,4 @@ - + \ No newline at end of file diff --git a/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift b/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift index 35f8328c2..ff3080a19 100644 --- a/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift +++ b/Course/Course/Data/Persistence/CoursePersistenceProtocol.swift @@ -9,12 +9,12 @@ import CoreData import Core public protocol CoursePersistenceProtocol { - func loadEnrollments() throws -> [Core.CourseItem] + func loadEnrollments() async throws -> [Core.CourseItem] func saveEnrollments(items: [Core.CourseItem]) - func loadCourseStructure(courseID: String) throws -> DataLayer.CourseStructure + func loadCourseStructure(courseID: String) async throws -> DataLayer.CourseStructure func saveCourseStructure(structure: DataLayer.CourseStructure) func saveSubtitles(url: String, subtitlesString: String) - func loadSubtitles(url: String) -> String? + func loadSubtitles(url: String) async -> String? func saveCourseDates(courseID: String, courseDates: CourseDates) func loadCourseDates(courseID: String) throws -> CourseDates } diff --git a/Course/Course/Domain/CourseInteractor.swift b/Course/Course/Domain/CourseInteractor.swift index 1b01e7a59..bcafab40d 100644 --- a/Course/Course/Domain/CourseInteractor.swift +++ b/Course/Course/Domain/CourseInteractor.swift @@ -13,6 +13,7 @@ public protocol CourseInteractorProtocol { func getCourseBlocks(courseID: String) async throws -> CourseStructure func getCourseVideoBlocks(fullStructure: CourseStructure) -> CourseStructure func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure + func getSequentialsContainsBlocks(blockIds: [String], courseID: String) async throws -> [CourseSequential] func blockCompletionRequest(courseID: String, blockID: String) async throws func getHandouts(courseID: String) async throws -> String? func getUpdates(courseID: String) async throws -> [CourseUpdate] @@ -55,15 +56,42 @@ public class CourseInteractor: CourseInteractorProtocol { media: course.media, certificate: course.certificate, org: course.org, - isSelfPaced: course.isSelfPaced + isSelfPaced: course.isSelfPaced, + courseProgress: course.courseProgress == nil ? nil : CourseProgress( + totalAssignmentsCount: course.courseProgress?.totalAssignmentsCount ?? 0, + assignmentsCompleted: course.courseProgress?.assignmentsCompleted ?? 0 + ) ) } public func getLoadedCourseBlocks(courseID: String) async throws -> CourseStructure { - return try repository.getLoadedCourseBlocks(courseID: courseID) + return try await repository.getLoadedCourseBlocks(courseID: courseID) + } + + public func getSequentialsContainsBlocks(blockIds: [String], courseID: String) async throws -> [CourseSequential] { + let courseStructure = try await repository.getLoadedCourseBlocks(courseID: courseID) + var sequentials: [CourseSequential] = [] + + for chapter in courseStructure.childs { + for sequential in chapter.childs { + let filteredChilds = sequential.childs.filter { vertical in + vertical.childs.contains { block in + blockIds.contains(block.id) + } + } + if !filteredChilds.isEmpty { + var newSequential = sequential + newSequential.childs = filteredChilds + sequentials.append(newSequential) + } + } + } + + return sequentials } public func blockCompletionRequest(courseID: String, blockID: String) async throws { + NotificationCenter.default.post(name: .onblockCompletionRequested, object: courseID) return try await repository.blockCompletionRequest(courseID: courseID, blockID: blockID) } @@ -127,7 +155,9 @@ public class CourseInteractor: CourseInteractorProtocol { displayName: sequential.displayName, type: sequential.type, completion: sequential.completion, - childs: newChilds + childs: newChilds, + sequentialProgress: sequential.sequentialProgress, + due: sequential.due ) } @@ -140,7 +170,8 @@ public class CourseInteractor: CourseInteractorProtocol { displayName: vertical.displayName, type: vertical.type, completion: vertical.completion, - childs: newChilds + childs: newChilds, + webUrl: vertical.webUrl ) } @@ -186,9 +217,15 @@ public class CourseInteractor: CourseInteractorProtocol { let endTime = startAndEndTimes.last ?? "00:00:00,000" let text = lines[2.. endTimeInverval { + endTimeInverval = startTimeInterval + } + let subtitle = Subtitle(id: id, - fromTo: DateInterval(start: Date(subtitleTime: startTime), - end: Date(subtitleTime: endTime)), + fromTo: DateInterval(start: startTimeInterval, + end: endTimeInverval), text: text.decodedHTMLEntities()) subtitles.append(subtitle) } diff --git a/Course/Course/Managers/CalendarManager.swift b/Course/Course/Managers/CalendarManager.swift deleted file mode 100644 index 118880aef..000000000 --- a/Course/Course/Managers/CalendarManager.swift +++ /dev/null @@ -1,480 +0,0 @@ -// -// CalendarManager.swift -// Course -// -// Created by Shafqat Muneer on 2/20/24. -// - -import Foundation -import EventKit -import Theme -import Core -import BranchSDK - -enum CalendarDeepLinkType: String { - case courseComponent = "course_component" -} - -private enum CalendarDeepLinkKeys: String, RawStringExtractable { - case courseID = "course_id" - case screenName = "screen_name" - case componentID = "component_id" -} - -struct CourseCalendar: Codable { - var identifier: String - let courseID: String - let title: String - var isOn: Bool - var modalPresented: Bool -} - -class CalendarManager: NSObject { - - private let courseName: String - private let courseID: String - private let courseStructure: CourseStructure? - private let config: ConfigProtocol - - private let eventStore = EKEventStore() - private let iCloudCalendar = "icloud" - private let alertOffset = -1 - private let calendarKey = "CalendarEntries" - - private var localCalendar: EKCalendar? { - if authorizationStatus != .authorized { return nil } - - var calendars = eventStore.calendars(for: .event).filter { $0.title == calendarName } - - if calendars.isEmpty { - return nil - } else { - let calendar = calendars.removeLast() - // calendars.removeLast() pop the element from array and after that, - // following is run on remaing members of array to remove them - // calendar app, if they had been added. - calendars.forEach { try? eventStore.removeCalendar($0, commit: true) } - - return calendar - } - } - - private let calendarColor = Theme.Colors.accentColor - - private var calendarSource: EKSource? { - eventStore.refreshSourcesIfNecessary() - - let iCloud = eventStore.sources.first( - where: { $0.sourceType == .calDAV && $0.title.localizedCaseInsensitiveContains(iCloudCalendar) }) - let local = eventStore.sources.first(where: { $0.sourceType == .local }) - let fallback = eventStore.defaultCalendarForNewEvents?.source - - return iCloud ?? local ?? fallback - } - - private func calendar() -> EKCalendar { - let calendar = EKCalendar(for: .event, eventStore: eventStore) - calendar.title = calendarName - calendar.cgColor = calendarColor.cgColor - calendar.source = calendarSource - - return calendar - } - - var authorizationStatus: EKAuthorizationStatus { - return EKEventStore.authorizationStatus(for: .event) - } - - var calendarName: String { - return config.platformName + " - " + courseName - } - - private lazy var branchEnabled: Bool = { - return config.branch.enabled - }() - - var syncOn: Bool { - get { - if let calendarEntry = calendarEntry, - let localCalendar = localCalendar, - calendarEntry.identifier == localCalendar.calendarIdentifier { - return calendarEntry.isOn - } else if let localCalendar = localCalendar { - let courseCalendar = CourseCalendar( - identifier: localCalendar.calendarIdentifier, - courseID: courseID, - title: calendarName, - isOn: true, - modalPresented: false - ) - addOrUpdateCalendarEntry(courseCalendar: courseCalendar) - return true - } - return false - } - set { - updateCalendarState(isOn: newValue) - } - } - - var isModalPresented: Bool { - get { - return getModalPresented() - } - set { - setModalPresented(presented: newValue) - } - } - - required init(courseID: String, courseName: String, courseStructure: CourseStructure?, config: ConfigProtocol) { - self.courseID = courseID - self.courseName = courseName - self.courseStructure = courseStructure - self.config = config - } - - func requestAccess(completion: @escaping (Bool, EKAuthorizationStatus, EKAuthorizationStatus) -> Void) { - let previousStatus = EKEventStore.authorizationStatus(for: .event) - let requestHandler: (Bool, Error?) -> Void = { [weak self] access, _ in - self?.eventStore.reset() - let currentStatus = EKEventStore.authorizationStatus(for: .event) - DispatchQueue.main.async { - completion(access, previousStatus, currentStatus) - } - } - - if #available(iOS 17.0, *) { - eventStore.requestFullAccessToEvents { access, error in - requestHandler(access, error) - } - } else { - eventStore.requestAccess(to: .event) { access, error in - requestHandler(access, error) - } - } - } - - private func generateCourseCalendar() -> Bool { - guard localCalendar == nil else { return true } - do { - let newCalendar = calendar() - try eventStore.saveCalendar(newCalendar, commit: true) - - let courseCalendar: CourseCalendar - - if var calendarEntry = calendarEntry { - calendarEntry.identifier = newCalendar.calendarIdentifier - courseCalendar = calendarEntry - } else { - courseCalendar = CourseCalendar( - identifier: newCalendar.calendarIdentifier, - courseID: courseID, - title: calendarName, - isOn: true, - modalPresented: false - ) - } - - addOrUpdateCalendarEntry(courseCalendar: courseCalendar) - - return true - } catch { - return false - } - } - - func removeCalendar(completion: ((Bool) -> Void)? = nil) { - guard let calendar = localCalendar else { return } - do { - try eventStore.removeCalendar(calendar, commit: true) - updateSyncSwitchStatus(isOn: false) - completion?(true) - } catch { - completion?(false) - } - } - - private func calendarEvent(for block: CourseDateBlock, generateDeepLink: Bool) -> EKEvent? { - guard !block.title.isEmpty else { return nil } - - let title = block.title + ": " + courseName - // startDate is the start date and time for the event, - // it is also being used as first alert for the event - let startDate = block.date.add(.hour, value: alertOffset) - let secondAlert = startDate.add(.day, value: alertOffset) - let endDate = block.date - var notes = "\(courseName)\n\n\(block.title)" - - if generateDeepLink && block.isAvailable && branchEnabled { - if let link = generateDeeplink(componentBlockID: block.firstComponentBlockID) { - notes += "\n\(link)" - } - } - - return generateEvent( - title: title, - startDate: startDate, - endDate: endDate, - secondAlert: secondAlert, - notes: notes - ) - } - - private func calendarEvent(for blocks: [CourseDateBlock], generateDeepLink: Bool) -> EKEvent? { - guard let block = blocks.first, !block.title.isEmpty else { return nil } - - let title = block.title + ": " + courseName - // startDate is the start date and time for the event, - // it is also being used as first alert for the event - let startDate = block.date.add(.hour, value: alertOffset) - let secondAlert = startDate.add(.day, value: alertOffset) - let endDate = block.date - let notes = "\(courseName)\n\n" + blocks.compactMap { block -> String in - if generateDeepLink && block.isAvailable && branchEnabled { - if let link = generateDeeplink(componentBlockID: block.firstComponentBlockID) { - return "\(block.title)\n\(link)" - } else { - return block.title - } - } else { - return block.title - } - }.joined(separator: "\n\n") - - return generateEvent( - title: title, - startDate: startDate, - endDate: endDate, - secondAlert: secondAlert, - notes: notes - ) - } - - private func generateDeeplink(componentBlockID: String) -> String? { - guard !componentBlockID.isEmpty else { return nil } - let branchUniversalObject = BranchUniversalObject( - canonicalIdentifier: "\(CalendarDeepLinkType.courseComponent.rawValue)/\(componentBlockID)" - ) - let dictionary: NSMutableDictionary = [ - CalendarDeepLinkKeys.screenName.rawValue: CalendarDeepLinkType.courseComponent.rawValue, - CalendarDeepLinkKeys.courseID.rawValue: courseID, - CalendarDeepLinkKeys.componentID.rawValue: componentBlockID - ] - let metadata = BranchContentMetadata() - metadata.customMetadata = dictionary - branchUniversalObject.contentMetadata = metadata - let properties = BranchLinkProperties() - if let block = courseStructure?.blockWithID(courseBlockId: componentBlockID), !block.webUrl.isEmpty { - properties.addControlParam("$desktop_url", withValue: block.webUrl) - } - return branchUniversalObject.getShortUrl(with: properties) - } - - private func generateEvent(title: String, - startDate: Date, - endDate: Date, - secondAlert: Date, - notes: String) -> EKEvent { - let event = EKEvent(eventStore: eventStore) - event.title = title - event.startDate = startDate - event.endDate = endDate - event.calendar = localCalendar - event.notes = notes - - if startDate > Date() { - let alarm = EKAlarm(absoluteDate: startDate) - event.addAlarm(alarm) - } - - if secondAlert > Date() { - let alarm = EKAlarm(absoluteDate: secondAlert) - event.addAlarm(alarm) - } - - return event - } - - private func addEvent(event: EKEvent) { - if !alreadyExist(event: event) { - try? eventStore.save(event, span: .thisEvent) - } - } - - private func alreadyExist(event eventToAdd: EKEvent) -> Bool { - guard let courseCalendar = calendarEntry else { return false } - let calendars = eventStore.calendars(for: .event).filter { $0.calendarIdentifier == courseCalendar.identifier } - let predicate = eventStore.predicateForEvents( - withStart: eventToAdd.startDate, - end: eventToAdd.endDate, - calendars: calendars - ) - let existingEvents = eventStore.events(matching: predicate) - - return existingEvents.contains { event -> Bool in - return event.title == eventToAdd.title - && event.startDate == eventToAdd.startDate - && event.endDate == eventToAdd.endDate - } - } - - private func setModalPresented(presented: Bool) { - guard var calendars = courseCalendars(), - let index = calendars.firstIndex(where: { $0.title == calendarName }) - else { return } - - calendars.modifyElement(atIndex: index) { element in - element.modalPresented = presented - } - - saveCalendarEntry(calendars: calendars) - } - - private func getModalPresented() -> Bool { - guard let calendars = courseCalendars(), - let calendar = calendars.first(where: { $0.title == calendarName }) - else { return false } - - return calendar.modalPresented - } - - private func removeCalendarEntry() { - guard var calendars = courseCalendars() else { return } - - if let index = calendars.firstIndex(where: { $0.title == calendarName }) { - calendars.remove(at: index) - } - - saveCalendarEntry(calendars: calendars) - } - - private func updateSyncSwitchStatus(isOn: Bool) { - guard var calendars = courseCalendars() else { return } - - if let index = calendars.firstIndex(where: { $0.title == calendarName }) { - calendars.modifyElement(atIndex: index) { element in - element.isOn = isOn - } - } - - saveCalendarEntry(calendars: calendars) - } - - private var calendarEntry: CourseCalendar? { - guard let calendars = courseCalendars() else { return nil } - return calendars.first(where: { $0.title == calendarName }) - } -} - -extension CalendarManager { - func addEventsToCalendar(for dateBlocks: [Date: [CourseDateBlock]], completion: @escaping (Bool) -> Void) { - if !generateCourseCalendar() { - completion(false) - return - } - - DispatchQueue.global().async { [weak self] in - guard let weakSelf = self else { return } - let events = weakSelf.generateEvents(for: dateBlocks, generateDeepLink: true) - - if events.isEmpty { - //Ideally this shouldn't happen, but in any case if this happen so lets remove the calendar - weakSelf.removeCalendar() - completion(false) - } else { - events.forEach { event in weakSelf.addEvent(event: event) } - do { - try weakSelf.eventStore.commit() - DispatchQueue.main.async { - completion(true) - } - } catch { - DispatchQueue.main.async { - completion(false) - } - } - } - } - } - - func checkIfEventsShouldBeShifted(for dateBlocks: [Date: [CourseDateBlock]]) -> Bool { - guard calendarEntry != nil else { return true } - - let events = generateEvents(for: dateBlocks, generateDeepLink: false) - let allEvents = events.allSatisfy { alreadyExist(event: $0) } - - return !allEvents - } - - private func generateEvents(for dateBlocks: [Date: [CourseDateBlock]], generateDeepLink: Bool) -> [EKEvent] { - var events: [EKEvent] = [] - dateBlocks.forEach { item in - let blocks = item.value - - if blocks.count > 1 { - if let generatedEvent = calendarEvent(for: blocks, generateDeepLink: generateDeepLink) { - events.append(generatedEvent) - } - } else { - if let block = blocks.first { - if let generatedEvent = calendarEvent(for: block, generateDeepLink: generateDeepLink) { - events.append(generatedEvent) - } - } - } - } - - return events - } - - private func addOrUpdateCalendarEntry(courseCalendar: CourseCalendar) { - var calenders: [CourseCalendar] = [] - - if let decodedCalendars = courseCalendars() { - calenders = decodedCalendars - } - - if let index = calenders.firstIndex(where: { $0.title == calendarName }) { - calenders.modifyElement(atIndex: index) { element in - element = courseCalendar - } - } else { - calenders.append(courseCalendar) - } - - saveCalendarEntry(calendars: calenders) - } - - private func updateCalendarState(isOn: Bool) { - guard var calendars = courseCalendars(), - let index = calendars.firstIndex(where: { $0.title == calendarName }) - else { return } - - calendars.modifyElement(atIndex: index) { element in - element.isOn = isOn - } - - saveCalendarEntry(calendars: calendars) - } - - private func courseCalendars() -> [CourseCalendar]? { - guard let data = UserDefaults.standard.data(forKey: calendarKey), - let courseCalendars = try? PropertyListDecoder().decode([CourseCalendar].self, from: data) - else { return nil } - - return courseCalendars - } - - private func saveCalendarEntry(calendars: [CourseCalendar]) { - guard let data = try? PropertyListEncoder().encode(calendars) else { return } - - UserDefaults.standard.set(data, forKey: calendarKey) - UserDefaults.standard.synchronize() - } -} - -fileprivate extension Date { - func add(_ unit: Calendar.Component, value: Int) -> Date { - return Calendar.current.date(byAdding: unit, value: value, to: self) ?? self - } -} diff --git a/Course/Course/Presentation/Container/CourseContainerView.swift b/Course/Course/Presentation/Container/CourseContainerView.swift index 322b37564..725f98ace 100644 --- a/Course/Course/Presentation/Container/CourseContainerView.swift +++ b/Course/Course/Presentation/Container/CourseContainerView.swift @@ -10,11 +10,14 @@ import Core import Discussion import Swinject import Theme +@_spi(Advanced) import SwiftUIIntrospect public struct CourseContainerView: View { @ObservedObject public var viewModel: CourseContainerViewModel + @ObservedObject + public var courseDatesViewModel: CourseDatesViewModel @State private var isAnimatingForTap: Bool = false public var courseID: String private var title: String @@ -22,12 +25,22 @@ public struct CourseContainerView: View { @State private var coordinate: CGFloat = .zero @State private var lastCoordinate: CGFloat = .zero @State private var collapsed: Bool = false + @State private var viewHeight: CGFloat = .zero @Environment(\.isHorizontal) private var isHorizontal @Namespace private var animationNamespace private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } private let coordinateBoundaryLower: CGFloat = -115 - private let coordinateBoundaryHigher: CGFloat = 40 + private let courseRawImage: String? + + private var coordinateBoundaryHigher: CGFloat { + let topInset = UIApplication.shared.windowInsets.top + guard topInset > 0 else { + return 40 + } + + return topInset + } private struct GeometryName { static let backButton = "backButton" @@ -39,8 +52,10 @@ public struct CourseContainerView: View { public init( viewModel: CourseContainerViewModel, + courseDatesViewModel: CourseDatesViewModel, courseID: String, - title: String + title: String, + courseRawImage: String? ) { self.viewModel = viewModel Task { @@ -55,6 +70,8 @@ public struct CourseContainerView: View { } self.courseID = courseID self.title = title + self.courseDatesViewModel = courseDatesViewModel + self.courseRawImage = courseRawImage } public var body: some View { @@ -81,6 +98,7 @@ public struct CourseContainerView: View { selection: $viewModel.selection, coordinate: $coordinate, collapsed: $collapsed, + viewHeight: $viewHeight, dateTabIndex: CourseTab.dates.rawValue ) } else { @@ -94,24 +112,51 @@ public struct CourseContainerView: View { collapsed: $collapsed, containerWidth: proxy.size.width, animationNamespace: animationNamespace, - isAnimatingForTap: $isAnimatingForTap + isAnimatingForTap: $isAnimatingForTap, + courseRawImage: courseRawImage ) } .offset( y: ignoreOffset ? (collapsed ? coordinateBoundaryLower : .zero) : ((coordinateBoundaryLower...coordinateBoundaryHigher).contains(coordinate) - ? coordinate + ? (collapsed ? coordinateBoundaryLower : coordinate) : (collapsed ? coordinateBoundaryLower : .zero)) ) backButton(containerWidth: proxy.size.width) } - }.ignoresSafeArea(edges: idiom == .pad ? .leading : .top) - .onAppear { - self.collapsed = isHorizontal - } + } + .ignoresSafeArea(edges: idiom == .pad ? .leading : .top) + .onAppear { + self.collapsed = isHorizontal + } } } + + switch courseDatesViewModel.eventState { + case .removedCalendar: + showDatesSuccessView( + title: CourseLocalization.CourseDates.calendarEvents, + message: CourseLocalization.CourseDates.calendarEventsRemoved + ) + case .updatedCalendar: + showDatesSuccessView( + title: CourseLocalization.CourseDates.calendarEvents, + message: CourseLocalization.CourseDates.calendarEventsUpdated + ) + default: + EmptyView() + } + } + + private func showDatesSuccessView(title: String, message: String) -> some View { + return DatesSuccessView( + title: title, + message: message, + selectedTab: .dates + ) { + courseDatesViewModel.resetEventState() + } } private func backButton(containerWidth: CGFloat) -> some View { @@ -154,6 +199,7 @@ public struct CourseContainerView: View { selection: $viewModel.selection, coordinate: $coordinate, collapsed: $collapsed, + viewHeight: $viewHeight, dateTabIndex: CourseTab.dates.rawValue ) .tabItem { @@ -171,6 +217,7 @@ public struct CourseContainerView: View { selection: $viewModel.selection, coordinate: $coordinate, collapsed: $collapsed, + viewHeight: $viewHeight, dateTabIndex: CourseTab.dates.rawValue ) .tabItem { @@ -184,8 +231,22 @@ public struct CourseContainerView: View { courseID: courseID, coordinate: $coordinate, collapsed: $collapsed, - viewModel: Container.shared.resolve(CourseDatesViewModel.self, - arguments: courseID, title)! + viewHeight: $viewHeight, + viewModel: courseDatesViewModel + ) + .tabItem { + tab.image + Text(tab.title) + } + .tag(tab) + .accentColor(Theme.Colors.accentColor) + case .offline: + OfflineView( + courseID: courseID, + coordinate: $coordinate, + collapsed: $collapsed, + viewHeight: $viewHeight, + viewModel: viewModel ) .tabItem { tab.image @@ -198,6 +259,7 @@ public struct CourseContainerView: View { courseID: courseID, coordinate: $coordinate, collapsed: $collapsed, + viewHeight: $viewHeight, viewModel: Container.shared.resolve(DiscussionTopicsViewModel.self, argument: title)!, router: Container.shared.resolve(DiscussionRouter.self)! @@ -213,6 +275,7 @@ public struct CourseContainerView: View { courseID: courseID, coordinate: $coordinate, collapsed: $collapsed, + viewHeight: $viewHeight, viewModel: Container.shared.resolve(HandoutsViewModel.self, argument: courseID)! ) .tabItem { @@ -225,7 +288,7 @@ public struct CourseContainerView: View { } } .tabViewStyle(.page(indexDisplayMode: .never)) - .introspect(.scrollView, on: .iOS(.v15, .v16, .v17), customize: { tabView in + .introspect(.scrollView, on: .iOS(.v16...), customize: { tabView in tabView.isScrollEnabled = false }) .onFirstAppear { @@ -311,9 +374,24 @@ struct CourseScreensView_Previews: PreviewProvider { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ), - courseID: "", title: "Title of Course") + courseDatesViewModel: CourseDatesViewModel( + interactor: CourseInteractor.mock, + router: CourseRouterMock(), + cssInjector: CSSInjectorMock(), + connectivity: Connectivity(), + config: ConfigMock(), + courseID: "1", + courseName: "a", + analytics: CourseAnalyticsMock(), + calendarManager: CalendarManagerMock() + ), + courseID: "", + title: "Title of Course", + courseRawImage: nil + ) } } #endif diff --git a/Course/Course/Presentation/Container/CourseContainerViewModel.swift b/Course/Course/Presentation/Container/CourseContainerViewModel.swift index 82572b60e..e156dcb1a 100644 --- a/Course/Course/Presentation/Container/CourseContainerViewModel.swift +++ b/Course/Course/Presentation/Container/CourseContainerViewModel.swift @@ -8,19 +8,22 @@ import Foundation import SwiftUI import Core +import OEXFoundation import Combine public enum CourseTab: Int, CaseIterable, Identifiable { public var id: Int { rawValue } - case course case videos - case discussion case dates + case offline + case discussion case handounds +} +extension CourseTab { public var title: String { switch self { case .course: @@ -29,13 +32,15 @@ public enum CourseTab: Int, CaseIterable, Identifiable { return CourseLocalization.CourseContainer.videos case .dates: return CourseLocalization.CourseContainer.dates + case .offline: + return CourseLocalization.CourseContainer.offline case .discussion: return CourseLocalization.CourseContainer.discussions case .handounds: return CourseLocalization.CourseContainer.handouts } } - + public var image: Image { switch self { case .course: @@ -44,6 +49,8 @@ public enum CourseTab: Int, CaseIterable, Identifiable { return CoreAssets.videos.swiftUIImage.renderingMode(.template) case .dates: return CoreAssets.dates.swiftUIImage.renderingMode(.template) + case .offline: + return CoreAssets.downloads.swiftUIImage.renderingMode(.template) case .discussion: return CoreAssets.discussions.swiftUIImage.renderingMode(.template) case .handounds: @@ -53,8 +60,8 @@ public enum CourseTab: Int, CaseIterable, Identifiable { } public class CourseContainerViewModel: BaseCourseViewModel { - - @Published public var selection: Int = CourseTab.course.rawValue + + @Published public var selection: Int @Published var isShowProgress = true @Published var isShowRefresh = false @Published var courseStructure: CourseStructure? @@ -67,7 +74,15 @@ public class CourseContainerViewModel: BaseCourseViewModel { @Published var userSettings: UserSettings? @Published var isInternetAvaliable: Bool = true @Published var dueDatesShifted: Bool = false - + @Published var updateCourseProgress: Bool = false + @Published var totalFilesSize: Int = 1 + @Published var downloadedFilesSize: Int = 0 + @Published var realDownloadedFilesSize: Int = 0 + @Published var largestDownloadBlocks: [CourseBlock] = [] + @Published var downloadAllButtonState: OfflineView.DownloadAllState = .start + + let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) + var errorMessage: String? { didSet { withAnimation { @@ -79,22 +94,25 @@ public class CourseContainerViewModel: BaseCourseViewModel { let router: CourseRouter let config: ConfigProtocol let connectivity: ConnectivityProtocol - + let isActive: Bool? let courseStart: Date? let courseEnd: Date? let enrollmentStart: Date? let enrollmentEnd: Date? - + let lastVisitedBlockID: String? + var courseDownloadTasks: [DownloadDataTask] = [] private(set) var waitingDownloads: [CourseBlock]? - + private let interactor: CourseInteractorProtocol private let authInteractor: AuthInteractorProtocol let analytics: CourseAnalytics let coreAnalytics: CoreAnalytics private(set) var storage: CourseStorage - + + private let cellularFileSizeLimit: Int = 100 * 1024 * 1024 + public init( interactor: CourseInteractorProtocol, authInteractor: AuthInteractorProtocol, @@ -109,7 +127,9 @@ public class CourseContainerViewModel: BaseCourseViewModel { courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - coreAnalytics: CoreAnalytics + lastVisitedBlockID: String?, + coreAnalytics: CoreAnalytics, + selection: CourseTab = CourseTab.course ) { self.interactor = interactor self.authInteractor = authInteractor @@ -125,12 +145,59 @@ public class CourseContainerViewModel: BaseCourseViewModel { self.storage = storage self.userSettings = storage.userSettings self.isInternetAvaliable = connectivity.isInternetAvaliable + self.lastVisitedBlockID = lastVisitedBlockID self.coreAnalytics = coreAnalytics - + self.selection = selection.rawValue + super.init(manager: manager) addObservers() } + func updateCourseIfNeeded(courseID: String) async { + if updateCourseProgress { + await getCourseBlocks(courseID: courseID, withProgress: false) + updateCourseProgress = false + } + } + + func openLastVisitedBlock() { + guard let continueWith = continueWith, + let courseStructure = courseStructure else { return } + let chapter = courseStructure.childs[continueWith.chapterIndex] + let sequential = chapter.childs[continueWith.sequentialIndex] + let continueUnit = sequential.childs[continueWith.verticalIndex] + + var continueBlock: CourseBlock? + continueUnit.childs.forEach { block in + if block.id == continueWith.lastVisitedBlockId { + continueBlock = block + } + } + + trackResumeCourseClicked( + blockId: continueBlock?.id ?? "" + ) + + router.showCourseUnit( + courseName: courseStructure.displayName, + blockId: continueBlock?.id ?? "", + courseID: courseStructure.id, + verticalIndex: continueWith.verticalIndex, + chapters: courseStructure.childs, + chapterIndex: continueWith.chapterIndex, + sequentialIndex: continueWith.sequentialIndex + ) + } + + @MainActor + func getCourseStructure(courseID: String) async throws -> CourseStructure? { + if isInternetAvaliable { + return try await interactor.getCourseBlocks(courseID: courseID) + } else { + return try await interactor.getLoadedCourseBlocks(courseID: courseID) + } + } + @MainActor func getCourseBlocks(courseID: String, withProgress: Bool = true) async { guard let courseStart, courseStart < Date() else { return } @@ -138,35 +205,29 @@ public class CourseContainerViewModel: BaseCourseViewModel { isShowProgress = withProgress isShowRefresh = !withProgress do { + let courseStructure = try await getCourseStructure(courseID: courseID) + await setDownloadsStates(courseStructure: courseStructure) + self.courseStructure = courseStructure + if isInternetAvaliable { - courseStructure = try await interactor.getCourseBlocks(courseID: courseID) - isShowProgress = false - isShowRefresh = false + NotificationCenter.default.post(name: .getCourseDates, object: courseID) if let courseStructure { - let continueWith = try await getResumeBlock( + try await getResumeBlock( courseID: courseID, courseStructure: courseStructure ) - withAnimation { - self.continueWith = continueWith - } } - } else { - courseStructure = try await interactor.getLoadedCourseBlocks(courseID: courseID) } courseVideosStructure = interactor.getCourseVideoBlocks(fullStructure: courseStructure!) - await setDownloadsStates() + await getDownloadingProgress() isShowProgress = false isShowRefresh = false - } catch let error { + } catch { isShowProgress = false isShowRefresh = false - if error.isInternetError || error is NoCachedDataError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError - } + courseStructure = nil + courseVideosStructure = nil } } @@ -178,14 +239,10 @@ public class CourseContainerViewModel: BaseCourseViewModel { self.courseDeadlineInfo = courseDeadlineInfo } } catch let error { - if error.isInternetError || error is NoCachedDataError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError - } + debugLog(error.localizedDescription) } } - + @MainActor func shiftDueDates(courseID: String, withProgress: Bool = true, screen: DatesStatusInfoScreen, type: String) async { isShowProgress = withProgress @@ -229,53 +286,31 @@ public class CourseContainerViewModel: BaseCourseViewModel { storage.userSettings?.downloadQuality = downloadQuality userSettings = storage.userSettings } - + @MainActor func tryToRefreshCookies() async { try? await authInteractor.getCookies(force: false) } @MainActor - private func getResumeBlock(courseID: String, courseStructure: CourseStructure) async throws -> ContinueWith? { - let result = try await interactor.resumeBlock(courseID: courseID) - return findContinueVertical( - blockID: result.blockID, - courseStructure: courseStructure - ) - } - - @MainActor - func onDownloadViewTap(chapter: CourseChapter, blockId: String, state: DownloadViewState) async { - guard let sequential = chapter.childs - .first(where: { $0.id == blockId }) else { - return - } - - let blocks = sequential.childs.flatMap { $0.childs } - .filter { $0.isDownloadable } - - if state == .available, isShowedAllowLargeDownloadAlert(blocks: blocks) { - return - } - - if state == .available { - analytics.bulkDownloadVideosSubsection( - courseID: courseStructure?.id ?? "", - sectionID: chapter.id, - subSectionID: sequential.id, - videos: blocks.count - ) - } else if state == .finished { - analytics.bulkDeleteVideosSubsection( - courseID: courseStructure?.id ?? "", - subSectionID: sequential.id, - videos: blocks.count + private func getResumeBlock(courseID: String, courseStructure: CourseStructure) async throws { + if let lastVisitedBlockID { + self.continueWith = findContinueVertical( + blockID: lastVisitedBlockID, + courseStructure: courseStructure ) + openLastVisitedBlock() + } else { + let result = try await interactor.resumeBlock(courseID: courseID) + withAnimation { + self.continueWith = findContinueVertical( + blockID: result.blockID, + courseStructure: courseStructure + ) + } } - - await download(state: state, blocks: blocks) } - + func verticalsBlocksDownloadable(by courseSequential: CourseSequential) -> [CourseBlock] { let verticals = downloadableVerticals.filter { verticalState in courseSequential.childs.contains(where: { item in @@ -284,7 +319,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { } return verticals.flatMap { $0.vertical.childs.filter { $0.isDownloadable } } } - + func getTasks(sequential: CourseSequential) -> [DownloadDataTask] { let blocks = verticalsBlocksDownloadable(by: sequential) let tasks = blocks.compactMap { block in @@ -292,20 +327,20 @@ public class CourseContainerViewModel: BaseCourseViewModel { } return tasks } - - func continueDownload() { + + func continueDownload() async { guard let blocks = waitingDownloads else { return } do { - try manager.addToDownloadQueue(blocks: blocks) + try await manager.addToDownloadQueue(blocks: blocks) } catch let error { if error is NoWiFiError { errorMessage = CoreLocalization.Error.wifi } } } - + func trackSelectedTab( selection: CourseTab, courseId: String, @@ -316,6 +351,8 @@ public class CourseContainerViewModel: BaseCourseViewModel { analytics.courseOutlineCourseTabClicked(courseId: courseId, courseName: courseName) case .videos: analytics.courseOutlineVideosTabClicked(courseId: courseId, courseName: courseName) + case .offline: + analytics.courseOutlineOfflineTabClicked(courseId: courseId, courseName: courseName) case .dates: analytics.courseOutlineDatesTabClicked(courseId: courseId, courseName: courseName) case .discussion: @@ -324,7 +361,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { analytics.courseOutlineHandoutsTabClicked(courseId: courseId, courseName: courseName) } } - + func trackVerticalClicked( courseId: String, courseName: String, @@ -345,7 +382,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { courseID: courseID ) } - + func trackSequentialClicked(_ sequential: CourseSequential) { guard let course = courseStructure else { return } analytics.sequentialClicked( @@ -364,7 +401,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { blockId: blockId ) } - + func completeBlock( chapterID: String, sequentialID: String, @@ -380,14 +417,14 @@ public class CourseContainerViewModel: BaseCourseViewModel { .childs.firstIndex(where: { $0.id == sequentialID }) else { return } - + guard let verticalIndex = courseStructure? .childs[chapterIndex] .childs[sequentialIndex] .childs.firstIndex(where: { $0.id == verticalID }) else { return } - + guard let blockIndex = courseStructure? .childs[chapterIndex] .childs[sequentialIndex] @@ -395,7 +432,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { .childs.firstIndex(where: { $0.id == blockID }) else { return } - + courseStructure? .childs[chapterIndex] .childs[sequentialIndex] @@ -405,7 +442,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { courseVideosStructure = interactor.getCourseVideoBlocks(fullStructure: $0) } } - + func hasVideoForDowbloads() -> Bool { guard let courseVideosStructure = courseVideosStructure else { return false @@ -414,7 +451,7 @@ public class CourseContainerViewModel: BaseCourseViewModel { .flatMap { $0.childs } .contains(where: { $0.isDownloadable }) } - + func isAllDownloading() -> Bool { let totalCount = downloadableVerticals.count let downloadingCount = downloadableVerticals.filter { $0.state == .downloading }.count @@ -422,17 +459,37 @@ public class CourseContainerViewModel: BaseCourseViewModel { if finishedCount == totalCount { return false } return totalCount - finishedCount == downloadingCount } - + @MainActor - func download(state: DownloadViewState, blocks: [CourseBlock]) async { + func isAllDownloaded() -> Bool { + guard let course = courseStructure else { return false } + for chapter in course.childs { + for sequential in chapter.childs where sequential.isDownloadable { + let blocks = downloadableBlocks(from: sequential) + for block in blocks { + if let task = courseDownloadTasks.first(where: { $0.blockId == block.id }) { + if task.state != .finished { + return false + } + } else { + return false + } + } + } + } + return true + } + + @MainActor + func download(state: DownloadViewState, blocks: [CourseBlock], sequentials: [CourseSequential]) async { do { switch state { case .available: - try manager.addToDownloadQueue(blocks: blocks) + try await manager.addToDownloadQueue(blocks: blocks) case .downloading: try await manager.cancelDownloading(courseId: courseStructure?.id ?? "", blocks: blocks) case .finished: - await manager.deleteFile(blocks: blocks) + presentRemoveDownloadAlert(blocks: blocks, sequentials: sequentials) } } catch let error { if error is NoWiFiError { @@ -440,7 +497,176 @@ public class CourseContainerViewModel: BaseCourseViewModel { } } } - + + private func presentNoInternetAlert(sequentials: [CourseSequential]) { + router.presentView( + transitionStyle: .coverVertical, + view: DownloadErrorAlertView( + errorType: .noInternetConnection, + sequentials: sequentials, + close: { [weak self] in + guard let self else { return } + self.router.dismiss(animated: true) + } + ), + completion: {} + ) + } + + private func presentWifiRequiredAlert(sequentials: [CourseSequential]) { + router.presentView( + transitionStyle: .coverVertical, + view: DownloadErrorAlertView( + errorType: .wifiRequired, + sequentials: sequentials, + close: { [weak self] in + guard let self else { return } + self.router.dismiss(animated: true) + } + ), + completion: {} + ) + } + + @MainActor + private func presentConfirmDownloadCellularAlert( + blocks: [CourseBlock], + sequentials: [CourseSequential], + totalFileSize: Int, + action: @escaping () -> Void = {} + ) async { + router.presentView( + transitionStyle: .coverVertical, + view: DownloadActionView( + actionType: .confirmDownloadCellular, + sequentials: sequentials, + action: { [weak self] in + guard let self else { return } + if !self.isEnoughSpace(for: totalFileSize) { + self.presentStorageFullAlert(sequentials: sequentials) + } else { + Task { + try? await self.manager.addToDownloadQueue(blocks: blocks) + } + action() + } + self.router.dismiss(animated: true) + }, + cancel: { [weak self] in + guard let self else { return } + self.router.dismiss(animated: true) + } + ), + completion: {} + ) + } + + private func presentStorageFullAlert(sequentials: [CourseSequential]) { + router.presentView( + transitionStyle: .coverVertical, + view: DeviceStorageFullAlertView( + sequentials: sequentials, + usedSpace: getUsedDiskSpace() ?? 0, + freeSpace: getFreeDiskSpace() ?? 0, + close: { [weak self] in + guard let self else { return } + self.router.dismiss(animated: true) + } + ), + completion: {} + ) + } + + @MainActor + private func presentConfirmDownloadAlert( + blocks: [CourseBlock], + sequentials: [CourseSequential], + totalFileSize: Int, + action: @escaping () -> Void = {} + ) async { + router.presentView( + transitionStyle: .coverVertical, + view: DownloadActionView( + actionType: .confirmDownload, + sequentials: manager.updateUnzippedFileSize(for: sequentials), + action: { [weak self] in + guard let self else { return } + if !self.isEnoughSpace(for: totalFileSize) { + self.router.dismiss(animated: true) + self.presentStorageFullAlert(sequentials: sequentials) + } else { + Task { + try? await self.manager.addToDownloadQueue(blocks: blocks) + } + action() + } + self.router.dismiss(animated: true) + }, + cancel: { [weak self] in + guard let self else { return } + self.router.dismiss(animated: true) + } + ), + completion: {} + ) + } + + private func presentRemoveDownloadAlert(blocks: [CourseBlock], sequentials: [CourseSequential]) { + router.presentView( + transitionStyle: .coverVertical, + view: DownloadActionView( + actionType: .remove, + sequentials: manager.updateUnzippedFileSize(for: sequentials), + action: { [weak self] in + guard let self else { return } + Task { + await self.manager.deleteFile(blocks: blocks) + } + self.router.dismiss(animated: true) + }, + cancel: { [weak self] in + guard let self else { return } + self.router.dismiss(animated: true) + } + ), + completion: {} + ) + } + + @MainActor + func collectBlocks(chapter: CourseChapter, blockId: String, state: DownloadViewState) async -> [CourseBlock] { + let sequentials = chapter.childs.filter({ $0.id == blockId }) + guard !sequentials.isEmpty else { return [] } + + let blocks = sequentials.flatMap { $0.childs.flatMap { $0.childs } } + .filter { $0.isDownloadable } + + if state == .available, isShowedAllowLargeDownloadAlert(blocks: blocks) { + return [] + } + + guard let sequential = chapter.childs.first(where: { $0.id == blockId }) else { + return [] + } + + if state == .available { + analytics.bulkDownloadVideosSubsection( + courseID: courseStructure?.id ?? "", + sectionID: chapter.id, + subSectionID: sequential.id, + videos: blocks.count + ) + } else if state == .finished { + analytics.bulkDeleteVideosSubsection( + courseID: courseStructure?.id ?? "", + subSectionID: sequential.id, + videos: blocks.count + ) + } + + return blocks + } + @MainActor func isShowedAllowLargeDownloadAlert(blocks: [CourseBlock]) -> Bool { waitingDownloads = nil @@ -454,7 +680,9 @@ public class CourseContainerViewModel: BaseCourseViewModel { self.router.dismiss(animated: true) }, okTapped: { - self.continueDownload() + Task { + await self.continueDownload() + } self.router.dismiss(animated: true) }, type: .default(positiveAction: CourseLocalization.Alert.accept, image: nil) @@ -463,7 +691,94 @@ public class CourseContainerViewModel: BaseCourseViewModel { } return false } - + + @MainActor + func downloadAll() async { + guard let course = courseStructure else { return } + var blocksToDownload: [CourseBlock] = [] + var sequentialsToDownload: [CourseSequential] = [] + + for chapter in course.childs { + for sequential in chapter.childs where sequential.isDownloadable { + let blocks = downloadableBlocks(from: sequential) + let notDownloadedBlocks = blocks.filter { !isBlockDownloaded($0) } + if !notDownloadedBlocks.isEmpty { + var updatedSequential = sequential + updatedSequential.childs = updatedSequential.childs.map { vertical in + var updatedVertical = vertical + updatedVertical.childs = vertical.childs.filter { block in + notDownloadedBlocks.contains { $0.id == block.id } + } + return updatedVertical + } + blocksToDownload.append(contentsOf: notDownloadedBlocks) + sequentialsToDownload.append(updatedSequential) + } + } + } + + if !blocksToDownload.isEmpty { + let totalFileSize = blocksToDownload.reduce(0) { $0 + ($1.fileSize ?? 0) } + + if !connectivity.isInternetAvaliable { + presentNoInternetAlert(sequentials: sequentialsToDownload) + } else if connectivity.isMobileData { + if storage.userSettings?.wifiOnly == true { + presentWifiRequiredAlert(sequentials: sequentialsToDownload) + } else { + await presentConfirmDownloadCellularAlert( + blocks: blocksToDownload, + sequentials: sequentialsToDownload, + totalFileSize: totalFileSize, + action: { [weak self] in + guard let self else { return } + self.downloadAllButtonState = .cancel + } + ) + } + } else { + if totalFileSize > 100 * 1024 * 1024 { + await presentConfirmDownloadAlert( + blocks: blocksToDownload, + sequentials: sequentialsToDownload, + totalFileSize: totalFileSize, + action: { [weak self] in + guard let self else { return } + self.downloadAllButtonState = .cancel + } + ) + } else { + try? await self.manager.addToDownloadQueue(blocks: blocksToDownload) + self.downloadAllButtonState = .cancel + } + } + } + } + + @MainActor + func filterNotDownloadedBlocks(_ blocks: [CourseBlock]) -> [CourseBlock] { + return blocks.filter { block in + let fileUrl = manager.fileUrl(for: block.id) + return fileUrl == nil + } + } + + @MainActor + func isBlockDownloaded(_ block: CourseBlock) -> Bool { + courseDownloadTasks.contains { $0.blockId == block.id && $0.state == .finished } + } + + @MainActor + func stopAllDownloads() async { + do { + try await manager.cancelAllDownloading() + await setDownloadsStates(courseStructure: self.courseStructure) + await getDownloadingProgress() + } catch { + errorMessage = CoreLocalization.Error.unknownError + } + } + @MainActor func downloadableBlocks(from sequential: CourseSequential) -> [CourseBlock] { let verticals = sequential.childs @@ -472,9 +787,76 @@ public class CourseContainerViewModel: BaseCourseViewModel { .filter { $0.isDownloadable } return blocks } - + + @MainActor + func getDownloadingProgress() async { + guard let course = courseStructure else { return } + + var totalFilesSize: Int = 0 + var downloadedFilesSize: Int = 0 + var sequentials: [CourseSequential] = [] + + var updatedBlocks: [CourseBlock] = [] + for chapter in course.childs { + for sequential in chapter.childs { + sequentials.append(sequential) + for vertical in sequential.childs { + for block in vertical.childs { + let updatedBlock = await updateFileSizeIfNeeded(for: block) + updatedBlocks.append(updatedBlock) + } + } + } + } + + for block in updatedBlocks { + if let fileSize = block.fileSize { + totalFilesSize += fileSize + } + } + + if connectivity.isInternetAvaliable { + let updatedSequentials = manager.updateUnzippedFileSize(for: sequentials) + realDownloadedFilesSize = updatedSequentials.flatMap { + $0.childs.flatMap { $0.childs.compactMap { $0.actualFileSize } } + }.reduce(0, { $0 + $1 }) + } + + for task in courseDownloadTasks where task.state == .finished { + if let fileUrl = manager.fileUrl(for: task.blockId), + let fileSize = getFileSize(at: fileUrl), + task.type == .video { + if fileSize > 0 { + downloadedFilesSize += fileSize + } + } else { + downloadedFilesSize += task.fileSize + } + } + + withAnimation(.linear(duration: 0.3)) { + self.downloadedFilesSize = downloadedFilesSize + } + withAnimation(.linear(duration: 0.3)) { + self.totalFilesSize = totalFilesSize + } + await fetchLargestDownloadBlocks() + } + + private func getFileSize(at url: URL) -> Int? { + do { + let fileAttributes = try FileManager.default.attributesOfItem(atPath: url.path) + if let fileSize = fileAttributes[.size] as? Int, fileSize > 0 { + return fileSize + } + } catch { + debugLog("Error getting file size: \(error.localizedDescription)") + } + return nil + } + @MainActor - func setDownloadsStates() async { + func setDownloadsStates(courseStructure: CourseStructure?) async { guard let course = courseStructure else { return } courseDownloadTasks = await manager.getDownloadTasksForCourse(course.id) downloadableVerticals = [] @@ -485,7 +867,19 @@ public class CourseContainerViewModel: BaseCourseViewModel { for vertical in sequential.childs where vertical.isDownloadable { var verticalsChilds: [DownloadViewState] = [] for block in vertical.childs where block.isDownloadable { - if let download = courseDownloadTasks.first(where: { $0.blockId == block.id }) { + if var download = courseDownloadTasks.first(where: { $0.blockId == block.id }) { + if let newDateOfLastModified = block.offlineDownload?.lastModified, + let oldDateOfLastModified = download.lastModified { + if Date(iso8601: newDateOfLastModified) > Date(iso8601: oldDateOfLastModified) { + guard isEnoughSpace(for: block.fileSize ?? 0) else { return } + download.lastModified = newDateOfLastModified + try? await manager.cancelDownloading(task: download) + sequentialsChilds.append(.available) + verticalsChilds.append(.available) + try? await self.manager.addToDownloadQueue(blocks: [block]) + continue + } + } switch download.state { case .waiting, .inProgress: sequentialsChilds.append(.downloading) @@ -515,6 +909,13 @@ public class CourseContainerViewModel: BaseCourseViewModel { sequentialsStates[sequential.id] = .available } } + let allStates = sequentialsStates.values + if allStates.contains(.downloading) { + downloadAllButtonState = .cancel + } else { + downloadAllButtonState = .start + } + self.sequentialsDownloadState = sequentialsStates } } @@ -539,7 +940,140 @@ public class CourseContainerViewModel: BaseCourseViewModel { } return nil } - + + private func isEnoughSpace(for fileSize: Int) -> Bool { + if let freeSpace = getFreeDiskSpace() { + return freeSpace > Int(Double(fileSize) * 1.2) + } + return false + } + + private func getFreeDiskSpace() -> Int? { + do { + let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String) + if let freeSpace = attributes[.systemFreeSize] as? Int64 { + return Int(freeSpace) + } + } catch { + print("Error retrieving free disk space: \(error.localizedDescription)") + } + return nil + } + + private func getUsedDiskSpace() -> Int? { + do { + let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String) + if let totalSpace = attributes[.systemSize] as? Int64, + let freeSpace = attributes[.systemFreeSize] as? Int64 { + return Int(totalSpace - freeSpace) + } + } catch { + print("Error retrieving used disk space: \(error.localizedDescription)") + } + return nil + } + + // MARK: Larges Downloads + + @MainActor + func fetchLargestDownloadBlocks() async { + let allBlocks = courseStructure?.childs.flatMap { $0.childs.flatMap { $0.childs.flatMap { $0.childs } } } ?? [] + let downloadedBlocks = allBlocks.filter { block in + if let task = courseDownloadTasks.first(where: { $0.blockId == block.id }) { + return task.state == .finished + } + return false + } + + var updatedDownloadedBlocks: [CourseBlock] = [] + + for block in downloadedBlocks { + let updatedBlock = await updateFileSizeIfNeeded(for: block) + updatedDownloadedBlocks.append(updatedBlock) + } + + let filteredBlocks = Array( + updatedDownloadedBlocks + .filter { $0.fileSize != nil } + .sorted { $0.fileSize! > $1.fileSize! } + .prefix(5) + ) + + withAnimation(.linear(duration: 0.3)) { + largestDownloadBlocks = filteredBlocks + } + } + + @MainActor + func updateFileSizeIfNeeded(for block: CourseBlock) async -> CourseBlock { + var updatedBlock = block + if let fileUrl = manager.fileUrl(for: block.id), + let fileSize = getFileSize(at: fileUrl), fileSize > 0, + block.type == .video { + updatedBlock.actualFileSize = fileSize + } + return updatedBlock + } + + @MainActor + func removeBlock(_ block: CourseBlock) async { + router.presentView( + transitionStyle: .coverVertical, + view: DownloadActionView( + actionType: .remove, + courseBlocks: [block], + action: { [weak self] in + guard let self else { return } + withAnimation(.linear(duration: 0.3)) { + self.largestDownloadBlocks.removeAll { $0.id == block.id } + } + Task { + await self.manager.deleteFile(blocks: [block]) + } + self.router.dismiss(animated: true) + }, + cancel: { [weak self] in + guard let self else { return } + self.router.dismiss(animated: true) + } + ), + completion: {} + ) + } + + @MainActor + func removeAllBlocks() async { + let allBlocks = courseStructure?.childs.flatMap { $0.childs.flatMap { $0.childs.flatMap { $0.childs } } } ?? [] + let blocksToRemove = allBlocks.filter { block in + if let task = courseDownloadTasks.first(where: { $0.blockId == block.id }) { + return task.state == .finished + } + return false + } + + router.presentView( + transitionStyle: .coverVertical, + view: DownloadActionView( + actionType: .remove, + courseBlocks: blocksToRemove, + courseName: courseStructure?.displayName ?? "", + action: { [weak self] in + guard let self else { return } + Task { + await self.stopAllDownloads() + await self.manager.deleteFile(blocks: blocksToRemove) + } + self.router.dismiss(animated: true) + }, + cancel: { [weak self] in + guard let self else { return } + self.router.dismiss(animated: true) + } + ), + completion: {} + ) + } + private func addObservers() { manager.eventPublisher() .sink { [weak self] state in @@ -547,23 +1081,37 @@ public class CourseContainerViewModel: BaseCourseViewModel { if case .progress = state { return } Task(priority: .background) { debugLog(state, "--- state ---") - await self.setDownloadsStates() + await self.setDownloadsStates(courseStructure: self.courseStructure) + await self.getDownloadingProgress() } } .store(in: &cancellables) - + connectivity.internetReachableSubject .sink { [weak self] _ in - guard let self else { return } + guard let self else { return } self.isInternetAvaliable = self.connectivity.isInternetAvaliable - } - .store(in: &cancellables) + } + .store(in: &cancellables) NotificationCenter.default.addObserver( self, selector: #selector(handleShiftDueDates), name: .shiftCourseDates, object: nil ) + + completionPublisher + .sink { [weak self] _ in + guard let self = self else { return } + updateCourseProgress = true + } + .store(in: &cancellables) + + $sequentialsDownloadState.sink(receiveValue: { states in + if states.values.allSatisfy({ $0 == .available }) { + self.downloadAllButtonState = .start + } + }).store(in: &cancellables) } deinit { @@ -598,8 +1146,8 @@ extension CourseContainerViewModel { struct VerticalsDownloadState: Hashable { let vertical: CourseVertical let state: DownloadViewState - + var downloadableBlocks: [CourseBlock] { - vertical.childs.filter { $0.isDownloadable } + vertical.childs.filter { $0.isDownloadable && $0.type == .video } } } diff --git a/Course/Course/Presentation/CourseAnalytics.swift b/Course/Course/Presentation/CourseAnalytics.swift index bc531c152..28eb5918a 100644 --- a/Course/Course/Presentation/CourseAnalytics.swift +++ b/Course/Course/Presentation/CourseAnalytics.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation public enum EnrollmentMode: String { case audit @@ -58,6 +59,7 @@ public protocol CourseAnalytics { func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) func courseOutlineCourseTabClicked(courseId: String, courseName: String) func courseOutlineVideosTabClicked(courseId: String, courseName: String) + func courseOutlineOfflineTabClicked(courseId: String, courseName: String) func courseOutlineDatesTabClicked(courseId: String, courseName: String) func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) @@ -87,6 +89,8 @@ public protocol CourseAnalytics { snackbar: SnackbarType ) func trackCourseEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) + func trackCourseScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) + func plsEvent( _ event: AnalyticsEvent, bivalue: EventBIValue, @@ -135,6 +139,7 @@ class CourseAnalyticsMock: CourseAnalytics { public func finishVerticalBackToOutlineClicked(courseId: String, courseName: String) {} public func courseOutlineCourseTabClicked(courseId: String, courseName: String) {} public func courseOutlineVideosTabClicked(courseId: String, courseName: String) {} + public func courseOutlineOfflineTabClicked(courseId: String, courseName: String) {} public func courseOutlineDatesTabClicked(courseId: String, courseName: String) {} public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) {} public func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) {} @@ -164,6 +169,7 @@ class CourseAnalyticsMock: CourseAnalytics { snackbar: SnackbarType ) {} public func trackCourseEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) {} + public func trackCourseScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) {} public func plsEvent( _ event: AnalyticsEvent, bivalue: EventBIValue, diff --git a/Course/Course/Presentation/CourseRouter.swift b/Course/Course/Presentation/CourseRouter.swift index 570b5a09c..f3efe9cae 100644 --- a/Course/Course/Presentation/CourseRouter.swift +++ b/Course/Course/Presentation/CourseRouter.swift @@ -46,7 +46,8 @@ public protocol CourseRouter: BaseRouter { handouts: String?, announcements: [CourseUpdate]?, router: Course.CourseRouter, - cssInjector: CSSInjector + cssInjector: CSSInjector, + type: HandoutsItemType ) func showCourseComponent( @@ -59,6 +60,10 @@ public protocol CourseRouter: BaseRouter { downloads: [DownloadDataTask], manager: DownloadManagerProtocol ) + + func showDatesAndCalendar() + + func showGatedContentError(url: String) } // Mark - For testing and SwiftUI preview @@ -103,7 +108,8 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { handouts: String?, announcements: [CourseUpdate]?, router: Course.CourseRouter, - cssInjector: CSSInjector + cssInjector: CSSInjector, + type: HandoutsItemType ) {} public func showCourseComponent( @@ -116,5 +122,9 @@ public class CourseRouterMock: BaseRouterMock, CourseRouter { downloads: [Core.DownloadDataTask], manager: Core.DownloadManagerProtocol ) {} + + public func showDatesAndCalendar() {} + + public func showGatedContentError(url: String) {} } #endif diff --git a/Course/Course/Presentation/Dates/CourseDatesView.swift b/Course/Course/Presentation/Dates/CourseDatesView.swift index 92dc86a6c..0bccd3853 100644 --- a/Course/Course/Presentation/Dates/CourseDatesView.swift +++ b/Course/Course/Presentation/Dates/CourseDatesView.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import Core +import OEXFoundation import Theme import SwiftUIIntrospect @@ -19,16 +20,19 @@ public struct CourseDatesView: View { private var viewModel: CourseDatesViewModel @Binding private var coordinate: CGFloat @Binding private var collapsed: Bool + @Binding private var viewHeight: CGFloat public init( courseID: String, coordinate: Binding, collapsed: Binding, + viewHeight: Binding, viewModel: CourseDatesViewModel ) { self.courseID = courseID self._coordinate = coordinate self._collapsed = collapsed + self._viewHeight = viewHeight self._viewModel = StateObject(wrappedValue: viewModel) } @@ -46,10 +50,33 @@ public struct CourseDatesView: View { viewModel: viewModel, coordinate: $coordinate, collapsed: $collapsed, + viewHeight: $viewHeight, courseDates: courseDates, courseID: courseID ) .padding(.top, 10) + } else { + GeometryReader { proxy in + VStack { + ScrollView { + DynamicOffsetView( + coordinate: $coordinate, + collapsed: $collapsed, + viewHeight: $viewHeight + ) + + FullScreenErrorView( + type: .noContent( + CourseLocalization.Error.courseDateUnavailable, + image: CoreAssets.information.swiftUIImage + ) + ) + .frame(maxWidth: .infinity) + .frame(height: proxy.size.height - viewHeight) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } } } @@ -154,6 +181,7 @@ struct CourseDateListView: View { @State private var isExpanded = false @Binding var coordinate: CGFloat @Binding var collapsed: Bool + @Binding var viewHeight: CGFloat var courseDates: CourseDates let courseID: String @@ -163,13 +191,15 @@ struct CourseDateListView: View { ScrollView { DynamicOffsetView( coordinate: $coordinate, - collapsed: $collapsed + collapsed: $collapsed, + viewHeight: $viewHeight ) VStack(alignment: .leading, spacing: 0) { + + CalendarSyncStatusView(status: viewModel.syncStatus(), router: viewModel.router) + .padding(.bottom, 16) + if !courseDates.hasEnded { - CalendarSyncView(courseID: courseID, viewModel: viewModel) - .padding(.bottom, 16) - DatesStatusInfoView( datesBannerInfo: courseDates.datesBannerInfo, courseID: courseID, @@ -240,7 +270,7 @@ struct CompletedBlocks: View { }) { HStack { VStack(alignment: .leading) { - Text(CompletionStatus.completed.rawValue) + Text(CompletionStatus.completed.localized) .font(Theme.Fonts.titleSmall) .foregroundColor(Theme.Colors.textPrimary) @@ -288,6 +318,8 @@ struct CompletedBlocks: View { if block.canShowLink && !block.firstComponentBlockID.isEmpty { Image(systemName: "chevron.right") .resizable() + .flipsForRightToLeftLayoutDirection(true) + .scaledToFit() .frame(width: 6.55, height: 11.15) .labelStyle(.iconOnly) @@ -327,6 +359,7 @@ struct BlockStatusView: View { if block.canShowLink && !block.firstComponentBlockID.isEmpty { Image(systemName: "chevron.right") .resizable() + .flipsForRightToLeftLayoutDirection(true) .scaledToFit() .frame(width: 6.55, height: 11.15) .labelStyle(.iconOnly) @@ -405,42 +438,6 @@ struct StyleBlock: View { } } -struct CalendarSyncView: View { - let courseID: String - @ObservedObject var viewModel: CourseDatesViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Spacer() - HStack { - CoreAssets.syncToCalendar.swiftUIImage - Text(CourseLocalization.CourseDates.syncToCalendar) - .font(Theme.Fonts.titleMedium) - .foregroundColor(Theme.Colors.textPrimary) - Toggle("", isOn: .constant(viewModel.isOn)) - .toggleStyle(SwitchToggleStyle(tint: Theme.Colors.accentButtonColor)) - .padding(.trailing, 0) - .onTapGesture { - viewModel.calendarState = !viewModel.isOn - } - } - .padding(.horizontal, 16) - - Text(CourseLocalization.CourseDates.syncToCalendarMessage) - .frame(maxWidth: .infinity, alignment: .leading) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textPrimary) - .padding(.horizontal, 16) - Spacer() - } - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Theme.Colors.datesSectionStroke, lineWidth: 2) - ) - .background(Theme.Colors.datesSectionBackground) - } -} - fileprivate extension BlockStatus { var title: String { switch self { @@ -504,14 +501,17 @@ struct CourseDatesView_Previews: PreviewProvider { config: ConfigMock(), courseID: "", courseName: "", - analytics: CourseAnalyticsMock() + analytics: CourseAnalyticsMock(), + calendarManager: CalendarManagerMock() ) CourseDatesView( courseID: "", coordinate: .constant(0), - collapsed: .constant(false), - viewModel: viewModel) + collapsed: .constant(false), + viewHeight: .constant(0), + viewModel: viewModel + ) } } #endif diff --git a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift index 2effb2678..5ec8101c6 100644 --- a/Course/Course/Presentation/Dates/CourseDatesViewModel.swift +++ b/Course/Course/Presentation/Dates/CourseDatesViewModel.swift @@ -8,6 +8,7 @@ import Foundation import Core import SwiftUI +import OEXFoundation public class CourseDatesViewModel: ObservableObject { @@ -24,15 +25,6 @@ public class CourseDatesViewModel: ObservableObject { @Published var courseDates: CourseDates? @Published var isOn: Bool = false @Published var eventState: EventState? - - lazy var calendar: CalendarManager = { - return CalendarManager( - courseID: courseID, - courseName: courseStructure?.displayName ?? config.platformName, - courseStructure: courseStructure, - config: config - ) - }() var errorMessage: String? { didSet { @@ -42,21 +34,6 @@ public class CourseDatesViewModel: ObservableObject { } } - var calendarState: Bool { - get { - return calendar.syncOn - } - set { - if newValue { - trackCalendarSyncToggle(action: .on) - handleCalendar() - } else { - trackCalendarSyncToggle(action: .off) - showRemoveCalendarAlert() - } - } - } - private let interactor: CourseInteractorProtocol let cssInjector: CSSInjector let router: CourseRouter @@ -66,6 +43,7 @@ public class CourseDatesViewModel: ObservableObject { let courseName: String var courseStructure: CourseStructure? let analytics: CourseAnalytics + let calendarManager: CalendarManagerProtocol public init( interactor: CourseInteractorProtocol, @@ -75,7 +53,8 @@ public class CourseDatesViewModel: ObservableObject { config: ConfigProtocol, courseID: String, courseName: String, - analytics: CourseAnalytics + analytics: CourseAnalytics, + calendarManager: CalendarManagerProtocol ) { self.interactor = interactor self.router = router @@ -85,6 +64,7 @@ public class CourseDatesViewModel: ObservableObject { self.courseID = courseID self.courseName = courseName self.analytics = analytics + self.calendarManager = calendarManager addObservers() } @@ -112,18 +92,13 @@ public class CourseDatesViewModel: ObservableObject { await getCourseStructure(courseID: courseID) if courseDates?.courseDateBlocks == nil { isShowProgress = false - errorMessage = CoreLocalization.Error.unknownError + courseDates = nil return } isShowProgress = false - addCourseEventsIfNecessary() - } catch let error { + } catch { isShowProgress = false - if error.isInternetError || error is NoCachedDataError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError - } + courseDates = nil } } @@ -144,18 +119,21 @@ public class CourseDatesViewModel: ObservableObject { func getCourseStructure(courseID: String) async { do { courseStructure = try await interactor.getLoadedCourseBlocks(courseID: courseID) - isOn = calendarState } catch _ { errorMessage = CourseLocalization.Error.componentNotFount } } + func syncStatus() -> SyncStatus { + return calendarManager.courseStatus(courseID: courseID) + } + @MainActor func shiftDueDates(courseID: String, withProgress: Bool = true, screen: DatesStatusInfoScreen, type: String) async { isShowProgress = withProgress do { try await interactor.shiftDueDates(courseID: courseID) - NotificationCenter.default.post(name: .shiftCourseDates, object: courseID) + NotificationCenter.default.post(name: .shiftCourseDates, object: (courseID, courseName)) isShowProgress = false trackPLSuccessEvent( .plsShiftDatesSuccess, @@ -195,6 +173,18 @@ extension CourseDatesViewModel { selector: #selector(handleShiftDueDates), name: .shiftCourseDates, object: nil ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(getCourseDates), + name: .getCourseDates, object: nil + ) + } + + @objc private func getCourseDates(_ notification: Notification) { + Task { + await getCourseDates(courseID: courseID) + } } @objc private func handleShiftDueDates(_ notification: Notification) { @@ -214,212 +204,6 @@ extension CourseDatesViewModel { } extension CourseDatesViewModel { - private func handleCalendar() { - calendar.requestAccess { [weak self] _, previousStatus, status in - guard let self else { return } - switch status { - case .authorized: - if previousStatus == .notDetermined { - trackCalendarSyncDialogAction(dialog: .devicePermission, action: .allow) - } - showAddCalendarAlert() - default: - if previousStatus == .notDetermined { - trackCalendarSyncDialogAction(dialog: .devicePermission, action: .doNotAllow) - } - isOn = false - if previousStatus == status { - self.showCalendarSettingsAlert() - } - } - } - } - - @MainActor - func addCourseEvents(trackAnalytics: Bool = true, completion: ((Bool) -> Void)? = nil) { - guard let dateBlocks = courseDates?.dateBlocks else { return } - showCalendarSyncProgressView { [weak self] in - self?.calendar.addEventsToCalendar(for: dateBlocks) { [weak self] calendarEventsAdded in - self?.isOn = calendarEventsAdded - if calendarEventsAdded { - self?.calendar.syncOn = calendarEventsAdded - self?.router.dismiss(animated: false) - self?.showEventsAddedSuccessAlert() - } - completion?(calendarEventsAdded) - } - } - } - - func removeCourseCalendar(trackAnalytics: Bool = true, completion: ((Bool) -> Void)? = nil) { - calendar.removeCalendar { [weak self] success in - guard let self else { return } - self.isOn = !success - completion?(success) - } - } - - private func showAddCalendarAlert() { - router.presentAlert( - alertTitle: CourseLocalization.CourseDates.addCalendarTitle, - alertMessage: CourseLocalization.CourseDates.addCalendarPrompt( - config.platformName, - courseName - ), - positiveAction: CoreLocalization.Alert.accept, - onCloseTapped: { [weak self] in - self?.trackCalendarSyncDialogAction(dialog: .addCalendar, action: .cancel) - self?.router.dismiss(animated: true) - self?.isOn = false - self?.calendar.syncOn = false - }, - okTapped: { [weak self] in - self?.trackCalendarSyncDialogAction(dialog: .addCalendar, action: .add) - self?.router.dismiss(animated: true) - Task { [weak self] in - await self?.addCourseEvents() - } - }, - type: .addCalendar - ) - } - - private func showRemoveCalendarAlert() { - router.presentAlert( - alertTitle: CourseLocalization.CourseDates.removeCalendarTitle, - alertMessage: CourseLocalization.CourseDates.removeCalendarPrompt( - config.platformName, - courseName - ), - positiveAction: CoreLocalization.Alert.accept, - onCloseTapped: { [weak self] in - self?.trackCalendarSyncDialogAction(dialog: .removeCalendar, action: .cancel) - self?.router.dismiss(animated: true) - }, - okTapped: { [weak self] in - self?.trackCalendarSyncDialogAction(dialog: .removeCalendar, action: .remove) - self?.router.dismiss(animated: true) - self?.removeCourseCalendar { [weak self] _ in - self?.trackCalendarSyncSnackbar(snackbar: .removed) - self?.eventState = .removedCalendar - } - - }, - type: .removeCalendar - ) - } - - private func showEventsAddedSuccessAlert() { - if calendar.isModalPresented { - trackCalendarSyncSnackbar(snackbar: .added) - eventState = .addedCalendar - return - } - calendar.isModalPresented = true - router.presentAlert( - alertTitle: "", - alertMessage: CourseLocalization.CourseDates.datesAddedAlertMessage( - calendar.calendarName - ), - positiveAction: CourseLocalization.CourseDates.calendarViewEvents, - onCloseTapped: { [weak self] in - self?.trackCalendarSyncDialogAction(dialog: .eventsAdded, action: .done) - self?.router.dismiss(animated: true) - self?.isOn = true - self?.calendar.syncOn = true - }, - okTapped: { [weak self] in - self?.trackCalendarSyncDialogAction(dialog: .eventsAdded, action: .viewEvent) - self?.router.dismiss(animated: true) - if let url = URL(string: "calshow://"), UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - }, - type: .calendarAdded - ) - } - - func showCalendarSyncProgressView(completion: @escaping (() -> Void)) { - router.presentView( - transitionStyle: .crossDissolve, - view: CalendarSyncProgressView( - title: CourseLocalization.CourseDates.calendarSyncMessage - ), - completion: completion - ) - } - - @MainActor - private func addCourseEventsIfNecessary() { - Task { - if calendar.syncOn && calendar.checkIfEventsShouldBeShifted(for: courseDates?.dateBlocks ?? [:]) { - showCalendarEventShiftAlert() - } - } - } - - @MainActor - private func showCalendarEventShiftAlert() { - router.presentAlert( - alertTitle: CourseLocalization.CourseDates.calendarOutOfDate, - alertMessage: CourseLocalization.CourseDates.calendarShiftMessage, - positiveAction: CourseLocalization.CourseDates.calendarShiftPromptUpdateNow, - onCloseTapped: { [weak self] in - // Remove course calendar - self?.trackCalendarSyncDialogAction(dialog: .updateCalendar, action: .remove) - self?.router.dismiss(animated: true) - self?.removeCourseCalendar { [weak self] _ in - self?.trackCalendarSyncSnackbar(snackbar: .removed) - self?.eventState = .removedCalendar - } - }, - okTapped: { [weak self] in - // Update Calendar Now - self?.trackCalendarSyncDialogAction(dialog: .updateCalendar, action: .update) - self?.router.dismiss(animated: true) - self?.removeCourseCalendar(trackAnalytics: false) { success in - self?.isOn = !success - self?.calendar.syncOn = false - self?.addCourseEvents(trackAnalytics: false) { [weak self] calendarEventsAdded in - self?.isOn = calendarEventsAdded - if calendarEventsAdded { - self?.trackCalendarSyncSnackbar(snackbar: .updated) - self?.calendar.syncOn = calendarEventsAdded - self?.eventState = .updatedCalendar - } - } - } - }, - type: .updateCalendar - ) - } - - private func showCalendarSettingsAlert() { - guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { - return - } - - router.presentAlert( - alertTitle: CourseLocalization.CourseDates.settings, - alertMessage: CourseLocalization.CourseDates.calendarPermissionNotDetermined(config.platformName), - positiveAction: CourseLocalization.CourseDates.openSettings, - onCloseTapped: { [weak self] in - self?.isOn = false - self?.router.dismiss(animated: true) - }, - okTapped: { [weak self] in - self?.isOn = false - if UIApplication.shared.canOpenURL(settingsURL) { - UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) - } - self?.router.dismiss(animated: true) - }, - type: .default( - positiveAction: CourseLocalization.CourseDates.openSettings, - image: CoreAssets.syncToCalendar.swiftUIImage - ) - ) - } func logdateComponentTapped(block: CourseDateBlock, supported: Bool) { analytics.datesComponentTapped( @@ -464,33 +248,3 @@ extension CourseDatesViewModel { ) } } - -extension CourseDatesViewModel { - private func trackCalendarSyncToggle(action: CalendarDialogueAction) { - analytics.calendarSyncToggle( - enrollmentMode: .none, - pacing: courseStructure?.isSelfPaced ?? true ? .`self` : .instructor, - courseId: courseID, - action: action - ) - } - - private func trackCalendarSyncDialogAction(dialog: CalendarDialogueType, action: CalendarDialogueAction) { - analytics.calendarSyncDialogAction( - enrollmentMode: .none, - pacing: courseStructure?.isSelfPaced ?? true ? .`self` : .instructor, - courseId: courseID, - dialog: dialog, - action: action - ) - } - - private func trackCalendarSyncSnackbar(snackbar: SnackbarType) { - analytics.calendarSyncSnackbar( - enrollmentMode: .none, - pacing: courseStructure?.isSelfPaced ?? true ? .`self` : .instructor, - courseId: courseID, - snackbar: snackbar - ) - } -} diff --git a/Course/Course/Presentation/Downloads/DownloadsView.swift b/Course/Course/Presentation/Downloads/DownloadsView.swift index 3c648ead4..2926d82b0 100644 --- a/Course/Course/Presentation/Downloads/DownloadsView.swift +++ b/Course/Course/Presentation/Downloads/DownloadsView.swift @@ -15,12 +15,14 @@ public struct DownloadsView: View { // MARK: - Properties @Environment(\.dismiss) private var dismiss + @Environment(\.isHorizontal) private var isHorizontal @StateObject private var viewModel: DownloadsViewModel var isSheet: Bool = true public init( isSheet: Bool = true, + router: CourseRouter, courseId: String? = nil, downloads: [DownloadDataTask] = [], manager: DownloadManagerProtocol @@ -28,6 +30,7 @@ public struct DownloadsView: View { self.isSheet = isSheet self._viewModel = .init( wrappedValue: .init( + router: router, courseId: courseId, downloads: downloads, manager: manager @@ -38,13 +41,36 @@ public struct DownloadsView: View { // MARK: - Body public var body: some View { - ZStack { + ZStack(alignment: .top) { Theme.Colors.background .ignoresSafeArea() + if !isSheet { + HStack { + Text(CourseLocalization.Download.downloads) + .titleSettings(color: Theme.Colors.textPrimary) + .accessibilityIdentifier("downloads_text") + } + .padding(.top, isHorizontal ? 10 : 0) + VStack { + BackNavigationButton( + color: Theme.Colors.accentColor, + action: { + viewModel.router.back() + } + ) + .backViewStyle() + .padding(.leading, 8) + + } + .frame(minWidth: 0, maxWidth: .infinity, alignment: .topLeading) + .padding(.top, isHorizontal ? 23 : 13) + + } content .sheetNavigation(isSheet: isSheet) { dismiss() } + .padding(.top, isSheet ? 0 : 40) } } @@ -100,6 +126,7 @@ public struct DownloadsView: View { } } label: { DownloadProgressView() + .id("cirle loading indicator " + task.id) .accessibilityElement(children: .ignore) .accessibilityLabel(CourseLocalization.Accessibility.cancelDownload) .accessibilityIdentifier("cancel_download_button") diff --git a/Course/Course/Presentation/Downloads/DownloadsViewModel.swift b/Course/Course/Presentation/Downloads/DownloadsViewModel.swift index 78c063778..cf74c6c65 100644 --- a/Course/Course/Presentation/Downloads/DownloadsViewModel.swift +++ b/Course/Course/Presentation/Downloads/DownloadsViewModel.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import Combine final class DownloadsViewModel: ObservableObject { @@ -15,15 +16,19 @@ final class DownloadsViewModel: ObservableObject { @Published private(set) var downloads: [DownloadDataTask] = [] private let courseId: String? + + let router: CourseRouter private let manager: DownloadManagerProtocol private var cancellables = Set() init( + router: CourseRouter, courseId: String? = nil, downloads: [DownloadDataTask] = [], manager: DownloadManagerProtocol ) { + self.router = router self.courseId = courseId self.manager = manager self.downloads = downloads diff --git a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift index 115262c09..c4c5923c7 100644 --- a/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsUpdatesDetailView.swift @@ -20,25 +20,26 @@ public struct HandoutsUpdatesDetailView: View { private var handouts: String? private var announcements: [CourseUpdate]? private let title: String + private let type: HandoutsItemType public init( handouts: String?, announcements: [CourseUpdate]?, router: CourseRouter, - cssInjector: CSSInjector + cssInjector: CSSInjector, + type: HandoutsItemType ) { - let noHandouts = handouts == nil && announcements == nil - - if announcements == nil { + switch type { + case .handouts: self.title = CourseLocalization.HandoutsCellHandouts.title - } else { + case .announcements: self.title = CourseLocalization.HandoutsCellAnnouncements.title } - - self.handouts = noHandouts ? CourseLocalization.Error.noHandouts : handouts + self.handouts = handouts self.announcements = announcements self.router = router self.cssInjector = cssInjector + self.type = type } private func updateColorScheme() { @@ -78,15 +79,31 @@ public struct HandoutsUpdatesDetailView: View { ZStack(alignment: .top) { Theme.Colors.background .ignoresSafeArea() - // MARK: - Page Body - WebViewHtml(html(), injections: [.accessibility, .readability]) - .padding(.top, 8) - .frame( - maxHeight: .infinity, - alignment: .topLeading) - .onRightSwipeGesture { - router.back() + + switch type { + case .handouts: + if handouts?.isEmpty ?? true { + FullScreenErrorView( + type: .noContent( + CourseLocalization.Error.handoutsUnavailable, + image: CoreAssets.noHandouts.swiftUIImage + ) + ) + } else { + webViewHtml } + case .announcements: + if announcements?.isEmpty ?? true { + FullScreenErrorView( + type: .noContent( + CourseLocalization.Error.announcementsUnavailable, + image: CoreAssets.noAnnouncements.swiftUIImage + ) + ) + } else { + webViewHtml + } + } } .navigationBarHidden(false) .navigationBarBackButtonHidden(false) @@ -97,6 +114,19 @@ public struct HandoutsUpdatesDetailView: View { } } + private var webViewHtml: some View { + // MARK: - Page Body + WebViewHtml(html(), injections: [.accessibility, .readability]) + .padding(.top, 8) + .frame( + maxHeight: .infinity, + alignment: .topLeading + ) + .onRightSwipeGesture { + router.back() + } + } + func html() -> String { var html: String = "" if let handouts { @@ -229,7 +259,8 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i content: loremIpsumHtml, status: "nice")], router: CourseRouterMock(), - cssInjector: CSSInjectorMock() + cssInjector: CSSInjectorMock(), + type: .handouts ) } } diff --git a/Course/Course/Presentation/Handouts/HandoutsView.swift b/Course/Course/Presentation/Handouts/HandoutsView.swift index de8bb631f..e3998963c 100644 --- a/Course/Course/Presentation/Handouts/HandoutsView.swift +++ b/Course/Course/Presentation/Handouts/HandoutsView.swift @@ -14,6 +14,7 @@ struct HandoutsView: View { private let courseID: String @Binding private var coordinate: CGFloat @Binding private var collapsed: Bool + @Binding private var viewHeight: CGFloat @StateObject private var viewModel: HandoutsViewModel @@ -22,11 +23,13 @@ struct HandoutsView: View { courseID: String, coordinate: Binding, collapsed: Binding, + viewHeight: Binding, viewModel: HandoutsViewModel ) { self.courseID = courseID self._coordinate = coordinate self._collapsed = collapsed + self._viewHeight = viewHeight self._viewModel = StateObject(wrappedValue: { viewModel }()) } @@ -38,7 +41,8 @@ struct HandoutsView: View { ScrollView { DynamicOffsetView( coordinate: $coordinate, - collapsed: $collapsed + collapsed: $collapsed, + viewHeight: $viewHeight ) if viewModel.isShowProgress { HStack(alignment: .center) { @@ -48,32 +52,37 @@ struct HandoutsView: View { } } else { VStack(alignment: .leading) { - HandoutsItemCell(type: .handouts, onTapAction: { + HandoutsItemCell(type: .handouts, onTapAction: { type in viewModel.router.showHandoutsUpdatesView( handouts: viewModel.handouts, announcements: nil, router: viewModel.router, - cssInjector: viewModel.cssInjector) - viewModel.analytics.trackCourseEvent( + cssInjector: viewModel.cssInjector, + type: type + ) + viewModel.analytics.trackCourseScreenEvent( .courseHandouts, biValue: .courseHandouts, courseID: courseID ) }) Divider() - HandoutsItemCell(type: .announcements, onTapAction: { - if !viewModel.updates.isEmpty { - viewModel.router.showHandoutsUpdatesView( - handouts: nil, - announcements: viewModel.updates, - router: viewModel.router, - cssInjector: viewModel.cssInjector) - viewModel.analytics.trackCourseEvent( - .courseAnnouncement, - biValue: .courseAnnouncement, - courseID: courseID - ) - } + .frame(height: 1) + .overlay(Theme.Colors.cardViewStroke) + .accessibilityIdentifier("divider") + HandoutsItemCell(type: .announcements, onTapAction: { type in + viewModel.router.showHandoutsUpdatesView( + handouts: nil, + announcements: viewModel.updates, + router: viewModel.router, + cssInjector: viewModel.cssInjector, + type: type + ) + viewModel.analytics.trackCourseEvent( + .courseAnnouncement, + biValue: .courseAnnouncement, + courseID: courseID + ) }) }.padding(.horizontal, 32) Spacer(minLength: 84) @@ -91,22 +100,6 @@ struct HandoutsView: View { } } ) - - // MARK: - Error Alert - if viewModel.showError { - VStack { - Spacer() - SnackBarView(message: viewModel.errorMessage) - } - .padding(.bottom, viewModel.connectivity.isInternetAvaliable - ? 0 : OfflineSnackBarView.height) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil - } - } - } } .onFirstAppear { @@ -137,57 +130,57 @@ struct HandoutsView_Previews: PreviewProvider { courseID: "", coordinate: .constant(0), collapsed: .constant(false), + viewHeight: .constant(0), viewModel: viewModel ) } } #endif -struct HandoutsItemCell: View { +public enum HandoutsItemType: String { + case handouts + case announcements - enum ItemType { - case handouts - case announcements - - var title: String { - switch self { - case .handouts: - return CourseLocalization.HandoutsCellHandouts.title - case .announcements: - return CourseLocalization.HandoutsCellAnnouncements.title - } - } - - var description: String { - switch self { - case .handouts: - return CourseLocalization.HandoutsCellHandouts.description - case .announcements: - return CourseLocalization.HandoutsCellAnnouncements.description - } + var title: String { + switch self { + case .handouts: + return CourseLocalization.HandoutsCellHandouts.title + case .announcements: + return CourseLocalization.HandoutsCellAnnouncements.title } - - var image: Image { - switch self { - case .handouts: - return CoreAssets.handouts.swiftUIImage - case .announcements: - return CoreAssets.announcements.swiftUIImage - } + } + + var description: String { + switch self { + case .handouts: + return CourseLocalization.HandoutsCellHandouts.description + case .announcements: + return CourseLocalization.HandoutsCellAnnouncements.description } } - private let type: ItemType - private let onTapAction: () -> Void + var image: Image { + switch self { + case .handouts: + return CoreAssets.handouts.swiftUIImage + case .announcements: + return CoreAssets.announcements.swiftUIImage + } + } +} + +struct HandoutsItemCell: View { + private let type: HandoutsItemType + private let onTapAction: (HandoutsItemType) -> Void - public init(type: ItemType, onTapAction: @escaping () -> Void) { + public init(type: HandoutsItemType, onTapAction: @escaping (HandoutsItemType) -> Void) { self.type = type self.onTapAction = onTapAction } public var body: some View { Button(action: { - onTapAction() + onTapAction(type) }, label: { HStack(spacing: 12) { type.image.renderingMode(.template) @@ -202,7 +195,9 @@ struct HandoutsItemCell: View { .font(Theme.Fonts.labelSmall) } Spacer() - Image(systemName: "chevron.right").resizable() + Image(systemName: "chevron.right") + .resizable() + .flipsForRightToLeftLayoutDirection(true) .frame(width: 7, height: 12) .foregroundColor(Theme.Colors.accentColor) } diff --git a/Course/Course/Presentation/Handouts/HandoutsViewModel.swift b/Course/Course/Presentation/Handouts/HandoutsViewModel.swift index c5fc64a64..f4380dc6c 100644 --- a/Course/Course/Presentation/Handouts/HandoutsViewModel.swift +++ b/Course/Course/Presentation/Handouts/HandoutsViewModel.swift @@ -55,11 +55,6 @@ public class HandoutsViewModel: ObservableObject { } } catch let error { isShowProgress = false - if error.isInternetError || error is NoCachedDataError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError - } } } @@ -71,12 +66,6 @@ public class HandoutsViewModel: ObservableObject { isShowProgress = false } catch let error { isShowProgress = false - if error.isInternetError || error is NoCachedDataError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError - } } } - } diff --git a/Course/Course/Presentation/Offline/OfflineView.swift b/Course/Course/Presentation/Offline/OfflineView.swift new file mode 100644 index 000000000..eab6e8331 --- /dev/null +++ b/Course/Course/Presentation/Offline/OfflineView.swift @@ -0,0 +1,274 @@ +// +// OfflineView.swift +// Course +// +// Created by  Stepanok Ivan on 17.06.2024. +// + +import SwiftUI +import Core +import OEXFoundation +import Theme + +struct OfflineView: View { + + enum DownloadAllState: Equatable { + case start + case cancel + + var color: Color { + switch self { + case .start: + Theme.Colors.accentColor + case .cancel: + Theme.Colors.snackbarErrorColor + } + } + + var image: Image { + switch self { + case .start: + CoreAssets.startDownloading.swiftUIImage + case .cancel: + CoreAssets.stopDownloading.swiftUIImage + } + } + + var title: String { + switch self { + case .start: + CourseLocalization.Course.Offline.downloadAll + case .cancel: + CourseLocalization.Course.Offline.cancelCourseDownload + } + } + + var textColor: Color { + switch self { + case .start: + Theme.Colors.white + case .cancel: + Theme.Colors.snackbarErrorColor + } + } + } + + private let courseID: String + @Binding private var coordinate: CGFloat + @Binding private var collapsed: Bool + @Binding private var viewHeight: CGFloat + + @StateObject + private var viewModel: CourseContainerViewModel + + public init( + courseID: String, + coordinate: Binding, + collapsed: Binding, + viewHeight: Binding, + viewModel: CourseContainerViewModel + ) { + self.courseID = courseID + self._coordinate = coordinate + self._collapsed = collapsed + self._viewHeight = viewHeight + self._viewModel = StateObject(wrappedValue: { viewModel }()) + } + + public var body: some View { + GeometryReader { proxy in + ZStack(alignment: .center) { + VStack(alignment: .center) { + + // MARK: - Page Body + if viewModel.isShowProgress { + HStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + } + } else { + ScrollView { + VStack(alignment: .leading) { + DynamicOffsetView( + coordinate: $coordinate, + collapsed: $collapsed, + viewHeight: $viewHeight + ) + TotalDownloadedProgressView( + downloadedFilesSize: viewModel.downloadedFilesSize, + totalFilesSize: viewModel.totalFilesSize, + isDownloading: Binding( + get: { viewModel.downloadAllButtonState == .cancel }, + set: { newValue in + viewModel.downloadAllButtonState = newValue ? .cancel : .start + } + ) + ) + .padding(.top, 36) + + if viewModel.downloadedFilesSize == 0 && viewModel.totalFilesSize != 0 { + Text(CourseLocalization.Course.Offline.youCanDownload) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.top, 8) + .padding(.bottom, 16) + } else if viewModel.downloadedFilesSize == 0 && viewModel.totalFilesSize == 0 { + Text(CourseLocalization.Course.Offline.youCantDownload) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.top, 8) + .padding(.bottom, 16) + } + downloadAll + + if !viewModel.largestDownloadBlocks.isEmpty { + LargestDownloadsView(viewModel: viewModel) + } + removeAllDownloads + + }.padding(.horizontal, 32) + Spacer(minLength: 84) + } + } + } + .frameLimit(width: proxy.size.width) + + // MARK: - Offline mode SnackBar + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: {} + ) + + // MARK: - Error Alert + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, viewModel.connectivity.isInternetAvaliable + ? 0 : OfflineSnackBarView.height) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } + } + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + } + } + + @ViewBuilder + private var downloadAll: some View { + if viewModel.connectivity.isInternetAvaliable + && ((viewModel.totalFilesSize - viewModel.downloadedFilesSize != 0) + || (viewModel.totalFilesSize == 0 && viewModel.downloadedFilesSize == 0)) { + Button(action: { + Task(priority: .low) { + switch viewModel.downloadAllButtonState { + case .start: + await viewModel.downloadAll() + case .cancel: + viewModel.downloadAllButtonState = .start + await viewModel.stopAllDownloads() + } + } + }) { + HStack { + viewModel.downloadAllButtonState.image + .renderingMode(.template) + Text(viewModel.downloadAllButtonState.title) + .font(Theme.Fonts.bodyMedium) + } + .foregroundStyle( + viewModel.totalFilesSize == 0 + ? Theme.Colors.disabledButtonText + : viewModel.downloadAllButtonState.textColor + ) + .frame(maxWidth: .infinity) + .frame(height: 42) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke( + viewModel.totalFilesSize == 0 + ? .clear + : viewModel.downloadAllButtonState.color, + lineWidth: 2 + ) + ) + .background( + viewModel.totalFilesSize == 0 + ? Theme.Colors.disabledButton + : viewModel.downloadAllButtonState == .start ? viewModel.downloadAllButtonState.color : .clear + ) + .cornerRadius(8) + } + } + } + + @ViewBuilder + private var removeAllDownloads: some View { + if viewModel.downloadAllButtonState == .start && !viewModel.largestDownloadBlocks.isEmpty { + VStack(spacing: 16) { + Button(action: { + Task { + await viewModel.removeAllBlocks() + } + }) { + HStack { + CoreAssets.remove.swiftUIImage + Text(CourseLocalization.Course.LargestDownloads.removeDownloads) + .font(Theme.Fonts.bodyMedium) + } + .foregroundStyle(Theme.Colors.snackbarErrorColor) + .frame(maxWidth: .infinity) + .frame(height: 42) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.snackbarErrorColor, lineWidth: 2) + ) + .background(Theme.Colors.background) + .cornerRadius(8) + } + } + .padding(.vertical, 4) + } + } +} + +#if DEBUG +#Preview { + let vm = CourseContainerViewModel( + interactor: CourseInteractor.mock, + authInteractor: AuthInteractor.mock, + router: CourseRouterMock(), + analytics: CourseAnalyticsMock(), + config: ConfigMock(), + connectivity: Connectivity(), + manager: DownloadManagerMock(), + storage: CourseStorageMock(), + isActive: true, + courseStart: nil, + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil, + lastVisitedBlockID: nil, + coreAnalytics: CoreAnalyticsMock() + ) + + return OfflineView( + courseID: "123", + coordinate: .constant(0), + collapsed: .constant(false), + viewHeight: .constant(0), + viewModel: vm + ).onAppear { + vm.isShowProgress = false + } +} +#endif diff --git a/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift b/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift new file mode 100644 index 000000000..6099d7e38 --- /dev/null +++ b/Course/Course/Presentation/Offline/Subviews/LargestDownloadsView.swift @@ -0,0 +1,178 @@ +// +// LargestDownloadsView.swift +// Course +// +// Created by  Stepanok Ivan on 18.06.2024. +// + +import SwiftUI +import Core +import Theme + +public struct LargestDownloadsView: View { + + @State private var isEditing = false + @ObservedObject + private var viewModel: CourseContainerViewModel + + init(viewModel: CourseContainerViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + VStack(alignment: .leading) { + HStack { + Text(CourseLocalization.Course.LargestDownloads.title) + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textPrimary) + Spacer() + if viewModel.downloadAllButtonState == .start { + Button(action: { + isEditing.toggle() + }) { + Text( + isEditing + ? CourseLocalization.Course.LargestDownloads.done + : CourseLocalization.Course.LargestDownloads.edit + ) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.accentColor) + } + } + } + .padding(.vertical) + + ForEach(viewModel.largestDownloadBlocks) { block in + HStack { + block.type.image + VStack(alignment: .leading) { + Text(block.displayName) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + if let fileSize = block.fileSize { + Text(fileSize.formattedFileSize()) + .font(Theme.Fonts.labelSmall) + .foregroundColor(Theme.Colors.textSecondary) + } + } + Spacer() + if isEditing { + Button(action: { + Task { + await viewModel.removeBlock(block) + } + }) { + CoreAssets.remove.swiftUIImage + .foregroundColor(Theme.Colors.alert) + } + } else { + CoreAssets.deleteDownloading.swiftUIImage + .foregroundColor(.green) + } + } + Divider() + .foregroundStyle(Theme.Colors.shade) + .padding(.vertical, 8) + } + } + .onChange(of: viewModel.downloadAllButtonState, perform: { state in + if state == .cancel { + self.isEditing = false + } + }) + .onAppear { + Task { + await viewModel.fetchLargestDownloadBlocks() + } + } + } +} + +#if DEBUG +struct LargestDownloadsView_Previews: PreviewProvider { + static var previews: some View { + + let vm = CourseContainerViewModel( + interactor: CourseInteractor.mock, + authInteractor: AuthInteractor.mock, + router: CourseRouterMock(), + analytics: CourseAnalyticsMock(), + config: ConfigMock(), + connectivity: Connectivity(), + manager: DownloadManagerMock(), + storage: CourseStorageMock(), + isActive: true, + courseStart: nil, + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil, + lastVisitedBlockID: nil, + coreAnalytics: CoreAnalyticsMock() + ) + + LargestDownloadsView(viewModel: vm) + .loadFonts() + .onAppear { + vm.largestDownloadBlocks = [ + CourseBlock( + blockId: "", + id: "1", + courseId: "", + graded: false, + due: nil, + completion: 0, + type: .discussion, + displayName: "Welcome to Mobile Testing", + studentUrl: "", + webUrl: "", + encodedVideo: nil, + multiDevice: nil, + offlineDownload: OfflineDownload( + fileUrl: "123", + lastModified: "e", + fileSize: 3423123214 + ) + ), + CourseBlock( + blockId: "", + id: "2", + courseId: "", + graded: false, + due: nil, + completion: 0, + type: .video, + displayName: "Advanced Mobile Sketching", + studentUrl: "", + webUrl: "", + encodedVideo: nil, + multiDevice: nil, + offlineDownload: OfflineDownload( + fileUrl: "123", + lastModified: "e", + fileSize: 34213214 + ) + ), + CourseBlock( + blockId: "", + id: "3", + courseId: "", + graded: false, + due: nil, + completion: 0, + type: .problem, + displayName: "File Naming Conventions", + studentUrl: "", + webUrl: "", + encodedVideo: nil, + multiDevice: nil, + offlineDownload: OfflineDownload( + fileUrl: "123", + lastModified: "e", + fileSize: 742343214 + ) + ) + ] + } + } +} +#endif diff --git a/Course/Course/Presentation/Offline/Subviews/TotalDownloadedProgressView.swift b/Course/Course/Presentation/Offline/Subviews/TotalDownloadedProgressView.swift new file mode 100644 index 000000000..59bcd40b8 --- /dev/null +++ b/Course/Course/Presentation/Offline/Subviews/TotalDownloadedProgressView.swift @@ -0,0 +1,105 @@ +// +// TotalDownloadedProgressView.swift +// Course +// +// Created by  Stepanok Ivan on 17.06.2024. +// + +import SwiftUI +import Theme +import Core + +public struct TotalDownloadedProgressView: View { + + private let downloadedFilesSize: Int + private let readyToDownload: Int + private let totalFilesSize: Int + @Binding var isDownloading: Bool + + public init(downloadedFilesSize: Int, totalFilesSize: Int, isDownloading: Binding) { + self.downloadedFilesSize = downloadedFilesSize + self.totalFilesSize = totalFilesSize + self.readyToDownload = totalFilesSize - downloadedFilesSize + self._isDownloading = isDownloading + } + + public var body: some View { + VStack(alignment: .center, spacing: 6) { + HStack { + Text(downloadedFilesSize.formattedFileSize()) + .foregroundStyle( + totalFilesSize == 0 + ? Theme.Colors.textSecondaryLight + : Theme.Colors.success + ) + Spacer() + if totalFilesSize != 0 { + Text(readyToDownload.formattedFileSize()) + } + } + .font(Theme.Fonts.titleLarge) + HStack { + CoreAssets.deleteDownloading.swiftUIImage.renderingMode(.template) + .foregroundStyle( + totalFilesSize == 0 + ? Theme.Colors.textSecondaryLight + : Theme.Colors.success + ) + Text(totalFilesSize == 0 + ? CourseLocalization.Course.TotalProgress.avaliableToDownload + : CourseLocalization.Course.TotalProgress.downloaded) + .foregroundStyle( + totalFilesSize == 0 + ? Theme.Colors.textSecondaryLight + : Theme.Colors.success + ) + Spacer() + if totalFilesSize != 0 { + CoreAssets.startDownloading.swiftUIImage + Text(isDownloading ? + CourseLocalization.Course.TotalProgress.downloading + : CourseLocalization.Course.TotalProgress.readyToDownload) + } + } + .font(Theme.Fonts.labelLarge) + .padding(.bottom, 10) + if totalFilesSize != 0 { + ZStack(alignment: .leading) { + GeometryReader { geometry in + RoundedRectangle(cornerRadius: 2.5) + .fill(Theme.Colors.textSecondary.opacity(0.5)) + .frame(width: geometry.size.width, height: 5) + + RoundedCorners(tl: 2.5, tr: 0, bl: 2.5, br: 0) + .fill(Theme.Colors.success) + .frame( + width: geometry.size.width * CGFloat( + downloadedFilesSize + ) / CGFloat(totalFilesSize), + height: 5 + ) + } + .frame(height: 5) + } + .cornerRadius(5) + .padding(.bottom, 10) + } + } + .onChange(of: readyToDownload, perform: { size in + if size == 0 { + self.isDownloading = false + } + }) + } +} + +#if DEBUG +#Preview { + TotalDownloadedProgressView( + downloadedFilesSize: 24341324514, + totalFilesSize: 324324132413, + isDownloading: .constant(false) + ) + .loadFonts() +} +#endif diff --git a/Course/Course/Presentation/Outline/ContinueWithView.swift b/Course/Course/Presentation/Outline/ContinueWithView.swift index 1c05c575b..9da8b7a6c 100644 --- a/Course/Course/Presentation/Outline/ContinueWithView.swift +++ b/Course/Course/Presentation/Outline/ContinueWithView.swift @@ -41,7 +41,6 @@ struct ContinueWithView: View { .frame(width: 200) } .padding(.horizontal, 24) - .padding(.top, 32) } else { VStack(alignment: .leading) { ContinueTitle(vertical: courseContinueUnit) @@ -83,26 +82,30 @@ struct ContinueWithView_Previews: PreviewProvider { id: "1", courseId: "123", graded: true, + due: Date(), completion: 0, type: .html, displayName: "Continue lesson", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ), CourseBlock( blockId: "2", id: "2", courseId: "123", graded: true, + due: Date(), completion: 0, type: .html, displayName: "Continue lesson", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ) ] @@ -121,7 +124,8 @@ struct ContinueWithView_Previews: PreviewProvider { displayName: "Second Unit", type: .vertical, completion: 1, - childs: blocks + childs: blocks, + webUrl: "" ) ) { } } diff --git a/Course/Course/Presentation/Outline/CourseOutlineView.swift b/Course/Course/Presentation/Outline/CourseOutlineView.swift index 18575dc7a..37e14b589 100644 --- a/Course/Course/Presentation/Outline/CourseOutlineView.swift +++ b/Course/Course/Presentation/Outline/CourseOutlineView.swift @@ -7,13 +7,14 @@ import SwiftUI import Core +import OEXFoundation import Kingfisher import Theme import SwiftUIIntrospect public struct CourseOutlineView: View { - @ObservedObject private var viewModel: CourseContainerViewModel + @StateObject private var viewModel: CourseContainerViewModel private let title: String private let courseID: String private let isVideo: Bool @@ -29,6 +30,9 @@ public struct CourseOutlineView: View { @Binding private var selection: Int @Binding private var coordinate: CGFloat @Binding private var collapsed: Bool + @Binding private var viewHeight: CGFloat + + @State private var expandedChapters: [String: Bool] = [:] public init( viewModel: CourseContainerViewModel, @@ -38,122 +42,105 @@ public struct CourseOutlineView: View { selection: Binding, coordinate: Binding, collapsed: Binding, + viewHeight: Binding, dateTabIndex: Int ) { self.title = title - self.viewModel = viewModel//StateObject(wrappedValue: { viewModel }()) + self._viewModel = StateObject(wrappedValue: { viewModel }()) self.courseID = courseID self.isVideo = isVideo self._selection = selection self._coordinate = coordinate self._collapsed = collapsed + self._viewHeight = viewHeight self.dateTabIndex = dateTabIndex } public var body: some View { ZStack(alignment: .top) { - // MARK: - Page name GeometryReader { proxy in VStack(alignment: .center) { - // MARK: - Page Body - RefreshableScrollViewCompat(action: { - await withTaskGroup(of: Void.self) { group in - group.addTask { - await viewModel.getCourseBlocks(courseID: courseID, withProgress: false) - } - group.addTask { - await viewModel.getCourseDeadlineInfo(courseID: courseID, withProgress: false) - } - } - }) { - DynamicOffsetView( - coordinate: $coordinate, - collapsed: $collapsed - ) - RefreshProgressView(isShowRefresh: $viewModel.isShowRefresh) - VStack(alignment: .leading) { - if let courseDeadlineInfo = viewModel.courseDeadlineInfo, - courseDeadlineInfo.datesBannerInfo.status == .resetDatesBanner, - !courseDeadlineInfo.hasEnded, - !isVideo { - DatesStatusInfoView( - datesBannerInfo: courseDeadlineInfo.datesBannerInfo, - courseID: courseID, - courseContainerViewModel: viewModel, - screen: .courseDashbaord - ) - .padding(.horizontal, 16) - } - - downloadQualityBars - certificateView - - if let continueWith = viewModel.continueWith, - let courseStructure = viewModel.courseStructure, - !isVideo { - let chapter = courseStructure.childs[continueWith.chapterIndex] - let sequential = chapter.childs[continueWith.sequentialIndex] - let continueUnit = sequential.childs[continueWith.verticalIndex] + ScrollView { + VStack(spacing: 0) { + DynamicOffsetView( + coordinate: $coordinate, + collapsed: $collapsed, + viewHeight: $viewHeight + ) + RefreshProgressView(isShowRefresh: $viewModel.isShowRefresh) + VStack(alignment: .leading) { - // MARK: - ContinueWith button - ContinueWithView( - data: continueWith, - courseContinueUnit: continueUnit - ) { - var continueBlock: CourseBlock? - continueUnit.childs.forEach { block in - if block.id == continueWith.lastVisitedBlockId { - continueBlock = block + if isVideo, + viewModel.isShowProgress == false { + downloadQualityBars(proxy: proxy) + } + certificateView + + if viewModel.courseStructure == nil, + viewModel.isShowProgress == false, + !isVideo { + FullScreenErrorView( + type: .noContent( + CourseLocalization.Error.coursewareUnavailable, + image: CoreAssets.information.swiftUIImage + ) + ) + .frame(maxWidth: .infinity) + .frame(height: proxy.size.height - viewHeight) + } else { + if let continueWith = viewModel.continueWith, + let courseStructure = viewModel.courseStructure, + !isVideo { + let chapter = courseStructure.childs[continueWith.chapterIndex] + let sequential = chapter.childs[continueWith.sequentialIndex] + let continueUnit = sequential.childs[continueWith.verticalIndex] + + ContinueWithView( + data: continueWith, + courseContinueUnit: continueUnit + ) { + viewModel.openLastVisitedBlock() } } - viewModel.trackResumeCourseClicked( - blockId: continueBlock?.id ?? "" - ) - - if let course = viewModel.courseStructure { - viewModel.router.showCourseUnit( - courseName: course.displayName, - blockId: continueBlock?.id ?? "", - courseID: course.id, - verticalIndex: continueWith.verticalIndex, - chapters: course.childs, - chapterIndex: continueWith.chapterIndex, - sequentialIndex: continueWith.sequentialIndex + if let course = isVideo + ? viewModel.courseVideosStructure + : viewModel.courseStructure { + + if !isVideo, + let progress = course.courseProgress, + progress.totalAssignmentsCount != 0 { + CourseProgressView(progress: progress) + .padding(.horizontal, 24) + .padding(.top, 16) + .padding(.bottom, 8) + } + + // MARK: - Sections + CustomDisclosureGroup( + isVideo: isVideo, + course: course, + proxy: proxy, + viewModel: viewModel ) + } else { + if let courseStart = viewModel.courseStart { + Text(courseStart > Date() ? CourseLocalization.Outline.courseHasntStarted : "") + .frame(maxWidth: .infinity) + .frame(maxHeight: .infinity) + .padding(.top, 100) + } + Spacer(minLength: viewHeight < 200 ? 200 : viewHeight) } } } - - if let course = isVideo - ? viewModel.courseVideosStructure - : viewModel.courseStructure { - - // MARK: - Sections - if viewModel.config.uiComponents.courseNestedListEnabled { - CourseStructureNestedListView( - proxy: proxy, - course: course, - viewModel: viewModel - ) - } else { - CourseStructureView( - proxy: proxy, - course: course, - viewModel: viewModel - ) - } - - } else { - if let courseStart = viewModel.courseStart { - Text(courseStart > Date() ? CourseLocalization.Outline.courseHasntStarted : "") - .frame(maxWidth: .infinity) - .padding(.top, 100) - } - } - Spacer(minLength: 200) + .frameLimit(width: proxy.size.width) } - .frameLimit(width: proxy.size.width) + } + .refreshable { + Task { + await viewModel.getCourseBlocks(courseID: courseID, withProgress: false) + } } .onRightSwipeGesture { viewModel.router.back() @@ -161,17 +148,6 @@ public struct CourseOutlineView: View { } .accessibilityAction {} - if viewModel.dueDatesShifted && !isVideo { - DatesSuccessView( - title: CourseLocalization.CourseDates.toastSuccessTitle, - message: CourseLocalization.CourseDates.toastSuccessMessage, - selectedTab: .course, - courseContainerViewModel: viewModel - ) { - selection = dateTabIndex - } - } - // MARK: - Offline mode SnackBar OfflineSnackBarView( connectivity: viewModel.connectivity, @@ -211,19 +187,26 @@ public struct CourseOutlineView: View { } } } + .onAppear { + Task { + await viewModel.updateCourseIfNeeded(courseID: courseID) + } + } .background( Theme.Colors.background .ignoresSafeArea() ) .sheet(isPresented: $showingDownloads) { - DownloadsView(manager: viewModel.manager) + DownloadsView(router: viewModel.router, manager: viewModel.manager) } .sheet(isPresented: $showingVideoDownloadQuality) { viewModel.storage.userSettings.map { VideoDownloadQualityContainerView( downloadQuality: $0.downloadQuality, didSelect: viewModel.update(downloadQuality:), - analytics: viewModel.coreAnalytics + analytics: viewModel.coreAnalytics, + router: viewModel.router, + isModal: true ) } } @@ -249,9 +232,8 @@ public struct CourseOutlineView: View { } @ViewBuilder - private var downloadQualityBars: some View { - if isVideo, - let courseVideosStructure = viewModel.courseVideosStructure, + private func downloadQualityBars(proxy: GeometryProxy) -> some View { + if let courseVideosStructure = viewModel.courseVideosStructure, viewModel.hasVideoForDowbloads() { VStack(spacing: 0) { CourseVideoDownloadBarView( @@ -277,6 +259,16 @@ public struct CourseOutlineView: View { } } } + } else { + FullScreenErrorView( + type: .noContent( + CourseLocalization.Error.videosUnavailable, + image: CoreAssets.noVideos.swiftUIImage + ) + ) + .frame(maxWidth: .infinity) + .frame(height: proxy.size.height - viewHeight) + Spacer(minLength: -200) } } @ViewBuilder @@ -299,7 +291,8 @@ public struct CourseOutlineView: View { content: { WebBrowser( url: url, - pageTitle: CourseLocalization.Outline.certificate + pageTitle: CourseLocalization.Outline.certificate, + connectivity: viewModel.connectivity ) } ) @@ -344,6 +337,7 @@ struct CourseOutlineView_Previews: PreviewProvider { courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) Task { @@ -363,9 +357,10 @@ struct CourseOutlineView_Previews: PreviewProvider { title: "Course title", courseID: "", isVideo: false, - selection: $selection, + selection: $selection, coordinate: .constant(0), collapsed: .constant(false), + viewHeight: .constant(0), dateTabIndex: 2 ) .preferredColorScheme(.light) @@ -379,6 +374,7 @@ struct CourseOutlineView_Previews: PreviewProvider { selection: $selection, coordinate: .constant(0), collapsed: .constant(false), + viewHeight: .constant(0), dateTabIndex: 2 ) .preferredColorScheme(.dark) diff --git a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift deleted file mode 100644 index 6e8ad3927..000000000 --- a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureNestedListView.swift +++ /dev/null @@ -1,237 +0,0 @@ -// -// CourseStructureNestedListView.swift -// Course -// -// Created by Eugene Yatsenko on 09.11.2023. -// - -import SwiftUI -import Core -import Kingfisher -import Theme - -struct CourseStructureNestedListView: View { - - private let proxy: GeometryProxy - private let course: CourseStructure - private let viewModel: CourseContainerViewModel - private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - - @State private var isExpandedIds: [String] = [] - - init(proxy: GeometryProxy, course: CourseStructure, viewModel: CourseContainerViewModel) { - self.proxy = proxy - self.course = course - self.viewModel = viewModel - } - - var body: some View { - ForEach(course.childs, content: disclosureGroup) - } - - private func disclosureGroup(chapter: CourseChapter) -> some View { - CustomDisclosureGroup( - animation: .easeInOut(duration: 0.2), - isExpanded: .constant(isExpandedIds.contains(where: { $0 == chapter.id })), - onClick: { onHeaderClick(chapter: chapter) }, - header: { isExpanded in header(chapter: chapter, isExpanded: isExpanded) }, - content: { section(chapter: chapter) } - ) - } - - private func header( - chapter: CourseChapter, - isExpanded: Bool - ) -> some View { - HStack { - Text(chapter.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .lineLimit(1) - .foregroundColor(Theme.Colors.textPrimary) - Spacer() - Image(systemName: "chevron.down").renderingMode(.template) - .foregroundColor(Theme.Colors.accentXColor) - .dropdownArrowRotationAnimation(value: isExpanded) - } - .padding(.horizontal, 30) - .padding(.vertical, 15) - } - - private func section(chapter: CourseChapter) -> some View { - ForEach(chapter.childs) { sequential in - VStack(spacing: 0) { - sequentialLabel( - sequential: sequential, - chapter: chapter, - isExpanded: false - ) - } - } - } - - @ViewBuilder - private func sequentialLabel( - sequential: CourseSequential, - chapter: CourseChapter, - isExpanded: Bool - ) -> some View { - HStack { - Button { - onLabelClick(sequential: sequential, chapter: chapter) - } label: { - HStack(spacing: 0) { - Group { - if sequential.completion == 1 { - CoreAssets.finished.swiftUIImage - .renderingMode(.template) - .foregroundColor(Theme.Colors.accentXColor) - } else { - sequential.type.image - } - Text(sequential.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .lineLimit(1) - } - .foregroundColor(Theme.Colors.textPrimary) - Spacer() - } - } - downloadButton( - sequential: sequential, - chapter: chapter - ) - - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(sequential.displayName) - .padding(.leading, 40) - .padding(.trailing, 28) - .padding(.vertical, 14) - } - - @ViewBuilder - private func downloadButton( - sequential: CourseSequential, - chapter: CourseChapter - ) -> some View { - if let state = viewModel.sequentialsDownloadState[sequential.id] { - switch state { - case .available: - if viewModel.isInternetAvaliable { - Button { - Task { - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: sequential.id, - state: state - ) - } - } label: { - DownloadAvailableView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.download) - } - } - case .downloading: - if viewModel.isInternetAvaliable { - Button { - viewModel.router.showDownloads( - downloads: viewModel.getTasks(sequential: sequential), - manager: viewModel.manager - ) - } label: { - ProgressBar(size: 30, lineWidth: 1.75) - } - } - case .finished: - Button { - viewModel.router.presentAlert( - alertTitle: "Warning", - alertMessage: "\(CourseLocalization.Alert.deleteVideos) \"\(sequential.displayName)\"?", - positiveAction: CoreLocalization.Alert.delete, - onCloseTapped: { - viewModel.router.dismiss(animated: true) - }, - okTapped: { - Task { - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: sequential.id, - state: state - ) - } - viewModel.router.dismiss(animated: true) - }, - type: .deleteVideo - ) - } label: { - DownloadFinishedView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.deleteDownload) - } - } - downloadCount(sequential: sequential) - } - } - - @ViewBuilder - private func downloadCount(sequential: CourseSequential) -> some View { - let downloadable = viewModel.verticalsBlocksDownloadable(by: sequential) - if !downloadable.isEmpty { - Text(String(downloadable.count)) - .foregroundColor(Color(UIColor.label)) - } - } - - private func onHeaderClick(chapter: CourseChapter) { - if let index = isExpandedIds.firstIndex(where: {$0 == chapter.id}) { - isExpandedIds.remove(at: index) - } else { - isExpandedIds.append(chapter.id) - } - } - - private func onLabelClick( - sequential: CourseSequential, - chapter: CourseChapter - ) { - guard let chapterIndex = course.childs.firstIndex( - where: { $0.id == chapter.id } - ) else { - return - } - - guard let sequentialIndex = chapter.childs.firstIndex( - where: { $0.id == sequential.id } - ) else { - return - } - - guard let courseVertical = sequential.childs.first else { - return - } - - guard let block = courseVertical.childs.first else { - return - } - - viewModel.trackVerticalClicked( - courseId: viewModel.courseStructure?.id ?? "", - courseName: viewModel.courseStructure?.displayName ?? "", - vertical: courseVertical - ) - viewModel.router.showCourseUnit( - courseName: viewModel.courseStructure?.displayName ?? "", - blockId: block.id, - courseID: viewModel.courseStructure?.id ?? "", - verticalIndex: 0, - chapters: course.childs, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) - - } - -} diff --git a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift index e2dd8646d..e69de29bb 100644 --- a/Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift +++ b/Course/Course/Presentation/Outline/CourseStructure/CourseStructureView.swift @@ -1,134 +0,0 @@ -// -// CourseStructureView.swift -// Course -// -// Created by Eugene Yatsenko on 15.12.2023. -// - -import SwiftUI -import Core -import Theme - -struct CourseStructureView: View { - - private let proxy: GeometryProxy - private let course: CourseStructure - private let viewModel: CourseContainerViewModel - private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - - init(proxy: GeometryProxy, course: CourseStructure, viewModel: CourseContainerViewModel) { - self.proxy = proxy - self.course = course - self.viewModel = viewModel - } - - var body: some View { - let chapters = course.childs - ForEach(chapters, id: \.id) { chapter in - let chapterIndex = chapters.firstIndex(where: { $0.id == chapter.id }) - Text(chapter.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .foregroundColor(Theme.Colors.textSecondary) - .padding(.horizontal, 24) - .padding(.top, 40) - ForEach(chapter.childs, id: \.id) { child in - let sequentialIndex = chapter.childs.firstIndex(where: { $0.id == child.id }) - VStack(alignment: .leading) { - HStack { - Button { - if let chapterIndex, let sequentialIndex { - viewModel.trackSequentialClicked(child) - viewModel.router.showCourseVerticalView( - courseID: viewModel.courseStructure?.id ?? "", - courseName: viewModel.courseStructure?.displayName ?? "", - title: child.displayName, - chapters: chapters, - chapterIndex: chapterIndex, - sequentialIndex: sequentialIndex - ) - } - } label: { - Group { - if child.completion == 1 { - CoreAssets.finished.swiftUIImage - .renderingMode(.template) - .foregroundColor(Theme.Colors.accentXColor) - } else { - child.type.image - } - Text(child.displayName) - .font(Theme.Fonts.titleMedium) - .multilineTextAlignment(.leading) - .lineLimit(1) - .frame( - maxWidth: idiom == .pad - ? proxy.size.width * 0.5 - : proxy.size.width * 0.6, - alignment: .leading - ) - } - .foregroundColor(Theme.Colors.textPrimary) - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(child.displayName) - Spacer() - if let state = viewModel.sequentialsDownloadState[child.id] { - switch state { - case .available: - DownloadAvailableView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.download) - .onTapGesture { - Task { - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: child.id, - state: state - ) - } - } - case .downloading: - DownloadProgressView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.cancelDownload) - .onTapGesture { - Task { - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: child.id, - state: state - ) - } - } - case .finished: - DownloadFinishedView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.deleteDownload) - .onTapGesture { - Task { - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: child.id, - state: state - ) - } - } - } - } - Image(systemName: "chevron.right") - .foregroundColor(Theme.Colors.accentColor) - } - .padding(.horizontal, 36) - .padding(.vertical, 20) - if chapterIndex != chapters.count - 1 { - Divider() - .frame(height: 1) - .overlay(Theme.Colors.cardViewStroke) - .padding(.horizontal, 24) - } - } - } - } - } -} diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift index 5d33fbd69..523c04e0a 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalImageView.swift @@ -34,14 +34,16 @@ struct CourseVerticalImageView_Previews: PreviewProvider { id: "1", courseId: "123", topicId: "1", - graded: false, + graded: false, + due: Date(), completion: 1, type: .video, displayName: "Block 1", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ) ] @@ -52,13 +54,15 @@ struct CourseVerticalImageView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 1, type: .problem, displayName: "Block 1", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ) ] let blocks3 = [ @@ -68,13 +72,15 @@ struct CourseVerticalImageView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 1, type: .discussion, displayName: "Block 1", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ) ] let blocks4 = [ @@ -84,13 +90,15 @@ struct CourseVerticalImageView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 1, type: .html, displayName: "Block 1", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ) ] let blocks5 = [ @@ -100,13 +108,15 @@ struct CourseVerticalImageView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 1, type: .unknown, displayName: "Block 1", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ) ] HStack { diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift index d16095762..cdbf81318 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalView.swift @@ -6,8 +6,8 @@ // import SwiftUI - import Core +import OEXFoundation import Kingfisher import Theme @@ -82,49 +82,8 @@ public struct CourseVerticalView: View { }).accessibilityElement(children: .ignore) .accessibilityLabel(vertical.displayName) Spacer() - if let state = viewModel.downloadState[vertical.id] { - switch state { - case .available: - DownloadAvailableView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.download) - .onTapGesture { - Task { - await viewModel.onDownloadViewTap( - blockId: vertical.id, - state: state - ) - } - - } - case .downloading: - DownloadProgressView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.cancelDownload) - .onTapGesture { - Task { - await viewModel.onDownloadViewTap( - blockId: vertical.id, - state: state - ) - } - - } - case .finished: - DownloadFinishedView() - .accessibilityElement(children: .ignore) - .accessibilityLabel(CourseLocalization.Accessibility.deleteDownload) - .onTapGesture { - Task { - await viewModel.onDownloadViewTap( - blockId: vertical.id, - state: state - ) - } - } - } - } Image(systemName: "chevron.right") + .flipsForRightToLeftLayoutDirection(true) .padding(.vertical, 8) } .padding(.horizontal, 36) @@ -202,8 +161,17 @@ struct CourseVerticalView_Previews: PreviewProvider { displayName: "Vertical", type: .vertical, completion: 0, - childs: []) - ]) + childs: [], + webUrl: "" + ) + ], + sequentialProgress: SequentialProgress( + assignmentType: "Advanced Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() + ) ]) ] diff --git a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift index 4ce5ebd70..e15cc024d 100644 --- a/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift +++ b/Course/Course/Presentation/Outline/CourseVertical/CourseVerticalViewModel.swift @@ -67,7 +67,7 @@ public class CourseVerticalViewModel: BaseCourseViewModel { do { switch state { case .available: - try manager.addToDownloadQueue(blocks: blocks) + try await manager.addToDownloadQueue(blocks: blocks) downloadState[vertical.id] = .downloading case .downloading: try await manager.cancelDownloading(courseId: vertical.courseId, blocks: blocks) @@ -83,19 +83,6 @@ public class CourseVerticalViewModel: BaseCourseViewModel { } } } - - func trackVerticalClicked( - courseId: String, - courseName: String, - vertical: CourseVertical - ) { - analytics.verticalClicked( - courseId: courseId, - courseName: courseName, - blockId: vertical.blockId, - blockName: vertical.displayName - ) - } @MainActor private func setDownloadsStates() async { @@ -126,4 +113,17 @@ public class CourseVerticalViewModel: BaseCourseViewModel { } downloadState = states } + + func trackVerticalClicked( + courseId: String, + courseName: String, + vertical: CourseVertical + ) { + analytics.verticalClicked( + courseId: courseId, + courseName: courseName, + blockId: vertical.blockId, + blockName: vertical.displayName + ) + } } diff --git a/Course/Course/Presentation/Subviews/ActionViews/DeviceStorageFullAlertView.swift b/Course/Course/Presentation/Subviews/ActionViews/DeviceStorageFullAlertView.swift new file mode 100644 index 000000000..4e440283c --- /dev/null +++ b/Course/Course/Presentation/Subviews/ActionViews/DeviceStorageFullAlertView.swift @@ -0,0 +1,235 @@ +// +// DeviceStorageFullAlertView.swift +// Course +// +// Created by  Stepanok Ivan on 13.06.2024. +// + +import SwiftUI +import Core +import Theme + +public struct DeviceStorageFullAlertView: View { + private let sequentials: [CourseSequential] + private let usedSpace: Int + private let freeSpace: Int + private let close: () -> Void + @State private var fadeEffect: Bool = false + + @Environment(\.isHorizontal) private var isHorizontal + + init( + sequentials: [CourseSequential], + usedSpace: Int, + freeSpace: Int, + close: @escaping () -> Void + ) { + self.sequentials = sequentials + self.usedSpace = usedSpace + self.freeSpace = freeSpace + self.close = close + } + + public var body: some View { + ZStack(alignment: .bottom) { + Color.black.opacity(fadeEffect ? 0.15 : 0) + .onTapGesture { + close() + fadeEffect = false + } + content + .padding(.bottom, 20) + } + .ignoresSafeArea() + .onAppear { + withAnimation(Animation.linear(duration: 0.3).delay(0.2)) { + fadeEffect = true + } + } + } + + private var content: some View { + VStack { + HStack { + CoreAssets.reportOctagon.swiftUIImage + .scaledToFit() + .foregroundStyle(Theme.Colors.alert) + Text(CourseLocalization.Course.StorageAlert.title) + .font(Theme.Fonts.titleLarge) + } + .padding(.top, 16) + .padding(.horizontal, 16) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + if sequentials.count <= 3 { + list + } else { + ScrollView { + list + } + .frame(maxHeight: isHorizontal ? 80 : 200) + } + + VStack(spacing: 4) { + StorageProgressBar( + usedSpace: usedSpace, + contentSize: totalSize + ) + .padding(.horizontal, 16) + .padding(.top, 8) + + HStack { + Text( + CourseLocalization.Course.StorageAlert.usedAndFree( + usedSpace.formattedFileSize(), + freeSpace.formattedFileSize() + ) + ) + .foregroundColor(Theme.Colors.textSecondaryLight) + .font(Theme.Fonts.bodySmall) + Spacer() + Text(totalSize.formattedFileSize()) + .foregroundColor(Theme.Colors.alert) + .font(Theme.Fonts.bodySmall) + } + .padding(.horizontal, 16) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + + Text(CourseLocalization.Course.StorageAlert.description) + .font(.subheadline) + .padding(.horizontal, 16) + .padding(.top, 8) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + VStack(spacing: 16) { + Button(action: { + fadeEffect = false + close() + }) { + Text(CourseLocalization.Course.Alert.close) + .font(Theme.Fonts.bodyMedium) + .foregroundStyle(Theme.Colors.accentColor) + .frame(maxWidth: .infinity) + .frame(height: 42) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.accentColor, lineWidth: 2) + ) + .background(Theme.Colors.background) + .cornerRadius(8) + + } + } + .padding(16) + } + .background(Theme.Colors.background) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.datesSectionStroke, lineWidth: 2) + ) + .cornerRadius(8) + .padding(16) + .frame(maxWidth: 400) + } + + @ViewBuilder + var list: some View { + VStack(spacing: 8) { + ForEach(sequentials) { sequential in + HStack { + sequential.type.image + .renderingMode(.template) + .foregroundStyle(Theme.Colors.textPrimary) + Text(sequential.displayName) + .font(Theme.Fonts.bodyMedium) + .lineLimit(1) + if sequential.totalSize != 0 { + Text(sequential.totalSize.formattedFileSize()) + .foregroundColor(Theme.Colors.textSecondaryLight) + .font(Theme.Fonts.bodySmall) + } + } + .padding(.horizontal, 16) + .padding(.top, 8) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + } + } + + private var totalSize: Int { + sequentials.reduce(0) { $0 + $1.totalSize } + } +} + +struct StorageProgressBar: View { + let usedSpace: Int + let contentSize: Int + + var body: some View { + GeometryReader { geometry in + let totalSpace = geometry.size.width + let usedSpace = Double(usedSpace) + let contentSize = Double(contentSize) + let total = usedSpace + contentSize + + let minSize: Double = 0.1 + let usedSpacePercentage = (usedSpace / total) + minSize + let contentSizePercentage = (contentSize / total) + minSize + let normalizationFactor = 1 / (usedSpacePercentage + contentSizePercentage) + + let normalizedUsedSpaceWidth = usedSpacePercentage * normalizationFactor + + ZStack { + RoundedRectangle(cornerRadius: 3) + .fill(Theme.Colors.datesSectionStroke) + .frame(width: totalSpace, height: 42) + + RoundedRectangle(cornerRadius: 2) + .fill(Theme.Colors.background) + .frame(width: totalSpace - 4, height: 38) + + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(Theme.Colors.alert) + .frame(width: totalSpace - 6, height: 36) + + HStack(spacing: 0) { + RoundedCorners(tl: 2, bl: 2) + .fill(Theme.Colors.datesSectionStroke) + .frame(width: (totalSpace - 6) * normalizedUsedSpaceWidth, height: 36) + Rectangle() + .fill(Theme.Colors.background) + .frame(width: 1, height: 36) + } + } + } + } + .frame(height: 44) + } +} + +#if DEBUG +struct DeviceStorageFullAlertView_Previews: PreviewProvider { + static var previews: some View { + DeviceStorageFullAlertView( + sequentials: [ + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ) + ], + usedSpace: 460580220928, + freeSpace: 33972756480, + close: { print("Close action triggered") } + ) + .loadFonts() + } +} +#endif diff --git a/Course/Course/Presentation/Subviews/ActionViews/DownloadActionView.swift b/Course/Course/Presentation/Subviews/ActionViews/DownloadActionView.swift new file mode 100644 index 000000000..1dbbce63c --- /dev/null +++ b/Course/Course/Presentation/Subviews/ActionViews/DownloadActionView.swift @@ -0,0 +1,326 @@ +// +// DownloadActionView.swift +// Course +// +// Created by  Stepanok Ivan on 12.06.2024. +// + +import SwiftUI +import Core +import Theme + +enum ContentActionType { + case remove + case confirmDownload + case confirmDownloadCellular +} + +struct Lesson: Identifiable { + let id = UUID() + let name: String + let size: Int + let image: Image +} + +public struct DownloadActionView: View { + private let actionType: ContentActionType + private let sequentials: [CourseSequential] + private let courseBlocks: [CourseBlock] + private let courseName: String? + private let action: () -> Void + private let cancel: () -> Void + @State private var fadeEffect: Bool = false + + @Environment(\.isHorizontal) private var isHorizontal + + init( + actionType: ContentActionType, + sequentials: [CourseSequential], + action: @escaping () -> Void, + cancel: @escaping () -> Void + ) { + self.actionType = actionType + self.sequentials = sequentials + self.courseName = nil + self.courseBlocks = [] + self.action = action + self.cancel = cancel + } + + init( + actionType: ContentActionType, + courseBlocks: [CourseBlock], + courseName: String? = nil, + action: @escaping () -> Void, + cancel: @escaping () -> Void + ) { + self.actionType = actionType + self.sequentials = [] + self.courseBlocks = courseBlocks + self.courseName = courseName + self.action = action + self.cancel = cancel + } + + public var body: some View { + ZStack(alignment: .bottom) { + Color.black.opacity(fadeEffect ? 0.15 : 0) + .onTapGesture { + cancel() + fadeEffect = false + } + content + .padding(.bottom, 20) + } + .ignoresSafeArea() + .onAppear { + withAnimation(Animation.linear(duration: 0.3).delay(0.2)) { + fadeEffect = true + } + } + } + + private var content: some View { + VStack { + HStack { + if actionType == .confirmDownloadCellular { + CoreAssets.warningFilled.swiftUIImage + .resizable() + .scaledToFit() + .frame(width: 22) + } + Text(headerTitle) + .font(Theme.Fonts.titleLarge) + } + .padding(.top, 16) + .padding(.horizontal, 16) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + if let courseName { + HStack { + Image(systemName: "doc.text") + .renderingMode(.template) + .foregroundStyle(Theme.Colors.textPrimary) + Text(courseName) + .font(Theme.Fonts.bodyMedium) + .lineLimit(1) + } + .padding(.horizontal, 16) + .padding(.top, 8) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } else { + if sequentials.count <= 3 { + list + } else { + ScrollView { + list + } + .frame(maxHeight: isHorizontal ? 80 : 200) + } + } + + Text(descriptionText) + .font(.subheadline) + .padding(.horizontal, 16) + .padding(.top, 8) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + VStack(spacing: 16) { + Button(action: { + fadeEffect = false + action() + }) { + HStack { + actionButtonImage + .renderingMode(.template) + Text(actionButtonText) + .font(Theme.Fonts.bodyMedium) + } + .foregroundStyle(Theme.Colors.white) + .frame(maxWidth: .infinity) + .frame(height: 42) + .background(actionButtonColor) + .cornerRadius(8) + } + + Button(action: { + fadeEffect = false + cancel() + }) { + Text(CourseLocalization.Course.Alert.cancel) + .font(Theme.Fonts.bodyMedium) + .foregroundStyle(Theme.Colors.accentColor) + .frame(maxWidth: .infinity) + .frame(height: 42) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.accentColor, lineWidth: 2) + ) + .background(Theme.Colors.background) + .cornerRadius(8) + + } + } + .padding([.leading, .trailing, .bottom], 16) + } + .background(Theme.Colors.background) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.datesSectionStroke, lineWidth: 2) + ) + .cornerRadius(8) + .padding(16) + .frame(maxWidth: 400) + } + + @ViewBuilder + var list: some View { + VStack(spacing: 8) { + if sequentials.isEmpty { + ForEach(Array(courseBlocks.enumerated()), id: \.offset) { _, block in + HStack { + block.type.image + .renderingMode(.template) + .foregroundStyle(Theme.Colors.textPrimary) + Text(block.displayName) + .font(Theme.Fonts.bodyMedium) + .lineLimit(1) + if let fileSize = block.fileSize, fileSize != 0 { + Text(fileSize.formattedFileSize()) + .foregroundColor(Theme.Colors.textSecondaryLight) + .font(Theme.Fonts.bodySmall) + } + } + .padding(.horizontal, 16) + .padding(.top, 8) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + } else { + ForEach(sequentials) { sequential in + HStack { + sequential.type.image + .renderingMode(.template) + .foregroundStyle(Theme.Colors.textPrimary) + Text(sequential.displayName) + .font(Theme.Fonts.bodyMedium) + .lineLimit(1) + if sequential.totalSize != 0 { + Text(sequential.totalSize.formattedFileSize()) + .foregroundColor(Theme.Colors.textSecondaryLight) + .font(Theme.Fonts.bodySmall) + } + } + .padding(.horizontal, 16) + .padding(.top, 8) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + } + } + } + + private var headerTitle: String { + switch actionType { + case .remove: + return CourseLocalization.Course.Alert.removeTitle + case .confirmDownload: + return CourseLocalization.Course.Alert.confirmDownloadTitle + case .confirmDownloadCellular: + return CourseLocalization.Course.Alert.confirmDownloadCellularTitle + } + } + + private var descriptionText: String { + switch actionType { + case .remove: + return CourseLocalization.Course.Alert.removeDescription(totalSize) + case .confirmDownload: + return CourseLocalization.Course.Alert.confirmDownloadDescription(totalSize) + case .confirmDownloadCellular: + return CourseLocalization.Course.Alert.confirmDownloadCellularDescription(totalSize) + } + } + + private var actionButtonText: String { + switch actionType { + case .remove: + return CourseLocalization.Course.Alert.remove + case .confirmDownload, .confirmDownloadCellular: + return CourseLocalization.Course.Alert.download + } + } + + private var actionButtonImage: Image { + switch actionType { + case .remove: + return CoreAssets.remove.swiftUIImage + case .confirmDownload, .confirmDownloadCellular: + return CoreAssets.startDownloading.swiftUIImage + } + } + + private var actionButtonColor: Color { + switch actionType { + case .remove: + Theme.Colors.snackbarErrorColor + case .confirmDownloadCellular, .confirmDownload: + Theme.Colors.accentColor + + } + } + + private var totalSize: String { + if sequentials.isEmpty { + courseBlocks.reduce(0) { $0 + ($1.fileSize ?? 0) }.formattedFileSize() + } else { + sequentials.reduce(0) { $0 + $1.totalSize }.formattedFileSize() + } + } +} + +#if DEBUG +struct ContentActionView_Previews: PreviewProvider { + static var previews: some View { + ScrollView { + DownloadActionView( + actionType: .remove, + sequentials: [ + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ), + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ), + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ) + ], + action: { + print("Action triggered") + }, + cancel: { print("Cancel triggered") } + ) + }.loadFonts() + } +} +#endif diff --git a/Course/Course/Presentation/Subviews/ActionViews/DownloadErrorAlertView.swift b/Course/Course/Presentation/Subviews/ActionViews/DownloadErrorAlertView.swift new file mode 100644 index 000000000..739d5267a --- /dev/null +++ b/Course/Course/Presentation/Subviews/ActionViews/DownloadErrorAlertView.swift @@ -0,0 +1,298 @@ +// +// DownloadErrorAlertView.swift +// Course +// +// Created by  Stepanok Ivan on 13.06.2024. +// + +import SwiftUI +import Core +import Theme + +public enum ContentErrorType { + case downloadFailed + case noInternetConnection + case wifiRequired +} + +public struct DownloadErrorAlertView: View { + + private let errorType: ContentErrorType + private let sequentials: [CourseSequential] + private let tryAgain: () -> Void + private let close: () -> Void + @State private var fadeEffect: Bool = false + + @Environment(\.isHorizontal) private var isHorizontal + + public init( + errorType: ContentErrorType, + sequentials: [CourseSequential], + tryAgain: @escaping () -> Void = {}, + close: @escaping () -> Void + ) { + self.errorType = errorType + self.sequentials = sequentials + self.tryAgain = tryAgain + self.close = close + } + + public var body: some View { + ZStack(alignment: .bottom) { + Color.black.opacity(fadeEffect ? 0.15 : 0) + .onTapGesture { + close() + fadeEffect = false + } + content + .padding(.bottom, 20) + } + .ignoresSafeArea() + .onAppear { + withAnimation(Animation.linear(duration: 0.3).delay(0.2)) { + fadeEffect = true + } + } + } + + private var content: some View { + VStack { + HStack { + CoreAssets.reportOctagon.swiftUIImage + .scaledToFit() + .foregroundStyle(Theme.Colors.alert) + Text(headerTitle) + .font(Theme.Fonts.titleLarge) + } + .padding(.top, 16) + .padding(.horizontal, 16) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + if sequentials.count <= 3 { + list + } else { + ScrollView { + list + } + .frame(maxHeight: isHorizontal ? 80 : 200) + } + + Text(descriptionText) + .font(.subheadline) + .padding(.horizontal, 16) + .padding(.top, 8) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + + VStack(spacing: 16) { + + if errorType == .downloadFailed { + Button(action: { + fadeEffect = false + tryAgain() + }) { + Text(CourseLocalization.Course.Alert.tryAgain) + .font(Theme.Fonts.bodyMedium) + .foregroundStyle(Theme.Colors.white) + .frame(maxWidth: .infinity) + .frame(height: 42) + .background(Theme.Colors.accentColor) + .cornerRadius(8) + + } + } + + Button(action: { + fadeEffect = false + close() + }) { + Text(CourseLocalization.Course.Alert.close) + .font(Theme.Fonts.bodyMedium) + .foregroundStyle(Theme.Colors.accentColor) + .frame(maxWidth: .infinity) + .frame(height: 42) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.accentColor, lineWidth: 2) + ) + .background(Theme.Colors.background) + .cornerRadius(8) + + } + } + .padding([.leading, .trailing, .bottom], 16) + } + .background(Theme.Colors.background) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.datesSectionStroke, lineWidth: 2) + ) + .cornerRadius(8) + .padding(16) + .frame(maxWidth: 400) + } + + @ViewBuilder + var list: some View { + VStack(spacing: 8) { + ForEach(sequentials) { sequential in + HStack { + sequential.type.image + .renderingMode(.template) + .foregroundStyle(Theme.Colors.textPrimary) + Text(sequential.displayName) + .font(Theme.Fonts.bodyMedium) + .lineLimit(1) + if sequential.totalSize != 0 { + Text(sequential.totalSize.formattedFileSize()) + .foregroundColor(Theme.Colors.textSecondaryLight) + .font(Theme.Fonts.bodySmall) + } + } + .padding(.horizontal, 16) + .padding(.top, 8) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + } + } + } + + private var headerTitle: String { + switch errorType { + case .downloadFailed: + return CourseLocalization.Course.Error.downloadFailedTitle + case .noInternetConnection: + return CourseLocalization.Course.Error.noInternetConnectionTitle + case .wifiRequired: + return CourseLocalization.Course.Error.wifiRequiredTitle + } + } + + private var descriptionText: String { + switch errorType { + case .downloadFailed: + return CourseLocalization.Course.Error.downloadFailedDescription + case .noInternetConnection: + return CourseLocalization.Course.Error.noInternetConnectionDescription + case .wifiRequired: + return CourseLocalization.Course.Error.wifiRequiredDescription + } + } +} + +#if DEBUG +struct DownloadErrorAlertView_Previews: PreviewProvider { + static var previews: some View { + ScrollView { + DownloadErrorAlertView( + errorType: .downloadFailed, + sequentials: [ + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ) + ], + close: { print("Cancel triggered") } + ) + + DownloadErrorAlertView( + errorType: .noInternetConnection, + sequentials: [ + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ), + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ), + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ), + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ), + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ), + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ), + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ) + ], + close: { print("Cancel triggered") } + ) + + DownloadErrorAlertView( + errorType: .wifiRequired, + sequentials: [ + CourseSequential( + blockId: "", + id: "", + displayName: "Course intro", + type: .chapter, + completion: 0, + childs: [], + sequentialProgress: nil, + due: nil + ) + ], + close: { print("Cancel triggered") } + ) + }.loadFonts() + } +} +#endif diff --git a/Course/Course/Views/CalendarSyncProgressView.swift b/Course/Course/Presentation/Subviews/CalendarSyncProgressView/CalendarSyncProgressView.swift similarity index 100% rename from Course/Course/Views/CalendarSyncProgressView.swift rename to Course/Course/Presentation/Subviews/CalendarSyncProgressView/CalendarSyncProgressView.swift diff --git a/Course/Course/Presentation/Subviews/CalendarSyncStatusView.swift b/Course/Course/Presentation/Subviews/CalendarSyncStatusView.swift new file mode 100644 index 000000000..c949a9d13 --- /dev/null +++ b/Course/Course/Presentation/Subviews/CalendarSyncStatusView.swift @@ -0,0 +1,71 @@ +// +// CalendarSyncStatusView.swift +// Course +// +// Created by  Stepanok Ivan on 12.06.2024. +// + +import SwiftUI +import Core +import Theme + +struct CalendarSyncStatusView: View { + + var status: SyncStatus + let router: CourseRouter + + var body: some View { + HStack { + icon + Text(statusText) + .font(Theme.Fonts.titleSmall) + Spacer() + } + .frame(height: 40) + .padding(.horizontal, 16) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.datesSectionStroke, lineWidth: 2) + ) + .background(Theme.Colors.datesSectionBackground) + .onTapGesture { + router.showDatesAndCalendar() + } + } + + private var icon: Image { + switch status { + case .synced: + return CoreAssets.synced.swiftUIImage + case .failed: + return CoreAssets.syncFailed.swiftUIImage + case .offline: + return CoreAssets.syncOffline.swiftUIImage + } + } + + private var statusText: String { + switch status { + case .synced: + return CourseLocalization.CalendarSyncStatus.synced + case .failed: + return CourseLocalization.CalendarSyncStatus.failed + case .offline: + return CourseLocalization.CalendarSyncStatus.offline + } + } +} + +#if DEBUG +struct CalendarSyncStatusView_Previews: PreviewProvider { + static var previews: some View { + VStack { + CalendarSyncStatusView(status: .synced, router: CourseRouterMock()) + CalendarSyncStatusView(status: .failed, router: CourseRouterMock()) + CalendarSyncStatusView(status: .offline, router: CourseRouterMock()) + } + .loadFonts() + .padding() + } +} +#endif diff --git a/Course/Course/Presentation/Subviews/CourseHeaderView.swift b/Course/Course/Presentation/Subviews/CourseHeaderView.swift index 26e8ad38e..3f604e7df 100644 --- a/Course/Course/Presentation/Subviews/CourseHeaderView.swift +++ b/Course/Course/Presentation/Subviews/CourseHeaderView.swift @@ -24,6 +24,7 @@ struct CourseHeaderView: View { private let collapsedVerticalHeight: CGFloat = 260 private let expandedHeight: CGFloat = 300 + private let courseRawImage: String? private enum GeometryName { case backButton case topTabBar @@ -38,7 +39,8 @@ struct CourseHeaderView: View { collapsed: Binding, containerWidth: CGFloat, animationNamespace: Namespace.ID, - isAnimatingForTap: Binding + isAnimatingForTap: Binding, + courseRawImage: String? ) { self.viewModel = viewModel self.title = title @@ -46,14 +48,15 @@ struct CourseHeaderView: View { self.containerWidth = containerWidth self.animationNamespace = animationNamespace self._isAnimatingForTap = isAnimatingForTap + self.courseRawImage = courseRawImage } var body: some View { ZStack(alignment: .bottomLeading) { ScrollView { - if let banner = viewModel.courseStructure?.media.image.raw + if let banner = (courseRawImage ?? viewModel.courseStructure?.media.image.raw)? .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - KFImage(URL(string: viewModel.config.baseURL.absoluteString + banner)) + KFImage(courseBannerURL(for: banner)) .onFailureImage(CoreAssets.noCourseImage.image) .resizable() .aspectRatio(contentMode: .fill) @@ -159,6 +162,13 @@ struct CourseHeaderView: View { .ignoresSafeArea(edges: .top) } + private func courseBannerURL(for path: String) -> URL? { + if path.contains("http://") || path.contains("https://") { + return URL(string: path) + } + return URL(string: viewModel.config.baseURL.absoluteString + path) + } + private func courseMenuBar(containerWidth: CGFloat) -> some View { ScrollSlidingTabBar( selection: $viewModel.selection, diff --git a/Course/Course/Presentation/Subviews/CourseProgressView.swift b/Course/Course/Presentation/Subviews/CourseProgressView.swift new file mode 100644 index 000000000..70ee1c2d8 --- /dev/null +++ b/Course/Course/Presentation/Subviews/CourseProgressView.swift @@ -0,0 +1,54 @@ +// +// CourseProgressView.swift +// Course +// +// Created by  Stepanok Ivan on 23.05.2024. +// + +import SwiftUI +import Theme +import Core + +public struct CourseProgressView: View { + private var progress: CourseProgress + + public init(progress: CourseProgress) { + self.progress = progress + } + + public var body: some View { + VStack(alignment: .leading) { + ZStack(alignment: .leading) { + GeometryReader { geometry in + RoundedRectangle(cornerRadius: 10) + .fill(Theme.Colors.textSecondary.opacity(0.5)) + .frame(width: geometry.size.width, height: 10) + + if let total = progress.totalAssignmentsCount, + let completed = progress.assignmentsCompleted { + RoundedCorners(tl: 5, tr: 0, bl: 5, br: 0) + .fill(Theme.Colors.accentColor) + .frame(width: geometry.size.width * CGFloat(completed) / CGFloat(total), height: 10) + } + } + .frame(height: 10) + } + .cornerRadius(10) + + if let total = progress.totalAssignmentsCount, + let completed = progress.assignmentsCompleted { + Text(CourseLocalization.Course.progressCompleted(completed, total)) + .foregroundColor(Theme.Colors.textPrimary) + .font(Theme.Fonts.labelSmall) + .padding(.top, 4) + } + } + } +} + +struct CourseProgressView_Previews: PreviewProvider { + static var previews: some View { + CourseProgressView(progress: CourseProgress(totalAssignmentsCount: 20, assignmentsCompleted: 12)) + .padding() + } +} diff --git a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift index 5caa8ece4..704a7ad64 100644 --- a/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift +++ b/Course/Course/Presentation/Subviews/CourseVideoDownloadBarView/CourseVideoDownloadBarViewModel.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import Combine final class CourseVideoDownloadBarViewModel: ObservableObject { @@ -43,18 +44,17 @@ final class CourseVideoDownloadBarViewModel: ObservableObject { return 0.0 } guard let index = courseViewModel.courseDownloadTasks.firstIndex( - where: { $0.id == currentDownloadTask.id } + where: { $0.id == currentDownloadTask.id && $0.type == .video } ) else { return 0.0 } courseViewModel.courseDownloadTasks[index].progress = currentDownloadTask.progress - return courseViewModel - .courseDownloadTasks - .reduce(0) { $0 + $1.progress } / Double(courseViewModel.courseDownloadTasks.count) + let videoTasks = courseViewModel.courseDownloadTasks.filter { $0.type == .video } + return videoTasks.reduce(0) { $0 + $1.progress } / Double(videoTasks.count) } var downloadableVerticals: Set { - courseViewModel.downloadableVerticals + courseViewModel.downloadableVerticals.filter { $0.downloadableBlocks.contains { $0.type == .video } } } var allVideosDownloaded: Bool { @@ -124,7 +124,7 @@ final class CourseVideoDownloadBarViewModel: ObservableObject { func onToggle() async { if allVideosDownloaded { courseViewModel.router.presentAlert( - alertTitle: "Warning", + alertTitle: CourseLocalization.Alert.warning, alertMessage: "\(CourseLocalization.Alert.deleteAllVideos) \"\(courseStructure.displayName)\"?", positiveAction: CoreLocalization.Alert.delete, onCloseTapped: { [weak self] in @@ -145,7 +145,7 @@ final class CourseVideoDownloadBarViewModel: ObservableObject { if isOn { courseViewModel.router.presentAlert( - alertTitle: "Warning", + alertTitle: CourseLocalization.Alert.warning, alertMessage: "\(CourseLocalization.Alert.stopDownloading) \"\(courseStructure.displayName)\"", positiveAction: CoreLocalization.Alert.accept, onCloseTapped: { [weak self] in @@ -180,7 +180,7 @@ final class CourseVideoDownloadBarViewModel: ObservableObject { let blocks = downloadableVerticals.filter { $0.state != .finished }.flatMap { $0.vertical.childs } await courseViewModel.download( state: .available, - blocks: blocks + blocks: blocks.filter { $0.type == .video }, sequentials: [] ) } else { do { @@ -188,7 +188,6 @@ final class CourseVideoDownloadBarViewModel: ObservableObject { } catch { debugLog(error) } - } } diff --git a/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift new file mode 100644 index 000000000..f7ae7ea54 --- /dev/null +++ b/Course/Course/Presentation/Subviews/CustomDisclosureGroup.swift @@ -0,0 +1,431 @@ +// +// CustomDisclosureGroup.swift +// Course +// +// Created by  Stepanok Ivan on 21.05.2024. +// + +import SwiftUI +import Core +import Theme + +struct CustomDisclosureGroup: View { + @State private var expandedSections: [String: Bool] = [:] + + private let isVideo: Bool + private let proxy: GeometryProxy + private let course: CourseStructure + private let viewModel: CourseContainerViewModel + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + init(isVideo: Bool, course: CourseStructure, proxy: GeometryProxy, viewModel: CourseContainerViewModel) { + self.isVideo = isVideo + self.course = course + self.proxy = proxy + self.viewModel = viewModel + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + ForEach(course.childs) { chapter in + let chapterIndex = course.childs.firstIndex(where: { $0.id == chapter.id }) + VStack(alignment: .leading) { + Button( + action: { + withAnimation(.linear(duration: course.childs.count > 1 ? 0.2 : 0.05)) { + expandedSections[chapter.id, default: false].toggle() + } + }, label: { + HStack { + CoreAssets.chevronRight.swiftUIImage + .rotationEffect(.degrees(expandedSections[chapter.id] ?? false ? -90 : 90)) + .foregroundColor(Theme.Colors.textPrimary) + if chapter.childs.allSatisfy({ $0.completion == 1 }) { + CoreAssets.finishedSequence.swiftUIImage + } + Text(chapter.displayName) + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textPrimary) + .lineLimit(1) + Spacer() + if canDownloadAllSections(in: chapter, videoOnly: isVideo), + let state = downloadAllButtonState(for: chapter, videoOnly: isVideo) { + Button( + action: { + downloadAllSubsections(in: chapter, state: state) + }, label: { + switch state { + case .available: + DownloadAvailableView() + case .downloading: + DownloadProgressView() + case .finished: + DownloadFinishedView() + } + + } + ) + } + } + } + ) + if expandedSections[chapter.id] ?? false { + VStack(alignment: .leading) { + ForEach(chapter.childs) { sequential in + let sequentialIndex = chapter.childs.firstIndex(where: { $0.id == sequential.id }) + VStack(alignment: .leading) { + HStack { + Button( + action: { + guard let chapterIndex = chapterIndex else { return } + guard let sequentialIndex else { return } + guard let courseVertical = sequential.childs.first else { return } + guard let block = courseVertical.childs.first else { + viewModel.router.showGatedContentError(url: courseVertical.webUrl) + return + } + + viewModel.trackSequentialClicked(sequential) + if viewModel.config.uiComponents.courseDropDownNavigationEnabled { + viewModel.router.showCourseUnit( + courseName: viewModel.courseStructure?.displayName ?? "", + blockId: block.id, + courseID: viewModel.courseStructure?.id ?? "", + verticalIndex: 0, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + } else { + viewModel.router.showCourseVerticalView( + courseID: viewModel.courseStructure?.id ?? "", + courseName: viewModel.courseStructure?.displayName ?? "", + title: sequential.displayName, + chapters: course.childs, + chapterIndex: chapterIndex, + sequentialIndex: sequentialIndex + ) + } + }, + label: { + VStack(alignment: .leading) { + HStack { + if sequential.completion == 1 { + CoreAssets.finishedSequence.swiftUIImage + .resizable() + .frame(width: 20, height: 20) + } else { + sequential.type.image + } + Text(sequential.displayName) + .font(Theme.Fonts.titleSmall) + .multilineTextAlignment(.leading) + .lineLimit(1) + .frame( + maxWidth: idiom == .pad + ? proxy.size.width * 0.5 + : proxy.size.width * 0.6, + alignment: .leading + ) + } + if let sequentialProgress = sequential.sequentialProgress, + let assignmentType = sequentialProgress.assignmentType, + let numPointsEarned = sequentialProgress.numPointsEarned, + let numPointsPossible = sequentialProgress.numPointsPossible, + let due = sequential.due { + let daysRemaining = getAssignmentStatus(for: due) + Text( + """ + \(assignmentType) - + \(daysRemaining) - + \(numPointsEarned) / + \(numPointsPossible) + """ + ) + .font(Theme.Fonts.bodySmall) + .multilineTextAlignment(.leading) + .lineLimit(2) + } + } + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityElement(children: .ignore) + .accessibilityLabel(sequential.displayName) + } + ) + Spacer() + if sequential.due != nil { + CoreAssets.chevronRight.swiftUIImage + .foregroundColor(Theme.Colors.textPrimary) + } + } + .padding(.vertical, 4) + } + } + } + + } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Theme.Colors.datesSectionBackground) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(style: .init(lineWidth: 1, lineCap: .round, lineJoin: .round, miterLimit: 1)) + .foregroundColor(Theme.Colors.cardViewStroke) + ) + } + } + .padding(.horizontal, 24) + .padding(.vertical, 8) + .onFirstAppear { + for chapter in course.childs { + expandedSections[chapter.id] = false + } + } + } + + private func deleteMessage(for chapter: CourseChapter) -> String { + "\(CourseLocalization.Alert.deleteVideos) \"\(chapter.displayName)\"?" + } + + func getAssignmentStatus(for date: Date) -> String { + let calendar = Calendar.current + let today = Date() + + if calendar.isDateInToday(date) { + return CourseLocalization.Course.dueToday + } else if calendar.isDateInTomorrow(date) { + return CourseLocalization.Course.dueTomorrow + } else if let daysUntil = calendar.dateComponents([.day], from: today, to: date).day, daysUntil > 0 { + return CourseLocalization.dueIn(daysUntil) + } else if let daysAgo = calendar.dateComponents([.day], from: date, to: today).day, daysAgo > 0 { + return CourseLocalization.pastDue(daysAgo) + } else { + return "" + } + } + + private func canDownloadAllSections(in chapter: CourseChapter, videoOnly: Bool) -> Bool { + for sequential in chapter.childs { + if videoOnly { + let isDownloadable = sequential.childs.flatMap { + $0.childs.filter({ $0.type == .video }) + }.contains(where: { $0.isDownloadable }) + guard isDownloadable else { return false } + } + if viewModel.sequentialsDownloadState[sequential.id] != nil { + return true + } + } + return false + } + + private func downloadAllSubsections(in chapter: CourseChapter, state: DownloadViewState) { + Task { + var allBlocks: [CourseBlock] = [] + for sequential in chapter.childs { + let blocks = await viewModel.collectBlocks(chapter: chapter, blockId: sequential.id, state: state) + allBlocks.append(contentsOf: blocks) + } + await viewModel.download( + state: state, + blocks: allBlocks, + sequentials: chapter.childs.filter({ $0.isDownloadable }) + ) + } + } + + private func downloadAllButtonState(for chapter: CourseChapter, videoOnly: Bool) -> DownloadViewState? { + if canDownloadAllSections(in: chapter, videoOnly: videoOnly) { + let downloads = chapter.childs.filter({ viewModel.sequentialsDownloadState[$0.id] != nil }) + + if downloads.contains(where: { viewModel.sequentialsDownloadState[$0.id] == .downloading }) { + return .downloading + } else if downloads.allSatisfy({ viewModel.sequentialsDownloadState[$0.id] == .finished }) { + return .finished + } else { + return .available + } + } + return nil + } + +} + +#if DEBUG +struct CustomDisclosureGroup_Previews: PreviewProvider { + + static var previews: some View { + + // Sample data models + let sampleCourseChapters: [CourseChapter] = [ + CourseChapter( + blockId: "1", + id: "1", + displayName: "Chapter 1", + type: .chapter, + childs: [ + CourseSequential( + blockId: "1-1", + id: "1-1", + displayName: "Sequential 1", + type: .sequential, + completion: 0, + childs: [ + CourseVertical( + blockId: "1-1-1", + id: "1-1-1", + courseId: "1", + displayName: "Vertical 1", + type: .vertical, + completion: 0, + childs: [], + webUrl: "" + ), + CourseVertical( + blockId: "1-1-2", + id: "1-1-2", + courseId: "1", + displayName: "Vertical 2", + type: .vertical, + completion: 1.0, + childs: [], + webUrl: "" + ) + ], + sequentialProgress: SequentialProgress( + assignmentType: "Advanced Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() + ), + CourseSequential( + blockId: "1-2", + id: "1-2", + displayName: "Sequential 2", + type: .sequential, + completion: 1.0, + childs: [ + CourseVertical( + blockId: "1-2-1", + id: "1-2-1", + courseId: "1", + displayName: "Vertical 3", + type: .vertical, + completion: 1.0, + childs: [], + webUrl: "" + ) + ], + sequentialProgress: SequentialProgress( + assignmentType: "Basic Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() + ) + ] + ), + CourseChapter( + blockId: "2", + id: "2", + displayName: "Chapter 2", + type: .chapter, + childs: [ + CourseSequential( + blockId: "2-1", + id: "2-1", + displayName: "Sequential 3", + type: .sequential, + completion: 1.0, + childs: [ + CourseVertical( + blockId: "2-1-1", + id: "2-1-1", + courseId: "2", + displayName: "Vertical 4", + type: .vertical, + completion: 1.0, + childs: [], + webUrl: "" + ), + CourseVertical( + blockId: "2-1-2", + id: "2-1-2", + courseId: "2", + displayName: "Vertical 5", + type: .vertical, + completion: 1.0, + childs: [], + webUrl: "" + ) + ], + sequentialProgress: SequentialProgress( + assignmentType: "Advanced Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() + ) + ] + ) + ] + + let viewModel = CourseContainerViewModel( + interactor: CourseInteractor.mock, + authInteractor: AuthInteractor.mock, + router: CourseRouterMock(), + analytics: CourseAnalyticsMock(), + config: ConfigMock(), + connectivity: Connectivity(), + manager: DownloadManagerMock(), + storage: CourseStorageMock(), + isActive: true, + courseStart: Date(), + courseEnd: nil, + enrollmentStart: Date(), + enrollmentEnd: nil, + lastVisitedBlockID: nil, + coreAnalytics: CoreAnalyticsMock() + ) + Task { + await withTaskGroup(of: Void.self) { group in + group.addTask { + await viewModel.getCourseBlocks(courseID: "courseId") + } + group.addTask { + await viewModel.getCourseDeadlineInfo(courseID: "courseId") + } + } + } + + return GeometryReader { proxy in + ScrollView { + CustomDisclosureGroup( + isVideo: false, + course: CourseStructure( + id: "Id", + graded: false, + completion: 0, + viewYouTubeUrl: "", + encodedVideo: "", + displayName: "Course", + childs: sampleCourseChapters, + media: DataLayer.CourseMedia.init(image: DataLayer.Image(raw: "", small: "", large: "")), + certificate: nil, + org: "org", + isSelfPaced: false, + courseProgress: nil + ), + proxy: proxy, + viewModel: viewModel + ) + } + } + } +} +#endif diff --git a/Course/Course/Views/DatesSuccessView.swift b/Course/Course/Presentation/Subviews/DatesSuccessView/DatesSuccessView.swift similarity index 100% rename from Course/Course/Views/DatesSuccessView.swift rename to Course/Course/Presentation/Subviews/DatesSuccessView/DatesSuccessView.swift diff --git a/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityBarView.swift b/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityBarView.swift index 56d4fa158..8617dcb58 100644 --- a/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityBarView.swift +++ b/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityBarView.swift @@ -9,7 +9,6 @@ import SwiftUI import Core import Theme import Combine -import Profile struct VideoDownloadQualityBarView: View { diff --git a/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift b/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift index 4418d3513..79007c35e 100644 --- a/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift +++ b/Course/Course/Presentation/Subviews/VideoDownloadQualityBarView/VideoDownloadQualityContainerView.swift @@ -16,11 +16,21 @@ struct VideoDownloadQualityContainerView: View { private var downloadQuality: DownloadQuality private var didSelect: ((DownloadQuality) -> Void)? private let analytics: CoreAnalytics + private let router: CourseRouter + private var isModal: Bool - init(downloadQuality: DownloadQuality, didSelect: ((DownloadQuality) -> Void)?, analytics: CoreAnalytics) { + init( + downloadQuality: DownloadQuality, + didSelect: ((DownloadQuality) -> Void)?, + analytics: CoreAnalytics, + router: CourseRouter, + isModal: Bool = false + ) { self.downloadQuality = downloadQuality self.didSelect = didSelect self.analytics = analytics + self.router = router + self.isModal = isModal } var body: some View { @@ -28,7 +38,9 @@ struct VideoDownloadQualityContainerView: View { VideoDownloadQualityView( downloadQuality: downloadQuality, didSelect: didSelect, - analytics: analytics + analytics: analytics, + router: router, + isModal: isModal ) .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Course/Course/Presentation/Unit/CourseNavigationView.swift b/Course/Course/Presentation/Unit/CourseNavigationView.swift index 334b3cf08..76788d26f 100644 --- a/Course/Course/Presentation/Unit/CourseNavigationView.swift +++ b/Course/Course/Presentation/Unit/CourseNavigationView.swift @@ -156,7 +156,7 @@ struct CourseNavigationView_Previews: PreviewProvider { chapterIndex: 1, sequentialIndex: 1, verticalIndex: 1, - interactor: CourseInteractor.mock, + interactor: CourseInteractor.mock, config: ConfigMock(), router: CourseRouterMock(), analytics: CourseAnalyticsMock(), diff --git a/Course/Course/Presentation/Unit/CourseUnitView.swift b/Course/Course/Presentation/Unit/CourseUnitView.swift index dff0a0ea9..2bbdc58cc 100644 --- a/Course/Course/Presentation/Unit/CourseUnitView.swift +++ b/Course/Course/Presentation/Unit/CourseUnitView.swift @@ -8,6 +8,7 @@ import Foundation import SwiftUI import Core +import OEXFoundation import Discussion import Combine import Theme @@ -34,6 +35,9 @@ public struct CourseUnitView: View { private let portraitTopSpacing: CGFloat = 60 private let landscapeTopSpacing: CGFloat = 75 + @State private var videoURL: URL? + @State private var webURL: URL? + let isDropdownActive: Bool var sequenceTitle: String { @@ -184,12 +188,14 @@ public struct CourseUnitView: View { isOnScreen: index == viewModel.index ) .frameLimit(width: reader.size.width) - + if !isHorizontal { Spacer(minLength: 150) } } else { - NoInternetView() + OfflineContentView( + isDownloadable: false + ) } } else { @@ -214,40 +220,49 @@ public struct CourseUnitView: View { ) .padding(.top, 5) .frameLimit(width: reader.size.width) - + if !isHorizontal { Spacer(minLength: 150) } } else { - NoInternetView() + OfflineContentView( + isDownloadable: true + ) } } + // MARK: Web - case let .web(url, injections): + case let .web(url, injections, blockId, isDownloadable): if index >= viewModel.index - 1 && index <= viewModel.index + 1 { - if viewModel.connectivity.isInternetAvaliable { + let localUrl = viewModel.urlForOfflineContent(blockId: blockId)?.absoluteString + if viewModel.connectivity.isInternetAvaliable || localUrl != nil { + // not need to add frame limit there because we did that with injection WebView( url: url, + localUrl: viewModel.connectivity.isInternetAvaliable ? nil : localUrl, injections: injections, + blockID: block.id, roundedBackgroundEnabled: !viewModel.courseUnitProgressEnabled ) - // not need to add frame limit there because we did that with injection } else { - NoInternetView() + OfflineContentView( + isDownloadable: isDownloadable + ) } } else { EmptyView() } + // MARK: Unknown case .unknown(let url): if index >= viewModel.index - 1 && index <= viewModel.index + 1 { if viewModel.connectivity.isInternetAvaliable { - UnknownView(url: url, viewModel: viewModel) + NotAvailableOnMobileView(url: url) .frameLimit(width: reader.size.width) - Spacer() - .frame(minHeight: 100) } else { - NoInternetView() + OfflineContentView( + isDownloadable: false + ) } } else { EmptyView() @@ -275,7 +290,7 @@ public struct CourseUnitView: View { //No need iPad paddings there bacause they were added //to PostsView that placed inside DiscussionView } else { - NoInternetView() + FullScreenErrorView(type: .noInternet) } } else { EmptyView() @@ -442,13 +457,15 @@ struct CourseUnitView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 1", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ), CourseBlock( blockId: "2", @@ -456,13 +473,15 @@ struct CourseUnitView_Previews: PreviewProvider { courseId: "123", topicId: "2", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 2", studentUrl: "2", webUrl: "2", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ), CourseBlock( blockId: "3", @@ -470,13 +489,15 @@ struct CourseUnitView_Previews: PreviewProvider { courseId: "123", topicId: "3", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "Lesson 3", studentUrl: "3", webUrl: "3", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ), CourseBlock( blockId: "4", @@ -484,13 +505,15 @@ struct CourseUnitView_Previews: PreviewProvider { courseId: "123", topicId: "4", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "4", studentUrl: "4", webUrl: "4", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ), ] @@ -515,12 +538,20 @@ struct CourseUnitView_Previews: PreviewProvider { displayName: "6", type: .vertical, completion: 0, - childs: blocks + childs: blocks, + webUrl: "" ) - ] + ], + sequentialProgress: SequentialProgress( + assignmentType: "Advanced Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() ) - ]), + ] + ), CourseChapter( blockId: "2", id: "2", @@ -541,9 +572,16 @@ struct CourseUnitView_Previews: PreviewProvider { displayName: "4", type: .vertical, completion: 0, - childs: blocks + childs: blocks, + webUrl: "" ) - ] + ], + sequentialProgress: SequentialProgress( + assignmentType: "Basic Assessment Tools", + numPointsEarned: 1, + numPointsPossible: 3 + ), + due: Date() ) ]) @@ -569,25 +607,3 @@ struct CourseUnitView_Previews: PreviewProvider { } //swiftlint:enable all #endif - -struct NoInternetView: View { - - var body: some View { - VStack(spacing: 28) { - Spacer() - CoreAssets.noWifi.swiftUIImage - .renderingMode(.template) - .foregroundStyle(Color.primary) - .scaledToFit() - Text(CoreLocalization.Error.Internet.noInternetTitle) - .font(Theme.Fonts.titleLarge) - .foregroundColor(Theme.Colors.textPrimary) - Text(CoreLocalization.Error.Internet.noInternetDescription) - .font(Theme.Fonts.bodyLarge) - .foregroundColor(Theme.Colors.textPrimary) - .multilineTextAlignment(.center) - .padding(.horizontal, 50) - Spacer() - }.frame(maxWidth: .infinity, maxHeight: .infinity) - } -} diff --git a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift index 8f4be45b8..8e3ef4497 100644 --- a/Course/Course/Presentation/Unit/CourseUnitViewModel.swift +++ b/Course/Course/Presentation/Unit/CourseUnitViewModel.swift @@ -9,9 +9,9 @@ import SwiftUI import Core public enum LessonType: Equatable { - case web(url: String, injections: [WebviewInjection]) - case youtube(youtubeVideoUrl: String, blockID: String) - case video(videoUrl: String, blockID: String) + case web(url: String, injections: [WebviewInjection], blockId: String, isDownloadable: Bool) + case youtube(youtubeVideoUrl: String, blockId: String) + case video(videoUrl: String, blockId: String) case unknown(String) case discussion(String, String, String) @@ -22,34 +22,66 @@ public enum LessonType: Equatable { return .unknown(block.studentUrl) case .unknown: if let multiDevice = block.multiDevice, multiDevice { - return .web(url: block.studentUrl, injections: mandatoryInjections) + return .web( + url: block.studentUrl, + injections: mandatoryInjections, + blockId: block.id, + isDownloadable: block.isDownloadable + ) } else { return .unknown(block.studentUrl) } case .html: - return .web(url: block.studentUrl, injections: mandatoryInjections) + return .web( + url: block.studentUrl, + injections: mandatoryInjections, + blockId: block.id, + isDownloadable: block.isDownloadable + ) case .discussion: return .discussion(block.topicId ?? "", block.id, block.displayName) case .video: if block.encodedVideo?.youtubeVideoUrl != nil, let encodedVideo = block.encodedVideo?.video(streamingQuality: streamingQuality)?.url { - return .video(videoUrl: encodedVideo, blockID: block.id) + return .video(videoUrl: encodedVideo, blockId: block.id) } else if let youtubeVideoUrl = block.encodedVideo?.youtubeVideoUrl { - return .youtube(youtubeVideoUrl: youtubeVideoUrl, blockID: block.id) + return .youtube(youtubeVideoUrl: youtubeVideoUrl, blockId: block.id) } else if let encodedVideo = block.encodedVideo?.video(streamingQuality: streamingQuality)?.url { - return .video(videoUrl: encodedVideo, blockID: block.id) + return .video(videoUrl: encodedVideo, blockId: block.id) + } else if let encodedVideo = block.encodedVideo?.video(downloadQuality: DownloadQuality.auto)?.url { + return .video(videoUrl: encodedVideo, blockId: block.id) } else { return .unknown(block.studentUrl) } case .problem: - return .web(url: block.studentUrl, injections: mandatoryInjections) + return .web( + url: block.studentUrl, + injections: mandatoryInjections, + blockId: block.id, + isDownloadable: block.isDownloadable + ) case .dragAndDropV2: - return .web(url: block.studentUrl, injections: mandatoryInjections + [.dragAndDropCss]) + return .web( + url: block.studentUrl, + injections: mandatoryInjections + [.dragAndDropCss], + blockId: block.id, + isDownloadable: block.isDownloadable + ) case .survey: - return .web(url: block.studentUrl, injections: mandatoryInjections + [.surveyCSS]) + return .web( + url: block.studentUrl, + injections: mandatoryInjections + [.surveyCSS], + blockId: block.id, + isDownloadable: block.isDownloadable + ) case .openassessment, .peerInstructionTool: - return .web(url: block.studentUrl, injections: mandatoryInjections) + return .web( + url: block.studentUrl, + injections: mandatoryInjections, + blockId: block.id, + isDownloadable: block.isDownloadable + ) } } } @@ -238,6 +270,10 @@ public class CourseUnitViewModel: ObservableObject { return URL(string: url) } } + + func urlForOfflineContent(blockId: String) -> URL? { + return manager.fileUrl(for: blockId) + } func trackFinishVerticalBackToOutlineClicked() { analytics.finishVerticalBackToOutlineClicked(courseId: courseID, courseName: courseName) diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift index 397bf332a..5a280cbeb 100644 --- a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownCell.swift @@ -77,15 +77,18 @@ struct CourseUnitDropDownCell_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 1, type: .video, displayName: "Lesson 1", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ) - ] + ], + webUrl: "" ) CourseUnitDropDownCell( diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift index f338a202f..e449f77fc 100644 --- a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitDropDownList.swift @@ -51,13 +51,15 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 1", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ), CourseBlock( blockId: "2", @@ -65,13 +67,15 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { courseId: "123", topicId: "2", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 2", studentUrl: "2", webUrl: "2", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ), CourseBlock( blockId: "3", @@ -79,13 +83,15 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { courseId: "123", topicId: "3", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "Lesson 3", studentUrl: "3", webUrl: "3", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ), CourseBlock( blockId: "4", @@ -93,13 +99,15 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { courseId: "123", topicId: "4", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "4", studentUrl: "4", webUrl: "4", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ) ] @@ -111,7 +119,8 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { displayName: "First Unit", type: .vertical, completion: 0, - childs: blocks + childs: blocks, + webUrl: "" ), CourseVertical( blockId: "2", @@ -120,7 +129,8 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { displayName: "Second Unit", type: .vertical, completion: 1, - childs: blocks + childs: blocks, + webUrl: "" ), CourseVertical( blockId: "3", @@ -129,7 +139,8 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { displayName: "Third Unit", type: .vertical, completion: 0, - childs: blocks + childs: blocks, + webUrl: "" ), CourseVertical( blockId: "4", @@ -138,7 +149,8 @@ struct CourseUnitDropDownList_Previews: PreviewProvider { displayName: "Fourth Unit", type: .vertical, completion: 1, - childs: blocks + childs: blocks, + webUrl: "" ) ] diff --git a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift index 805c709a1..89f410ed3 100644 --- a/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift +++ b/Course/Course/Presentation/Unit/Subviews/DropdownList/CourseUnitVerticalsDropdownView.swift @@ -64,13 +64,15 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { courseId: "123", topicId: "1", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 1", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ), CourseBlock( @@ -79,13 +81,15 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { courseId: "123", topicId: "2", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 2", studentUrl: "2", webUrl: "2", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ), CourseBlock( @@ -94,13 +98,15 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { courseId: "123", topicId: "3", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "Lesson 3", studentUrl: "3", webUrl: "3", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ), CourseBlock( @@ -109,13 +115,15 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { courseId: "123", topicId: "4", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "4", studentUrl: "4", webUrl: "4", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ) ] @@ -127,7 +135,8 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { displayName: "First Unit", type: .vertical, completion: 0, - childs: blocks + childs: blocks, + webUrl: "" ), CourseVertical( blockId: "2", @@ -136,7 +145,8 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { displayName: "Second Unit", type: .vertical, completion: 1, - childs: blocks + childs: blocks, + webUrl: "" ), CourseVertical( blockId: "3", @@ -145,7 +155,8 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { displayName: "Third Unit", type: .vertical, completion: 0, - childs: blocks + childs: blocks, + webUrl: "" ), CourseVertical( blockId: "4", @@ -154,7 +165,8 @@ struct CourseUnitVerticalsDropdownView_Previews: PreviewProvider { displayName: "Fourth Unit", type: .vertical, completion: 1, - childs: blocks + childs: blocks, + webUrl: "" ) ] diff --git a/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift b/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift index b569a4f65..8233051d0 100644 --- a/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift +++ b/Course/Course/Presentation/Unit/Subviews/LessonLineProgressView.swift @@ -11,7 +11,7 @@ import Theme struct LessonLineProgressView: View { @ObservedObject var viewModel: CourseUnitViewModel - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal init(viewModel: CourseUnitViewModel) { self.viewModel = viewModel diff --git a/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift index d1e848258..32badbf2a 100644 --- a/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift +++ b/Course/Course/Presentation/Unit/Subviews/LessonProgressView.swift @@ -12,7 +12,7 @@ import Theme struct LessonProgressView: View { @ObservedObject var viewModel: CourseUnitViewModel - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal init(viewModel: CourseUnitViewModel) { self.viewModel = viewModel diff --git a/Course/Course/Presentation/Unit/Subviews/NotAvailableOnMobileView.swift b/Course/Course/Presentation/Unit/Subviews/NotAvailableOnMobileView.swift new file mode 100644 index 000000000..dac965193 --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/NotAvailableOnMobileView.swift @@ -0,0 +1,46 @@ +// +// NotAvailableOnMobileView.swift +// Course +// +// Created by  Stepanok Ivan on 30.05.2023. +// + +import SwiftUI +import Core +import Theme + +public struct NotAvailableOnMobileView: View { + let url: String + + public init(url: String) { + self.url = url + } + + public var body: some View { + ZStack(alignment: .center) { + VStack(spacing: 10) { + Spacer() + CoreAssets.notAvaliable.swiftUIImage + Text(CourseLocalization.NotAvaliable.title) + .font(Theme.Fonts.titleLarge) + .multilineTextAlignment(.center) + .padding(.top, 40) + Text(CourseLocalization.NotAvaliable.description) + .font(Theme.Fonts.bodyLarge) + .multilineTextAlignment(.center) + .padding(.top, 12) + StyledButton(CourseLocalization.NotAvaliable.button, action: { + if let url = URL(string: url), UIApplication.shared.canOpenURL(url) { + // Added empty options to avoid calling overridden open(url) function in facebook sdk + UIApplication.shared.open(url, options: [:]) + } + }) + .frame(width: 215) + .padding(.top, 40) + Spacer() + } + .padding(24) + } + .navigationBarHidden(false) + } +} diff --git a/Course/Course/Presentation/Unit/Subviews/OfflineContentView.swift b/Course/Course/Presentation/Unit/Subviews/OfflineContentView.swift new file mode 100644 index 000000000..cfe51c9f2 --- /dev/null +++ b/Course/Course/Presentation/Unit/Subviews/OfflineContentView.swift @@ -0,0 +1,65 @@ +// +// OfflineContentView.swift +// Course +// +// Created by  Stepanok Ivan on 22.06.2024. +// + +import SwiftUI +import Core +import Theme + +public struct OfflineContentView: View { + + enum OfflineContentState { + case notDownloaded + case notAvailableOffline + + var title: String { + switch self { + case .notDownloaded: + return CourseLocalization.Offline.NotDownloaded.title + case .notAvailableOffline: + return CourseLocalization.Offline.NotAvaliable.title + } + } + + var description: String { + switch self { + case .notDownloaded: + return CourseLocalization.Offline.NotDownloaded.description + case .notAvailableOffline: + return CourseLocalization.Offline.NotAvaliable.description + } + } + } + + @State private var contentState: OfflineContentState + + public init(isDownloadable: Bool) { + contentState = isDownloadable ? .notDownloaded : .notAvailableOffline + } + + public var body: some View { + VStack(spacing: 0) { + Spacer() + CoreAssets.notAvaliable.swiftUIImage + Text(contentState.title) + .font(Theme.Fonts.titleLarge) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.top, 40) + Text(contentState.description) + .font(Theme.Fonts.bodyLarge) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding(.top, 12) + Spacer() + } + .padding(24) + } +} + +#Preview { + OfflineContentView(isDownloadable: true) +} diff --git a/Course/Course/Presentation/Unit/Subviews/UnknownView.swift b/Course/Course/Presentation/Unit/Subviews/UnknownView.swift deleted file mode 100644 index d38104a23..000000000 --- a/Course/Course/Presentation/Unit/Subviews/UnknownView.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// UnknownView.swift -// Course -// -// Created by  Stepanok Ivan on 30.05.2023. -// - -import SwiftUI -import Core -import Theme - -struct UnknownView: View { - let url: String - let viewModel: CourseUnitViewModel - - var body: some View { - VStack(spacing: 0) { - Spacer() - CoreAssets.notAvaliable.swiftUIImage - Text(CourseLocalization.NotAvaliable.title) - .font(Theme.Fonts.titleLarge) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding(.top, 40) - Text(CourseLocalization.NotAvaliable.description) - .font(Theme.Fonts.bodyLarge) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding(.top, 12) - StyledButton(CourseLocalization.NotAvaliable.button, action: { - if let url = URL(string: url) { - UIApplication.shared.open(url) - } - }) - .frame(width: 215) - .padding(.top, 40) - Spacer() - } - .padding(24) - } -} diff --git a/Course/Course/Presentation/Unit/Subviews/WebView.swift b/Course/Course/Presentation/Unit/Subviews/WebView.swift index 8d1b0c7ad..b8823bda6 100644 --- a/Course/Course/Presentation/Unit/Subviews/WebView.swift +++ b/Course/Course/Presentation/Unit/Subviews/WebView.swift @@ -12,15 +12,20 @@ import Theme struct WebView: View { let url: String + let localUrl: String? let injections: [WebviewInjection] + let blockID: String var roundedBackgroundEnabled: Bool = true - + var body: some View { VStack(spacing: 0) { WebUnitView( url: url, + dataUrl: localUrl, viewModel: Container.shared.resolve(WebUnitViewModel.self)!, - injections: injections + connectivity: Connectivity(), + injections: injections, + blockID: blockID ) if roundedBackgroundEnabled { Spacer(minLength: 5) diff --git a/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift b/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift index 934d75803..3be341524 100644 --- a/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift +++ b/Course/Course/Presentation/Unit/Subviews/YouTubeView.swift @@ -24,7 +24,7 @@ struct YouTubeView: View { var body: some View { let vm = Container.shared.resolve( YouTubeVideoPlayerViewModel.self, - arguments: url, + arguments: URL(string: url), blockID, courseID, languages, diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift index cdea26e70..72e0c5dde 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayer.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayer.swift @@ -27,10 +27,7 @@ public struct EncodedVideoPlayer: View { @State private var orientation = UIDevice.current.orientation @State private var isLoading: Bool = true @State private var isAnimating: Bool = false - @State private var isViewedOnce: Bool = false - @State private var currentTime: Double = 0 @State private var isOrientationChanged: Bool = false - @State private var pause: Bool = false @State var showAlert = false @State var alertMessage: String? { @@ -57,32 +54,13 @@ public struct EncodedVideoPlayer: View { VStack(spacing: 10) { HStack { VStack { - PlayerViewController( - videoURL: viewModel.url, - playerHolder: viewModel.controllerHolder, - bitrate: viewModel.getVideoResolution(), - progress: { progress in - if progress >= 0.8 { - if !isViewedOnce { - Task { - await viewModel.blockCompletionRequest() - } - isViewedOnce = true - } - } - if progress == 1 { - viewModel.router.presentAppReview() - } - - }, seconds: { seconds in - currentTime = seconds - }) + PlayerViewController(playerController: viewModel.controller) .aspectRatio(16 / 9, contentMode: .fit) .frame(minWidth: playerWidth(for: reader.size)) .cornerRadius(12) .onAppear { - if !viewModel.controllerHolder.isPlayingInPip, - !viewModel.controllerHolder.isOtherPlayerInPip { + if !viewModel.isPlayingInPip, + !viewModel.isOtherPlayerInPip { viewModel.controller.player?.play() } } @@ -91,9 +69,9 @@ public struct EncodedVideoPlayer: View { } } if isHorizontal { - SubtittlesView( + SubtitlesView( languages: viewModel.languages, - currentTime: $currentTime, + currentTime: $viewModel.currentTime, viewModel: viewModel, scrollTo: { date in viewModel.controller.player?.seek( @@ -103,15 +81,15 @@ public struct EncodedVideoPlayer: View { ) ) viewModel.controller.player?.play() - pauseScrolling() - currentTime = (date.secondsSinceMidnight() + 1) + viewModel.pauseScrolling() + viewModel.currentTime = (date.secondsSinceMidnight() + 1) }) } } if !isHorizontal { - SubtittlesView( + SubtitlesView( languages: viewModel.languages, - currentTime: $currentTime, + currentTime: $viewModel.currentTime, viewModel: viewModel, scrollTo: { date in viewModel.controller.player?.seek( @@ -121,8 +99,8 @@ public struct EncodedVideoPlayer: View { ) ) viewModel.controller.player?.play() - pauseScrolling() - currentTime = (date.secondsSinceMidnight() + 1) + viewModel.pauseScrolling() + viewModel.currentTime = (date.secondsSinceMidnight() + 1) }) } } @@ -134,17 +112,11 @@ public struct EncodedVideoPlayer: View { viewModel.controller.player?.allowsExternalPlayback = false } .onAppear { + viewModel.controller.player?.allowsExternalPlayback = true viewModel.controller.setNeedsStatusBarAppearanceUpdate() } } - private func pauseScrolling() { - pause = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - self.pause = false - } - } - private func playerWidth(for size: CGSize) -> CGFloat { if isHorizontal { return size.width * 0.6 @@ -163,17 +135,10 @@ struct EncodedVideoPlayer_Previews: PreviewProvider { static var previews: some View { EncodedVideoPlayer( viewModel: EncodedVideoPlayerViewModel( - url: URL(string: "")!, - blockID: "", - courseID: "", languages: [], playerStateSubject: CurrentValueSubject(nil), - interactor: CourseInteractor(repository: CourseRepositoryMock()), - router: CourseRouterMock(), - appStorage: CoreStorageMock(), connectivity: Connectivity(), - pipManager: PipManagerProtocolMock(), - selectedCourseTab: 0 + playerHolder: PlayerViewControllerHolder.mock ), isOnScreen: true ) diff --git a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift index bb5eb8d3e..b7bdaed9f 100644 --- a/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/EncodedVideoPlayerViewModel.swift @@ -10,83 +10,7 @@ import Core import Combine public class EncodedVideoPlayerViewModel: VideoPlayerViewModel { - - let url: URL? - - let controllerHolder: PlayerViewControllerHolder var controller: AVPlayerViewController { - controllerHolder.playerController - } - private var subscription = Set() - - public init( - url: URL?, - blockID: String, - courseID: String, - languages: [SubtitleUrl], - playerStateSubject: CurrentValueSubject, - interactor: CourseInteractorProtocol, - router: CourseRouter, - appStorage: CoreStorage, - connectivity: ConnectivityProtocol, - pipManager: PipManagerProtocol, - selectedCourseTab: Int - ) { - self.url = url - - if let holder = pipManager.holder( - for: url, - blockID: blockID, - courseID: courseID, - selectedCourseTab: selectedCourseTab - ) { - controllerHolder = holder - } else { - let holder = PlayerViewControllerHolder( - url: url, - blockID: blockID, - courseID: courseID, - selectedCourseTab: selectedCourseTab - ) - controllerHolder = holder - } - - super.init(blockID: blockID, - courseID: courseID, - languages: languages, - interactor: interactor, - router: router, - appStorage: appStorage, - connectivity: connectivity) - - playerStateSubject.sink(receiveValue: { [weak self] state in - switch state { - case .pause: - if self?.controllerHolder.isPlayingInPip != true { - self?.controller.player?.pause() - } - case .kill: - if self?.controllerHolder.isPlayingInPip != true { - self?.controller.player?.replaceCurrentItem(with: nil) - } - case .none: - break - } - }).store(in: &subscription) - } - - func getVideoResolution() -> CGSize { - switch appStorage.userSettings?.streamingQuality { - case .auto: - return CGSize(width: 1280, height: 720) - case .low: - return CGSize(width: 640, height: 360) - case .medium: - return CGSize(width: 854, height: 480) - case .high: - return CGSize(width: 1280, height: 720) - case .none: - return CGSize(width: 1280, height: 720) - } + (playerHolder.playerController as? AVPlayerViewController) ?? AVPlayerViewController() } } diff --git a/Course/Course/Presentation/Video/PipManagerProtocol.swift b/Course/Course/Presentation/Video/PipManagerProtocol.swift new file mode 100644 index 000000000..c92550cb3 --- /dev/null +++ b/Course/Course/Presentation/Video/PipManagerProtocol.swift @@ -0,0 +1,53 @@ +// +// PipManagerProtocol.swift +// Course +// +// Created by Vadim Kuznetsov on 22.04.24. +// + +import Combine +import Foundation + +public protocol PipManagerProtocol { + var isPipActive: Bool { get } + var isPipPlaying: Bool { get } + + func holder( + for url: URL?, + blockID: String, + courseID: String, + selectedCourseTab: Int + ) -> PlayerViewControllerHolderProtocol? + func set(holder: PlayerViewControllerHolderProtocol) + func remove(holder: PlayerViewControllerHolderProtocol) + func restore(holder: PlayerViewControllerHolderProtocol) async throws + func pipRatePublisher() -> AnyPublisher? + func pauseCurrentPipVideo() +} + +#if DEBUG +public class PipManagerProtocolMock: PipManagerProtocol { + public var isPipActive: Bool { + false + } + + public var isPipPlaying: Bool { + false + } + + public init() {} + public func holder( + for url: URL?, + blockID: String, + courseID: String, + selectedCourseTab: Int + ) -> PlayerViewControllerHolderProtocol? { + return nil + } + public func set(holder: PlayerViewControllerHolderProtocol) {} + public func remove(holder: PlayerViewControllerHolderProtocol) {} + public func restore(holder: PlayerViewControllerHolderProtocol) async throws {} + public func pipRatePublisher() -> AnyPublisher? { nil } + public func pauseCurrentPipVideo() {} +} +#endif diff --git a/Course/Course/Presentation/Video/PlayerControllerProtocol.swift b/Course/Course/Presentation/Video/PlayerControllerProtocol.swift new file mode 100644 index 000000000..df376e466 --- /dev/null +++ b/Course/Course/Presentation/Video/PlayerControllerProtocol.swift @@ -0,0 +1,15 @@ +// +// PlayerControllerProtocol.swift +// Course +// +// Created by Vadim Kuznetsov on 22.04.24. +// + +import Foundation + +public protocol PlayerControllerProtocol { + func play() + func pause() + func seekTo(to date: Date) + func stop() +} diff --git a/Course/Course/Presentation/Video/PlayerDelegateProtocol.swift b/Course/Course/Presentation/Video/PlayerDelegateProtocol.swift new file mode 100644 index 000000000..1297e9ddf --- /dev/null +++ b/Course/Course/Presentation/Video/PlayerDelegateProtocol.swift @@ -0,0 +1,62 @@ +// +// PlayerDelegateProtocol.swift +// Course +// +// Created by Vadim Kuznetsov on 22.04.24. +// + +import AVKit + +public protocol PlayerDelegateProtocol: AVPlayerViewControllerDelegate { + var isPlayingInPip: Bool { get } + var playerHolder: PlayerViewControllerHolderProtocol? { get set } + init(pipManager: PipManagerProtocol) +} + +public class PlayerDelegate: NSObject, PlayerDelegateProtocol { + private(set) public var isPlayingInPip: Bool = false + private let pipManager: PipManagerProtocol + weak public var playerHolder: PlayerViewControllerHolderProtocol? + + required public init(pipManager: PipManagerProtocol) { + self.pipManager = pipManager + super.init() + } + + public func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { + isPlayingInPip = true + if let holder = playerHolder { + pipManager.set(holder: holder) + } + } + + public func playerViewController( + _ playerViewController: AVPlayerViewController, + failedToStartPictureInPictureWithError error: any Error + ) { + isPlayingInPip = false + if let holder = playerHolder { + pipManager.remove(holder: holder) + } + } + + public func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { + isPlayingInPip = false + if let holder = playerHolder { + pipManager.remove(holder: holder) + } + } + + public func playerViewControllerRestoreUserInterfaceForPictureInPictureStop( + _ playerViewController: AVPlayerViewController + ) async -> Bool { + do { + if let holder = playerHolder { + try await pipManager.restore(holder: holder) + } + return true + } catch { + return false + } + } +} diff --git a/Course/Course/Presentation/Video/PlayerServiceProtocol.swift b/Course/Course/Presentation/Video/PlayerServiceProtocol.swift new file mode 100644 index 000000000..3619de512 --- /dev/null +++ b/Course/Course/Presentation/Video/PlayerServiceProtocol.swift @@ -0,0 +1,63 @@ +// +// PlayerServiceProtocol.swift +// Course +// +// Created by Vadim Kuznetsov on 22.04.24. +// + +import SwiftUI + +public protocol PlayerServiceProtocol { + var router: CourseRouter { get } + + init(courseID: String, blockID: String, interactor: CourseInteractorProtocol, router: CourseRouter) + func blockCompletionRequest() async throws + func presentAppReview() + func presentView(transitionStyle: UIModalTransitionStyle, animated: Bool, content: () -> any View) + func getSubtitles(url: String, selectedLanguage: String) async throws -> [Subtitle] +} + +public class PlayerService: PlayerServiceProtocol { + private let courseID: String + private let blockID: String + private let interactor: CourseInteractorProtocol + public let router: CourseRouter + + public required init( + courseID: String, + blockID: String, + interactor: CourseInteractorProtocol, + router: CourseRouter + ) { + self.courseID = courseID + self.blockID = blockID + self.interactor = interactor + self.router = router + } + + @MainActor + public func blockCompletionRequest() async throws { + try await interactor.blockCompletionRequest(courseID: courseID, blockID: blockID) + NotificationCenter.default.post( + name: NSNotification.blockCompletion, + object: nil + ) + } + + @MainActor + public func presentAppReview() { + router.presentAppReview() + } + + @MainActor + public func presentView(transitionStyle: UIModalTransitionStyle, animated: Bool, content: () -> any View) { + router.presentView(transitionStyle: transitionStyle, animated: animated, content: content) + } + + public func getSubtitles(url: String, selectedLanguage: String) async throws -> [Subtitle] { + try await interactor.getSubtitles( + url: url, + selectedLanguage: selectedLanguage + ) + } +} diff --git a/Course/Course/Presentation/Video/PlayerTrackerProtocol.swift b/Course/Course/Presentation/Video/PlayerTrackerProtocol.swift new file mode 100644 index 000000000..775f8e0d6 --- /dev/null +++ b/Course/Course/Presentation/Video/PlayerTrackerProtocol.swift @@ -0,0 +1,324 @@ +// +// PlayerTrackerProtocol.swift +// Course +// +// Created by Vadim Kuznetsov on 22.04.24. +// + +import AVKit +import Combine +import Foundation + +public protocol PlayerTrackerProtocol { + associatedtype Player + var player: Player? { get } + var duration: Double { get } + var progress: Double { get } + var isPlaying: Bool { get } + var isReady: Bool { get } + init(url: URL?) + + func getTimePublisher() -> AnyPublisher + func getRatePublisher() -> AnyPublisher + func getFinishPublisher() -> AnyPublisher + func getReadyPublisher() -> AnyPublisher +} + +#if DEBUG +class PlayerTrackerProtocolMock: PlayerTrackerProtocol { + let player: AVPlayer? + var duration: Double { + 1 + } + var progress: Double { + 0 + } + let isPlaying = false + let isReady = false + private let timePublisher: CurrentValueSubject + private let ratePublisher: CurrentValueSubject + private let finishPublisher: PassthroughSubject + private let readyPublisher: PassthroughSubject + + required init(url: URL?) { + var item: AVPlayerItem? + if let url = url { + item = AVPlayerItem(url: url) + } + self.player = AVPlayer(playerItem: item) + timePublisher = CurrentValueSubject(0) + ratePublisher = CurrentValueSubject(0) + finishPublisher = PassthroughSubject() + readyPublisher = PassthroughSubject() + } + + func getTimePublisher() -> AnyPublisher { + timePublisher.eraseToAnyPublisher() + } + + func getRatePublisher() -> AnyPublisher { + ratePublisher.eraseToAnyPublisher() + } + + func getFinishPublisher() -> AnyPublisher { + finishPublisher.eraseToAnyPublisher() + } + + func getReadyPublisher() -> AnyPublisher { + readyPublisher.eraseToAnyPublisher() + } + + func sendProgress(_ progress: Double) { + timePublisher.send(progress) + } + + func sendFinish() { + finishPublisher.send() + } +} +#endif +// MARK: Video +public class PlayerTracker: PlayerTrackerProtocol { + public var isReady: Bool = false + public let player: AVPlayer? + public var duration: Double { + player?.currentItem?.duration.seconds ?? .nan + } + public var isPlaying: Bool { + (player?.rate ?? 0) > 0 + } + + public var progress: Double { + let currentTime = player?.currentTime().seconds ?? 0 + guard !currentTime.isNaN && !currentTime.isInfinite && duration.isNormal + else { + return 0 + } + + return currentTime/duration + } + + private var cancellations: [AnyCancellable] = [] + private var timeObserver: Any? + private let timePublisher: CurrentValueSubject + private let ratePublisher: CurrentValueSubject + private let finishPublisher: PassthroughSubject + private let readyPublisher: PassthroughSubject + + public required init(url: URL?) { + var item: AVPlayerItem? + if let url = url { + item = AVPlayerItem(url: url) + } + self.player = AVPlayer(playerItem: item) + + var playerTime = player?.currentTime().seconds ?? 0.0 + if playerTime.isNaN == true { + playerTime = 0.0 + } + + timePublisher = CurrentValueSubject(playerTime) + ratePublisher = CurrentValueSubject(player?.rate ?? 0) + finishPublisher = PassthroughSubject() + readyPublisher = PassthroughSubject() + observe() + } + + deinit { + clear() + } + + private func observe() { + let interval = CMTime( + seconds: 0.1, + preferredTimescale: CMTimeScale(NSEC_PER_SEC) + ) + + timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) {[weak self] time in + self?.timePublisher.send(time.seconds) + } + + player?.publisher(for: \.rate) + .sink {[weak self] rate in + self?.ratePublisher.send(rate) + } + .store(in: &cancellations) + + player?.publisher(for: \.status) + .sink {[weak self] status in + guard let strongSelf = self else { return } + strongSelf.isReady = status == .readyToPlay + strongSelf.readyPublisher.send(strongSelf.isReady) + } + .store(in: &cancellations) + + NotificationCenter.default.publisher( + for: AVPlayerItem.didPlayToEndTimeNotification, + object: player?.currentItem + ) + .sink {[weak self] _ in + if self?.player?.currentItem != nil { + self?.finishPublisher.send() + } + } + .store(in: &cancellations) + } + + private func clear() { + if let observer = timeObserver { + player?.removeTimeObserver(observer) + } + cancellations.removeAll() + } + + public func getTimePublisher() -> AnyPublisher { + timePublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + public func getRatePublisher() -> AnyPublisher { + ratePublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + public func getFinishPublisher() -> AnyPublisher { + finishPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + public func getReadyPublisher() -> AnyPublisher { + readyPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } +} + +// MARK: YouTube +import YouTubePlayerKit +public class YoutubePlayerTracker: PlayerTrackerProtocol { + public var isReady: Bool = false + + public let player: YouTubePlayer? + public var duration: Double = 0 + public var isPlaying: Bool { + player?.isPlaying ?? false + } + + public var progress: Double { + timePublisher.value / duration + } + + private var cancellations: [AnyCancellable] = [] + private let timePublisher: CurrentValueSubject + private let ratePublisher: CurrentValueSubject + private let finishPublisher: PassthroughSubject + private let readyPublisher: PassthroughSubject + + public required init(url: URL?) { + if let url = url { + let videoID = url.absoluteString.replacingOccurrences(of: "https://www.youtube.com/watch?v=", with: "") + let configuration = YouTubePlayer.Configuration(configure: { + $0.playInline = true + $0.showFullscreenButton = true + $0.allowsPictureInPictureMediaPlayback = false + $0.showControls = true + $0.useModestBranding = false + $0.progressBarColor = .white + $0.showRelatedVideos = false + $0.showCaptions = false + $0.showAnnotations = false + $0.customUserAgent = """ + Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; ja-jp) + AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5 + """ + }) + self.player = YouTubePlayer(source: .video(id: videoID), configuration: configuration) + self.player?.pause() + } else { + self.player = nil + } + + timePublisher = CurrentValueSubject(0) + ratePublisher = CurrentValueSubject(0) + finishPublisher = PassthroughSubject() + readyPublisher = PassthroughSubject() + observe() + } + + deinit { + clear() + } + + private func observe() { + player?.durationPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] duration in + self?.duration = duration.value + } + .store(in: &cancellations) + + player?.currentTimePublisher(updateInterval: 0.1) + .sink { [weak self] time in + self?.timePublisher.send(time.value) + } + .store(in: &cancellations) + player?.statePublisher + .sink { [weak self] state in + switch state { + case .ready: + self?.isReady = true + self?.readyPublisher.send(true) + default: + self?.isReady = false + self?.readyPublisher.send(false) + } + } + .store(in: &cancellations) + + player?.playbackStatePublisher + .sink { [weak self] state in + guard let strongSelf = self else { return } + switch state { + case .playing: + strongSelf.ratePublisher.send(1) + case .ended: + strongSelf.ratePublisher.send(0) + strongSelf.finishPublisher.send() + default: + strongSelf.ratePublisher.send(0) + } + } + .store(in: &cancellations) + } + + private func clear() { + cancellations.removeAll() + } + + public func getTimePublisher() -> AnyPublisher { + timePublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + public func getRatePublisher() -> AnyPublisher { + ratePublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + public func getFinishPublisher() -> AnyPublisher { + finishPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + public func getReadyPublisher() -> AnyPublisher { + readyPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } +} diff --git a/Course/Course/Presentation/Video/PlayerViewController.swift b/Course/Course/Presentation/Video/PlayerViewController.swift index 0bb477635..573a1195e 100644 --- a/Course/Course/Presentation/Video/PlayerViewController.swift +++ b/Course/Course/Presentation/Video/PlayerViewController.swift @@ -11,131 +11,17 @@ import SwiftUI import _AVKit_SwiftUI struct PlayerViewController: UIViewControllerRepresentable { - - var videoURL: URL? - var videoResolution: CGSize - var playerHolder: PlayerViewControllerHolder - var progress: ((Float) -> Void) - var seconds: ((Double) -> Void) - - init( - videoURL: URL?, - playerHolder: PlayerViewControllerHolder, - bitrate: CGSize, - progress: @escaping ((Float) -> Void), - seconds: @escaping ((Double) -> Void) - ) { - self.videoURL = videoURL - self.playerHolder = playerHolder - self.videoResolution = bitrate - self.progress = progress - self.seconds = seconds - } - + var playerController: AVPlayerViewController + func makeUIViewController(context: Context) -> AVPlayerViewController { - context.coordinator.currentHolder = playerHolder - if playerHolder.isPlayingInPip { - return playerHolder.playerController - } - - let controller = playerHolder.playerController - controller.modalPresentationStyle = .fullScreen - controller.allowsPictureInPicturePlayback = true - controller.canStartPictureInPictureAutomaticallyFromInline = true - let player = AVPlayer() - controller.player = player - context.coordinator.setPlayer(player) { progress, seconds in - self.progress(progress) - self.seconds(seconds) - } - do { try AVAudioSession.sharedInstance().setCategory(.playback) } catch { print(error.localizedDescription) } - return controller - } - - func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) { - let asset = playerController.player?.currentItem?.asset as? AVURLAsset - if asset?.url.absoluteString != videoURL?.absoluteString && !playerHolder.isPlayingInPip { - let player = context.coordinator.player(from: playerController) - player?.replaceCurrentItem(with: AVPlayerItem(url: videoURL!)) - player?.currentItem?.preferredMaximumResolution = videoResolution - - context.coordinator.setPlayer(player) { progress, seconds in - self.progress(progress) - self.seconds(seconds) - } - } - } - - func makeCoordinator() -> Coordinator { - Coordinator() + return playerController } - static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: Coordinator) { - coordinator.setPlayer(nil) { _, _ in } - } - - class Coordinator { - var currentPlayer: AVPlayer? - var observer: Any? - var cancellations: [AnyCancellable] = [] - weak var currentHolder: PlayerViewControllerHolder? - - func player(from playerController: AVPlayerViewController) -> AVPlayer? { - var player = playerController.player - if player == nil { - player = AVPlayer() - player?.allowsExternalPlayback = true - playerController.player = player - } - return player - } - - func setPlayer(_ player: AVPlayer?, currentProgress: @escaping ((Float, Double) -> Void)) { - cancellations.removeAll() - if let observer = observer { - currentPlayer?.removeTimeObserver(observer) - if currentHolder?.isPlayingInPip == false { - currentPlayer?.pause() - } - } - - let interval = CMTime( - seconds: 0.1, - preferredTimescale: CMTimeScale(NSEC_PER_SEC) - ) - - observer = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) {[weak player] time in - var progress: Float = .zero - let currentSeconds = CMTimeGetSeconds(time) - guard let duration = player?.currentItem?.duration else { return } - let totalSeconds = CMTimeGetSeconds(duration) - progress = Float(currentSeconds / totalSeconds) - currentProgress(progress, currentSeconds) - } - - player?.publisher(for: \.rate) - .sink {[weak self] rate in - guard rate > 0 else { return } - self?.currentHolder?.pausePipIfNeed() - } - .store(in: &cancellations) - currentHolder?.pipRatePublisher()? - .sink {[weak self] rate in - guard rate > 0 else { return } - if self?.currentHolder?.isPlayingInPip == false { - self?.currentPlayer?.pause() - } - } - .store(in: &cancellations) - - currentPlayer = player - - } - } + func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {} } diff --git a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift index 3ba64b192..a56e8dfb8 100644 --- a/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift +++ b/Course/Course/Presentation/Video/PlayerViewControllerHolder.swift @@ -7,125 +7,214 @@ import AVKit import Combine -import Swinject -public protocol PipManagerProtocol { - var isPipActive: Bool { get } - - func holder(for url: URL?, blockID: String, courseID: String, selectedCourseTab: Int) -> PlayerViewControllerHolder? - func set(holder: PlayerViewControllerHolder) - func remove(holder: PlayerViewControllerHolder) - func restore(holder: PlayerViewControllerHolder) async throws - func pipRatePublisher() -> AnyPublisher? - func pauseCurrentPipVideo() -} - -#if DEBUG -public class PipManagerProtocolMock: PipManagerProtocol { - public var isPipActive: Bool { - false - } +public protocol PlayerViewControllerHolderProtocol: AnyObject { + var url: URL? { get } + var blockID: String { get } + var courseID: String { get } + var selectedCourseTab: Int { get } + var playerController: PlayerControllerProtocol? { get } + var isPlaying: Bool { get } + var isPlayingInPip: Bool { get } + var isOtherPlayerInPipPlaying: Bool { get } - public init() {} - public func holder( - for url: URL?, + init( + url: URL?, blockID: String, courseID: String, - selectedCourseTab: Int - ) -> PlayerViewControllerHolder? { - return nil - } - public func set(holder: PlayerViewControllerHolder) {} - public func remove(holder: PlayerViewControllerHolder) {} - public func restore(holder: PlayerViewControllerHolder) async throws {} - public func pipRatePublisher() -> AnyPublisher? { nil } - public func pauseCurrentPipVideo() {} + selectedCourseTab: Int, + videoResolution: CGSize, + pipManager: PipManagerProtocol, + playerTracker: any PlayerTrackerProtocol, + playerDelegate: PlayerDelegateProtocol?, + playerService: PlayerServiceProtocol + ) + func getTimePublisher() -> AnyPublisher + func getErrorPublisher() -> AnyPublisher + func getRatePublisher() -> AnyPublisher + func getReadyPublisher() -> AnyPublisher + func getService() -> PlayerServiceProtocol + func sendCompletion() async } -#endif -public class PlayerViewControllerHolder: NSObject, AVPlayerViewControllerDelegate { +public class PlayerViewControllerHolder: PlayerViewControllerHolderProtocol { public let url: URL? public let blockID: String public let courseID: String public let selectedCourseTab: Int - public var isPlayingInPip: Bool = false - public var isOtherPlayerInPip: Bool { + + public var isPlaying: Bool { + playerTracker.isPlaying + } + public var timePublisher: AnyPublisher { + playerTracker.getTimePublisher() + } + + public var isPlayingInPip: Bool { + playerDelegate?.isPlayingInPip ?? false + } + + public var isOtherPlayerInPipPlaying: Bool { let holder = pipManager.holder( for: url, blockID: blockID, courseID: courseID, selectedCourseTab: selectedCourseTab ) - return holder == nil && pipManager.isPipActive + return holder == nil && pipManager.isPipActive && pipManager.isPipPlaying } - - private let pipManager: PipManagerProtocol - - public lazy var playerController: AVPlayerViewController = { + public var duration: Double { + playerTracker.duration + } + private let playerTracker: any PlayerTrackerProtocol + private let playerDelegate: PlayerDelegateProtocol? + private let playerService: PlayerServiceProtocol + private let videoResolution: CGSize + private let errorPublisher = PassthroughSubject() + private var isViewedOnce: Bool = false + private var cancellations: [AnyCancellable] = [] + + let pipManager: PipManagerProtocol + + public lazy var playerController: PlayerControllerProtocol? = { let playerController = AVPlayerViewController() - playerController.delegate = self + playerController.modalPresentationStyle = .fullScreen + playerController.allowsPictureInPicturePlayback = true + playerController.canStartPictureInPictureAutomaticallyFromInline = true + playerController.delegate = playerDelegate + playerController.player = playerTracker.player as? AVPlayer + playerController.player?.currentItem?.preferredMaximumResolution = videoResolution return playerController }() - - public init( + + required public init( url: URL?, blockID: String, courseID: String, - selectedCourseTab: Int + selectedCourseTab: Int, + videoResolution: CGSize, + pipManager: PipManagerProtocol, + playerTracker: any PlayerTrackerProtocol, + playerDelegate: PlayerDelegateProtocol?, + playerService: PlayerServiceProtocol ) { self.url = url self.blockID = blockID self.courseID = courseID self.selectedCourseTab = selectedCourseTab - self.pipManager = Container.shared.resolve(PipManagerProtocol.self)! + self.videoResolution = videoResolution + self.pipManager = pipManager + self.playerTracker = playerTracker + self.playerDelegate = playerDelegate + self.playerService = playerService + addObservers() } - public func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { - isPlayingInPip = true - pipManager.set(holder: self) + private func addObservers() { + timePublisher + .sink {[weak self] _ in + guard let strongSelf = self else { return } + if strongSelf.playerTracker.progress > 0.8 && !strongSelf.isViewedOnce { + strongSelf.isViewedOnce = true + Task { + await strongSelf.sendCompletion() + } + } + } + .store(in: &cancellations) + playerTracker.getFinishPublisher() + .sink { [weak self] in + self?.playerService.presentAppReview() + } + .store(in: &cancellations) + playerTracker.getRatePublisher() + .sink {[weak self] rate in + guard rate > 0 else { return } + self?.pausePipIfNeed() + } + .store(in: &cancellations) + pipManager.pipRatePublisher()? + .sink {[weak self] rate in + guard rate > 0, self?.isPlayingInPip == false else { return } + self?.playerController?.pause() + } + .store(in: &cancellations) + } + + public func pausePipIfNeed() { + if !isPlayingInPip { + pipManager.pauseCurrentPipVideo() + } } - public func playerViewController( - _ playerViewController: AVPlayerViewController, - failedToStartPictureInPictureWithError error: any Error - ) { - isPlayingInPip = false - pipManager.remove(holder: self) + public func getTimePublisher() -> AnyPublisher { + playerTracker.getTimePublisher() + } + + public func getErrorPublisher() -> AnyPublisher { + errorPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + public func getRatePublisher() -> AnyPublisher { + playerTracker.getRatePublisher() } - public func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { - isPlayingInPip = false - pipManager.remove(holder: self) + public func getReadyPublisher() -> AnyPublisher { + playerTracker.getReadyPublisher() + } + + public func getService() -> PlayerServiceProtocol { + playerService } - public func playerViewControllerRestoreUserInterfaceForPictureInPictureStop( - _ playerViewController: AVPlayerViewController - ) async -> Bool { + public func sendCompletion() async { do { - try await Container.shared.resolve(PipManagerProtocol.self)?.restore(holder: self) - return true + try await playerService.blockCompletionRequest() } catch { - return false + errorPublisher.send(error) } } +} + +extension AVPlayerViewController: PlayerControllerProtocol { + public func play() { + player?.play() + } - public override func isEqual(_ object: Any?) -> Bool { - guard let object = object as? PlayerViewControllerHolder else { - return false - } - return url?.absoluteString == object.url?.absoluteString && - courseID == object.courseID && - blockID == object.blockID && - selectedCourseTab == object.selectedCourseTab + public func pause() { + player?.pause() } - public func pausePipIfNeed() { - if !isPlayingInPip { - pipManager.pauseCurrentPipVideo() - } + public func seekTo(to date: Date) { + player?.seek(to: date) } - public func pipRatePublisher() -> AnyPublisher? { - pipManager.pipRatePublisher() + public func stop() { + player?.replaceCurrentItem(with: nil) } } + +#if DEBUG +extension PlayerViewControllerHolder { + static var mock: PlayerViewControllerHolder { + PlayerViewControllerHolder( + url: URL(string: "")!, + blockID: "", + courseID: "", + selectedCourseTab: 0, + videoResolution: .zero, + pipManager: PipManagerProtocolMock(), + playerTracker: PlayerTrackerProtocolMock(url: URL(string: "")), + playerDelegate: nil, + playerService: PlayerService( + courseID: "", + blockID: "", + interactor: CourseInteractor.mock, + router: CourseRouterMock() + ) + ) + } +} +#endif diff --git a/Course/Course/Presentation/Video/SubtittlesView.swift b/Course/Course/Presentation/Video/SubtitlesView.swift similarity index 92% rename from Course/Course/Presentation/Video/SubtittlesView.swift rename to Course/Course/Presentation/Video/SubtitlesView.swift index fb38221cc..ef68d69b9 100644 --- a/Course/Course/Presentation/Video/SubtittlesView.swift +++ b/Course/Course/Presentation/Video/SubtitlesView.swift @@ -1,5 +1,5 @@ // -// SubtittlesView.swift +// SubtitlesView.swift // Course // // Created by  Stepanok Ivan on 04.04.2023. @@ -15,9 +15,9 @@ public struct Subtitle { var text: String } -public struct SubtittlesView: View { +public struct SubtitlesView: View { - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal @ObservedObject private var viewModel: VideoPlayerViewModel @@ -113,20 +113,19 @@ public struct SubtittlesView: View { } #if DEBUG +import Combine struct SubtittlesView_Previews: PreviewProvider { static var previews: some View { - SubtittlesView( + SubtitlesView( languages: [SubtitleUrl(language: "fr", url: "url"), SubtitleUrl(language: "uk", url: "url2")], currentTime: .constant(0), viewModel: VideoPlayerViewModel( - blockID: "", courseID: "", languages: [], - interactor: CourseInteractor(repository: CourseRepositoryMock()), - router: CourseRouterMock(), - appStorage: CoreStorageMock(), - connectivity: Connectivity() + playerStateSubject: CurrentValueSubject(nil), + connectivity: Connectivity(), + playerHolder: PlayerViewControllerHolder.mock ), scrollTo: {_ in } ) } diff --git a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift index 27b214068..c35f9df83 100644 --- a/Course/Course/Presentation/Video/VideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/VideoPlayerViewModel.swift @@ -7,17 +7,16 @@ import Foundation import Core +import OEXFoundation import _AVKit_SwiftUI +import Combine public class VideoPlayerViewModel: ObservableObject { - - private var blockID: String - private var courseID: String + @Published var pause: Bool = false + @Published var currentTime: Double = 0 + @Published var isLoading: Bool = true - private let interactor: CourseInteractorProtocol public let connectivity: ConnectivityProtocol - public let router: CourseRouter - public let appStorage: CoreStorage private var subtitlesDownloaded: Bool = false @Published var subtitles: [Subtitle] = [] @@ -31,53 +30,81 @@ public class VideoPlayerViewModel: ObservableObject { showError = errorMessage != nil } } + var isPlayingInPip: Bool { + playerHolder.isPlayingInPip + } + + var isOtherPlayerInPip: Bool { + playerHolder.isOtherPlayerInPipPlaying + } + public let playerHolder: PlayerViewControllerHolderProtocol + internal var subscription = Set() public init( - blockID: String, - courseID: String, languages: [SubtitleUrl], - interactor: CourseInteractorProtocol, - router: CourseRouter, - appStorage: CoreStorage, - connectivity: ConnectivityProtocol + playerStateSubject: CurrentValueSubject? = nil, + connectivity: ConnectivityProtocol, + playerHolder: PlayerViewControllerHolderProtocol ) { - self.blockID = blockID - self.courseID = courseID self.languages = languages - self.interactor = interactor - self.router = router - self.appStorage = appStorage self.connectivity = connectivity + self.playerHolder = playerHolder self.prepareLanguages() + + observePlayer(with: playerStateSubject) } - @MainActor - func blockCompletionRequest() async { - do { - try await interactor.blockCompletionRequest(courseID: courseID, blockID: blockID) - NotificationCenter.default.post( - name: NSNotification.blockCompletion, - object: nil - ) - } catch let error { - if error.isInternetError || error is NoCachedDataError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError + func observePlayer(with playerStateSubject: CurrentValueSubject?) { + playerStateSubject?.sink { [weak self] state in + switch state { + case .pause: + if self?.playerHolder.isPlayingInPip != true { + self?.playerHolder.playerController?.pause() + } + case .kill: + if self?.playerHolder.isPlayingInPip != true { + self?.playerHolder.playerController?.stop() + } + case .none: + break } } + .store(in: &subscription) + + playerHolder.getTimePublisher() + .sink {[weak self] time in + self?.currentTime = time + } + .store(in: &subscription) + playerHolder.getErrorPublisher() + .sink {[weak self] error in + if error.isInternetError || error is NoCachedDataError { + self?.errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + self?.errorMessage = CoreLocalization.Error.unknownError + } + } + .store(in: &subscription) + playerHolder.getReadyPublisher() + .sink {[weak self] isReady in + guard isReady else { return } + self?.isLoading = false + } + .store(in: &subscription) + } @MainActor public func getSubtitles(subtitlesUrl: String) async { do { - let result = try await interactor.getSubtitles( + let result = try await playerHolder.getService().getSubtitles( url: subtitlesUrl, selectedLanguage: self.selectedLanguage ?? "en" ) + subtitles = result } catch { - print(">>>>> ⛔️⛔️⛔️⛔️⛔️⛔️⛔️⛔️", error) + debugLog(">>>>> ⛔️⛔️⛔️⛔️⛔️⛔️⛔️⛔️", error) } } @@ -94,6 +121,13 @@ public class VideoPlayerViewModel: ObservableObject { return locale.localizedString(forLanguageCode: code)?.capitalized ?? "" } + func pauseScrolling() { + pause = true + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.pause = false + } + } + private func generateLanguageItems() { items = languages.map { language in let name = generateLanguageName(code: language.language) @@ -133,7 +167,9 @@ public class VideoPlayerViewModel: ObservableObject { } func presentPicker() { - router.presentView( + let service = playerHolder.getService() + let router = service.router + service.presentView( transitionStyle: .crossDissolve, animated: true ) { diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift index 631e11a7c..2374a4f14 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayer.swift @@ -56,10 +56,10 @@ public struct YouTubeVideoPlayer: View { } } ZStack { - SubtittlesView( + SubtitlesView( languages: viewModel.languages, currentTime: $viewModel.currentTime, - viewModel: viewModel, + viewModel: viewModel, scrollTo: { date in viewModel.youtubePlayer.seek( to: Measurement(value: date.secondsSinceMidnight(), unit: UnitDuration.seconds), @@ -86,16 +86,10 @@ struct YouTubeVideoPlayer_Previews: PreviewProvider { static var previews: some View { YouTubeVideoPlayer( viewModel: YouTubeVideoPlayerViewModel( - url: "", - blockID: "", - courseID: "", languages: [], playerStateSubject: CurrentValueSubject(nil), - interactor: CourseInteractor(repository: CourseRepositoryMock()), - router: CourseRouterMock(), - appStorage: CoreStorageMock(), connectivity: Connectivity(), - pipManager: PipManagerProtocolMock() + playerHolder: YoutubePlayerViewControllerHolder.mock ), isOnScreen: true ) diff --git a/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift index 077a1e0e3..acaacde23 100644 --- a/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift +++ b/Course/Course/Presentation/Video/YouTubeVideoPlayerViewModel.swift @@ -13,138 +13,7 @@ import Swinject public class YouTubeVideoPlayerViewModel: VideoPlayerViewModel { - @Published var youtubePlayer: YouTubePlayer - private (set) var play = false - @Published var isLoading: Bool = true - @Published var currentTime: Double = 0 - @Published var pause: Bool = false - - private var subscription = Set() - private var duration: Double? - private var isViewedOnce: Bool = false - private var url: String - private let pipManager: PipManagerProtocol - - public init( - url: String, - blockID: String, - courseID: String, - languages: [SubtitleUrl], - playerStateSubject: CurrentValueSubject, - interactor: CourseInteractorProtocol, - router: CourseRouter, - appStorage: CoreStorage, - connectivity: ConnectivityProtocol, - pipManager: PipManagerProtocol - ) { - self.url = url - - let videoID = url.replacingOccurrences(of: "https://www.youtube.com/watch?v=", with: "") - let configuration = YouTubePlayer.Configuration(configure: { - $0.autoPlay = !pipManager.isPipActive - $0.playInline = true - $0.showFullscreenButton = true - $0.allowsPictureInPictureMediaPlayback = false - $0.showControls = true - $0.useModestBranding = false - $0.progressBarColor = .white - $0.showRelatedVideos = false - $0.showCaptions = false - $0.showAnnotations = false - $0.customUserAgent = """ - Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; ja-jp) - AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5 - """ - }) - self.youtubePlayer = YouTubePlayer(source: .video(id: videoID), configuration: configuration) - self.pipManager = pipManager - super.init( - blockID: blockID, - courseID: courseID, - languages: languages, - interactor: interactor, - router: router, - appStorage: appStorage, - connectivity: connectivity - ) - - self.youtubePlayer.pause() - - subscrube(playerStateSubject: playerStateSubject) - } - - func pauseScrolling() { - pause = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - self.pause = false - } - } - - private func subscrube(playerStateSubject: CurrentValueSubject) { - playerStateSubject.sink(receiveValue: { [weak self] state in - switch state { - case .pause: - self?.youtubePlayer.stop() - case .kill, .none: - break - } - }).store(in: &subscription) - - youtubePlayer.durationPublisher.sink(receiveValue: { [weak self] duration in - self?.duration = duration.value - }).store(in: &subscription) - - youtubePlayer.currentTimePublisher(updateInterval: 0.1).sink(receiveValue: { [weak self] time in - guard let self else { return } - if !self.pause { - self.currentTime = time.value - } - - if let duration = self.duration { - if (time.value / duration) >= 0.8 { - if !isViewedOnce { - Task { - await self.blockCompletionRequest() - - } - isViewedOnce = true - } - } - if (time.value / duration) >= 0.999 { - self.router.presentAppReview() - } - } - }).store(in: &subscription) - - youtubePlayer.playbackStatePublisher.sink(receiveValue: { [weak self] state in - guard let self else { return } - switch state { - case .unstarted: - self.play = false - case .ended: - self.play = false - case .playing: - self.play = true - self.pipManager.pauseCurrentPipVideo() - case .paused: - self.play = false - case .buffering, .cued: - break - } - }).store(in: &subscription) - - youtubePlayer.statePublisher.sink(receiveValue: { [weak self] state in - guard let self else { return } - if state == .ready { - self.isLoading = false - } - }).store(in: &subscription) - - pipManager.pipRatePublisher()? - .sink {[weak self] rate in - guard rate > 0 else { return } - self?.youtubePlayer.pause() - } - .store(in: &subscription) + var youtubePlayer: YouTubePlayer { + (playerHolder.playerController as? YouTubePlayer) ?? YouTubePlayer() } } diff --git a/Course/Course/Presentation/Video/YoutubePlayerViewControllerHolder.swift b/Course/Course/Presentation/Video/YoutubePlayerViewControllerHolder.swift new file mode 100644 index 000000000..16a4d9eca --- /dev/null +++ b/Course/Course/Presentation/Video/YoutubePlayerViewControllerHolder.swift @@ -0,0 +1,185 @@ +// +// YoutubePlayerViewControllerHolder.swift +// Course +// +// Created by Vadim Kuznetsov on 22.04.24. +// + +import Combine +import Foundation +import YouTubePlayerKit + +public class YoutubePlayerViewControllerHolder: PlayerViewControllerHolderProtocol { + public let url: URL? + public let blockID: String + public let courseID: String + public let selectedCourseTab: Int + + public var isPlaying: Bool { + playerTracker.isPlaying + } + public var timePublisher: AnyPublisher { + playerTracker.getTimePublisher() + } + + public let isPlayingInPip: Bool = false + + public var isOtherPlayerInPipPlaying: Bool { + pipManager.isPipActive && pipManager.isPipPlaying + } + + public var duration: Double { + playerTracker.duration + } + private let playerTracker: any PlayerTrackerProtocol + private let playerService: PlayerServiceProtocol + private let videoResolution: CGSize + private let errorPublisher = PassthroughSubject() + private var isViewedOnce: Bool = false + private var cancellations: [AnyCancellable] = [] + + let pipManager: PipManagerProtocol + + public var playerController: PlayerControllerProtocol? { + playerTracker.player as? YouTubePlayer + } + + required public init( + url: URL?, + blockID: String, + courseID: String, + selectedCourseTab: Int, + videoResolution: CGSize, + pipManager: PipManagerProtocol, + playerTracker: any PlayerTrackerProtocol, + playerDelegate: PlayerDelegateProtocol?, + playerService: PlayerServiceProtocol + ) { + self.url = url + self.blockID = blockID + self.courseID = courseID + self.selectedCourseTab = selectedCourseTab + self.videoResolution = videoResolution + self.pipManager = pipManager + self.playerTracker = playerTracker + self.playerService = playerService + let youtubePlayer = playerTracker.player as? YouTubePlayer + var configuration = youtubePlayer?.configuration + configuration?.autoPlay = !pipManager.isPipActive + if let configuration = configuration { + youtubePlayer?.update(configuration: configuration) + } + addObservers() + } + + private func addObservers() { + timePublisher + .sink {[weak self] _ in + guard let strongSelf = self else { return } + if strongSelf.playerTracker.progress > 0.8 && !strongSelf.isViewedOnce { + strongSelf.isViewedOnce = true + Task { + await strongSelf.sendCompletion() + } + } + } + .store(in: &cancellations) + playerTracker.getFinishPublisher() + .sink { [weak self] in + self?.playerService.presentAppReview() + } + .store(in: &cancellations) + playerTracker.getRatePublisher() + .sink {[weak self] rate in + guard rate > 0 else { return } + self?.pausePipIfNeed() + } + .store(in: &cancellations) + pipManager.pipRatePublisher()? + .sink {[weak self] rate in + guard rate > 0, self?.isPlayingInPip == false else { return } + self?.playerController?.pause() + } + .store(in: &cancellations) + } + + public func pausePipIfNeed() { + if !isPlayingInPip { + pipManager.pauseCurrentPipVideo() + } + } + + public func getTimePublisher() -> AnyPublisher { + playerTracker.getTimePublisher() + } + + public func getErrorPublisher() -> AnyPublisher { + errorPublisher + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() + } + + public func getRatePublisher() -> AnyPublisher { + playerTracker.getRatePublisher() + } + + public func getReadyPublisher() -> AnyPublisher { + playerTracker.getReadyPublisher() + } + + public func getService() -> PlayerServiceProtocol { + playerService + } + + public func sendCompletion() async { + do { + try await playerService.blockCompletionRequest() + } catch { + errorPublisher.send(error) + } + } +} + +extension YouTubePlayer: PlayerControllerProtocol { + public func play() { + self.play(completion: nil) + } + + public func pause() { + self.pause(completion: nil) + } + + public func seekTo(to date: Date) { + self.seek( + to: Measurement(value: date.secondsSinceMidnight(), unit: UnitDuration.seconds), + allowSeekAhead: true + ) + } + + public func stop() { + self.stop(completion: nil) + } +} + +#if DEBUG +extension YoutubePlayerViewControllerHolder { + static var mock: YoutubePlayerViewControllerHolder { + YoutubePlayerViewControllerHolder( + url: URL(string: "")!, + blockID: "", + courseID: "", + selectedCourseTab: 0, + videoResolution: .zero, + pipManager: PipManagerProtocolMock(), + playerTracker: PlayerTrackerProtocolMock(url: URL(string: "")), + playerDelegate: nil, + playerService: PlayerService( + courseID: "", + blockID: "", + interactor: CourseInteractor.mock, + router: CourseRouterMock() + ) + ) + } +} +#endif diff --git a/Course/Course/SwiftGen/Strings.swift b/Course/Course/SwiftGen/Strings.swift index a66bfae07..84693e4a3 100644 --- a/Course/Course/SwiftGen/Strings.swift +++ b/Course/Course/SwiftGen/Strings.swift @@ -10,6 +10,14 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum CourseLocalization { + /// Plural format key: "%#@due_in@" + public static func dueIn(_ p1: Int) -> String { + return CourseLocalization.tr("Localizable", "due_in", p1, fallback: "Plural format key: \"%#@due_in@\"") + } + /// Plural format key: "%#@past_due@" + public static func pastDue(_ p1: Int) -> String { + return CourseLocalization.tr("Localizable", "past_due", p1, fallback: "Plural format key: \"%#@past_due@\"") + } public enum Accessibility { /// Cancel download public static let cancelDownload = CourseLocalization.tr("Localizable", "ACCESSIBILITY.CANCEL_DOWNLOAD", fallback: "Cancel download") @@ -29,6 +37,122 @@ public enum CourseLocalization { public static let rotateDevice = CourseLocalization.tr("Localizable", "ALERT.ROTATE_DEVICE", fallback: "Rotate your device to view this video in full screen.") /// Turning off the switch will stop downloading and delete all downloaded videos for public static let stopDownloading = CourseLocalization.tr("Localizable", "ALERT.STOP_DOWNLOADING", fallback: "Turning off the switch will stop downloading and delete all downloaded videos for") + /// Warning + public static let warning = CourseLocalization.tr("Localizable", "ALERT.WARNING", fallback: "Warning") + } + public enum CalendarSyncStatus { + /// Calendar Sync Failed + public static let failed = CourseLocalization.tr("Localizable", "CALENDAR_SYNC_STATUS.FAILED", fallback: "Calendar Sync Failed") + /// Offline + public static let offline = CourseLocalization.tr("Localizable", "CALENDAR_SYNC_STATUS.OFFLINE", fallback: "Offline") + /// Synced to Calendar + public static let synced = CourseLocalization.tr("Localizable", "CALENDAR_SYNC_STATUS.SYNCED", fallback: "Synced to Calendar") + } + public enum Course { + /// Due Today + public static let dueToday = CourseLocalization.tr("Localizable", "COURSE.DUE_TODAY", fallback: "Due Today") + /// Due Tomorrow + public static let dueTomorrow = CourseLocalization.tr("Localizable", "COURSE.DUE_TOMORROW", fallback: "Due Tomorrow") + /// %@ of %@ assignments complete + public static func progressCompleted(_ p1: Any, _ p2: Any) -> String { + return CourseLocalization.tr("Localizable", "COURSE.PROGRESS_COMPLETED", String(describing: p1), String(describing: p2), fallback: "%@ of %@ assignments complete") + } + public enum Alert { + /// Cancel + public static let cancel = CourseLocalization.tr("Localizable", "COURSE.ALERT.CANCEL", fallback: "Cancel") + /// Close + public static let close = CourseLocalization.tr("Localizable", "COURSE.ALERT.CLOSE", fallback: "Close") + /// Downloading this content will use %@ of cellular data. + public static func confirmDownloadCellularDescription(_ p1: Any) -> String { + return CourseLocalization.tr("Localizable", "COURSE.ALERT.CONFIRM_DOWNLOAD_CELLULAR_DESCRIPTION", String(describing: p1), fallback: "Downloading this content will use %@ of cellular data.") + } + /// Download on Cellular? + public static let confirmDownloadCellularTitle = CourseLocalization.tr("Localizable", "COURSE.ALERT.CONFIRM_DOWNLOAD_CELLULAR_TITLE", fallback: "Download on Cellular?") + /// Downloading this %@ of content will save available blocks offline. + public static func confirmDownloadDescription(_ p1: Any) -> String { + return CourseLocalization.tr("Localizable", "COURSE.ALERT.CONFIRM_DOWNLOAD_DESCRIPTION", String(describing: p1), fallback: "Downloading this %@ of content will save available blocks offline.") + } + /// Confirm Download + public static let confirmDownloadTitle = CourseLocalization.tr("Localizable", "COURSE.ALERT.CONFIRM_DOWNLOAD_TITLE", fallback: "Confirm Download") + /// Download + public static let download = CourseLocalization.tr("Localizable", "COURSE.ALERT.DOWNLOAD", fallback: "Download") + /// Remove + public static let remove = CourseLocalization.tr("Localizable", "COURSE.ALERT.REMOVE", fallback: "Remove") + /// Removing this content will free up %@. + public static func removeDescription(_ p1: Any) -> String { + return CourseLocalization.tr("Localizable", "COURSE.ALERT.REMOVE_DESCRIPTION", String(describing: p1), fallback: "Removing this content will free up %@.") + } + /// Remove Offline Content? + public static let removeTitle = CourseLocalization.tr("Localizable", "COURSE.ALERT.REMOVE_TITLE", fallback: "Remove Offline Content?") + /// Try again + public static let tryAgain = CourseLocalization.tr("Localizable", "COURSE.ALERT.TRY_AGAIN", fallback: "Try again") + } + public enum Error { + /// Unfortunately, this content failed to download. Please try again later or report this issue. + public static let downloadFailedDescription = CourseLocalization.tr("Localizable", "COURSE.ERROR.DOWNLOAD_FAILED_DESCRIPTION", fallback: "Unfortunately, this content failed to download. Please try again later or report this issue.") + /// Download Failed + public static let downloadFailedTitle = CourseLocalization.tr("Localizable", "COURSE.ERROR.DOWNLOAD_FAILED_TITLE", fallback: "Download Failed") + /// Downloading this content requires an active internet connection. Please connect to the internet and try again. + public static let noInternetConnectionDescription = CourseLocalization.tr("Localizable", "COURSE.ERROR.NO_INTERNET_CONNECTION_DESCRIPTION", fallback: "Downloading this content requires an active internet connection. Please connect to the internet and try again.") + /// No Internet Connection + public static let noInternetConnectionTitle = CourseLocalization.tr("Localizable", "COURSE.ERROR.NO_INTERNET_CONNECTION_TITLE", fallback: "No Internet Connection") + /// Downloading this content requires an active WiFi connection. Please connect to a WiFi network and try again. + public static let wifiRequiredDescription = CourseLocalization.tr("Localizable", "COURSE.ERROR.WIFI_REQUIRED_DESCRIPTION", fallback: "Downloading this content requires an active WiFi connection. Please connect to a WiFi network and try again.") + /// Wi-Fi Required + public static let wifiRequiredTitle = CourseLocalization.tr("Localizable", "COURSE.ERROR.WIFI_REQUIRED_TITLE", fallback: "Wi-Fi Required") + } + public enum LargestDownloads { + /// Done + public static let done = CourseLocalization.tr("Localizable", "COURSE.LARGEST_DOWNLOADS.DONE", fallback: "Done") + /// Edit + public static let edit = CourseLocalization.tr("Localizable", "COURSE.LARGEST_DOWNLOADS.EDIT", fallback: "Edit") + /// Remove all downloads + public static let removeDownloads = CourseLocalization.tr("Localizable", "COURSE.LARGEST_DOWNLOADS.REMOVE_DOWNLOADS", fallback: "Remove all downloads") + /// Largest Downloads + public static let title = CourseLocalization.tr("Localizable", "COURSE.LARGEST_DOWNLOADS.TITLE", fallback: "Largest Downloads") + } + public enum Offline { + /// %@%% of this course can be completed offline. + public static func canBeCompleted(_ p1: Any) -> String { + return CourseLocalization.tr("Localizable", "COURSE.OFFLINE.CAN_BE_COMPLETED", String(describing: p1), fallback: "%@%% of this course can be completed offline.") + } + /// Cancel Course Download + public static let cancelCourseDownload = CourseLocalization.tr("Localizable", "COURSE.OFFLINE.CANCEL_COURSE_DOWNLOAD", fallback: "Cancel Course Download") + /// Download all + public static let downloadAll = CourseLocalization.tr("Localizable", "COURSE.OFFLINE.DOWNLOAD_ALL", fallback: "Download all") + /// %@%% of this course is downloadable. + public static func downloadable(_ p1: Any) -> String { + return CourseLocalization.tr("Localizable", "COURSE.OFFLINE.DOWNLOADABLE", String(describing: p1), fallback: "%@%% of this course is downloadable.") + } + /// %@%% of this course is visible on mobile. + public static func visible(_ p1: Any) -> String { + return CourseLocalization.tr("Localizable", "COURSE.OFFLINE.VISIBLE", String(describing: p1), fallback: "%@%% of this course is visible on mobile.") + } + /// You can download course content offline to learn on the go, without requiring an active internet connection or using mobile data. + public static let youCanDownload = CourseLocalization.tr("Localizable", "COURSE.OFFLINE.YOU_CAN_DOWNLOAD", fallback: "You can download course content offline to learn on the go, without requiring an active internet connection or using mobile data.") + /// None of this course’s content is currently avaliable to download offline. + public static let youCantDownload = CourseLocalization.tr("Localizable", "COURSE.OFFLINE.YOU_CANT_DOWNLOAD", fallback: "None of this course’s content is currently avaliable to download offline.") + } + public enum StorageAlert { + /// Your device does not have enough free space to download this content. Please free up some space and try again. + public static let description = CourseLocalization.tr("Localizable", "COURSE.STORAGE_ALERT.DESCRIPTION", fallback: "Your device does not have enough free space to download this content. Please free up some space and try again.") + /// Device Storage Full + public static let title = CourseLocalization.tr("Localizable", "COURSE.STORAGE_ALERT.TITLE", fallback: "Device Storage Full") + /// %@ used, %@ free + public static func usedAndFree(_ p1: Any, _ p2: Any) -> String { + return CourseLocalization.tr("Localizable", "COURSE.STORAGE_ALERT.USED_AND_FREE", String(describing: p1), String(describing: p2), fallback: "%@ used, %@ free") + } + } + public enum TotalProgress { + /// Available to Download + public static let avaliableToDownload = CourseLocalization.tr("Localizable", "COURSE.TOTAL_PROGRESS.AVALIABLE_TO_DOWNLOAD", fallback: "Available to Download") + /// Downloaded + public static let downloaded = CourseLocalization.tr("Localizable", "COURSE.TOTAL_PROGRESS.DOWNLOADED", fallback: "Downloaded") + /// Downloading + public static let downloading = CourseLocalization.tr("Localizable", "COURSE.TOTAL_PROGRESS.DOWNLOADING", fallback: "Downloading") + /// Ready to Download + public static let readyToDownload = CourseLocalization.tr("Localizable", "COURSE.TOTAL_PROGRESS.READY_TO_DOWNLOAD", fallback: "Ready to Download") + } } public enum Courseware { /// Back to outline @@ -65,6 +189,8 @@ public enum CourseLocalization { public static let handoutsInDeveloping = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.HANDOUTS_IN_DEVELOPING", fallback: "Handouts In developing") /// Home public static let home = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.HOME", fallback: "Home") + /// Offline + public static let offline = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.OFFLINE", fallback: "Offline") /// Videos public static let videos = CourseLocalization.tr("Localizable", "COURSE_CONTAINER.VIDEOS", fallback: "Videos") } @@ -145,36 +271,6 @@ public enum CourseLocalization { public static let successMessage = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.SUCCESS_MESSAGE", fallback: "Your dates have been successfully shifted.") /// Course Dates public static let title = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.TITLE", fallback: "Course Dates") - public enum ResetDateBanner { - /// Don't worry - shift our suggested schedule to complete past due assignments without losing any progress. - public static let body = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.BODY", fallback: "Don't worry - shift our suggested schedule to complete past due assignments without losing any progress.") - /// Shift due dates - public static let button = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.BUTTON", fallback: "Shift due dates") - /// Missed some deadlines? - public static let header = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.HEADER", fallback: "Missed some deadlines?") - } - public enum TabInfoBanner { - /// We built a suggested schedule to help you stay on track. But don’t worry – it’s flexible so you can learn at your own pace. If you happen to fall behind, you’ll be able to adjust the dates to keep yourself on track. - public static let body = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.TAB_INFO_BANNER.BODY", fallback: "We built a suggested schedule to help you stay on track. But don’t worry – it’s flexible so you can learn at your own pace. If you happen to fall behind, you’ll be able to adjust the dates to keep yourself on track.") - /// - public static let header = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.TAB_INFO_BANNER.HEADER", fallback: "") - } - public enum UpgradeToCompleteGradedBanner { - /// To complete graded assignments as part of this course, you can upgrade today. - public static let body = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.BODY", fallback: "To complete graded assignments as part of this course, you can upgrade today.") - /// - public static let button = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.BUTTON", fallback: "") - /// - public static let header = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.HEADER", fallback: "") - } - public enum UpgradeToResetBanner { - /// You are auditing this course, which means that you are unable to participate in graded assignments. It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today. - public static let body = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.BODY", fallback: "You are auditing this course, which means that you are unable to participate in graded assignments. It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today.") - /// - public static let button = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.BUTTON", fallback: "") - /// - public static let header = CourseLocalization.tr("Localizable", "COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.HEADER", fallback: "") - } } } public enum Download { @@ -205,14 +301,22 @@ public enum CourseLocalization { public static let videos = CourseLocalization.tr("Localizable", "DOWNLOAD.VIDEOS", fallback: "Videos") } public enum Error { + /// There are currently no announcements for this course. + public static let announcementsUnavailable = CourseLocalization.tr("Localizable", "ERROR.ANNOUNCEMENTS_UNAVAILABLE", fallback: "There are currently no announcements for this course.") /// Course component not found, please reload public static let componentNotFount = CourseLocalization.tr("Localizable", "ERROR.COMPONENT_NOT_FOUNT", fallback: "Course component not found, please reload") - /// There are currently no handouts for this course - public static let noHandouts = CourseLocalization.tr("Localizable", "ERROR.NO_HANDOUTS", fallback: "There are currently no handouts for this course") + /// Course dates are not currently available. + public static let courseDateUnavailable = CourseLocalization.tr("Localizable", "ERROR.COURSE_DATE_UNAVAILABLE", fallback: "Course dates are not currently available.") + /// No course content is currently available. + public static let coursewareUnavailable = CourseLocalization.tr("Localizable", "ERROR.COURSEWARE_UNAVAILABLE", fallback: "No course content is currently available.") + /// There are currently no handouts for this course. + public static let handoutsUnavailable = CourseLocalization.tr("Localizable", "ERROR.HANDOUTS_UNAVAILABLE", fallback: "There are currently no handouts for this course.") /// You are not connected to the Internet. Please check your Internet connection. public static let noInternet = CourseLocalization.tr("Localizable", "ERROR.NO_INTERNET", fallback: "You are not connected to the Internet. Please check your Internet connection.") /// Reload public static let reload = CourseLocalization.tr("Localizable", "ERROR.RELOAD", fallback: "Reload") + /// There are currently no vidoes for this course. + public static let videosUnavailable = CourseLocalization.tr("Localizable", "ERROR.VIDEOS_UNAVAILABLE", fallback: "There are currently no vidoes for this course.") } public enum HandoutsCellAnnouncements { /// Keep up with the latest news @@ -234,6 +338,20 @@ public enum CourseLocalization { /// This interactive component isn't available on mobile public static let title = CourseLocalization.tr("Localizable", "NOT_AVALIABLE.TITLE", fallback: "This interactive component isn't available on mobile") } + public enum Offline { + public enum NotAvaliable { + /// Explore other parts of this course or view this when you reconnect. + public static let description = CourseLocalization.tr("Localizable", "OFFLINE.NOT_AVALIABLE.DESCRIPTION", fallback: "Explore other parts of this course or view this when you reconnect.") + /// This component is not yet available offline + public static let title = CourseLocalization.tr("Localizable", "OFFLINE.NOT_AVALIABLE.TITLE", fallback: "This component is not yet available offline") + } + public enum NotDownloaded { + /// Explore other parts of this course or download this when you reconnect. + public static let description = CourseLocalization.tr("Localizable", "OFFLINE.NOT_DOWNLOADED.DESCRIPTION", fallback: "Explore other parts of this course or download this when you reconnect.") + /// This component is not downloaded + public static let title = CourseLocalization.tr("Localizable", "OFFLINE.NOT_DOWNLOADED.TITLE", fallback: "This component is not downloaded") + } + } public enum Outline { /// Certificate public static let certificate = CourseLocalization.tr("Localizable", "OUTLINE.CERTIFICATE", fallback: "Certificate") diff --git a/Course/Course/en.lproj/Localizable.strings b/Course/Course/en.lproj/Localizable.strings index 90c6472f3..3fe024cb8 100644 --- a/Course/Course/en.lproj/Localizable.strings +++ b/Course/Course/en.lproj/Localizable.strings @@ -27,16 +27,22 @@ "ERROR.NO_INTERNET" = "You are not connected to the Internet. Please check your Internet connection."; "ERROR.RELOAD" = "Reload"; "ERROR.COMPONENT_NOT_FOUNT" = "Course component not found, please reload"; -"ERROR.NO_HANDOUTS" = "There are currently no handouts for this course"; +"ERROR.HANDOUTS_UNAVAILABLE" = "There are currently no handouts for this course."; +"ERROR.ANNOUNCEMENTS_UNAVAILABLE" = "There are currently no announcements for this course."; +"ERROR.VIDEOS_UNAVAILABLE" = "There are currently no vidoes for this course."; +"ERROR.COURSE_DATE_UNAVAILABLE" = "Course dates are not currently available."; +"ERROR.COURSEWARE_UNAVAILABLE" = "No course content is currently available."; "ALERT.ROTATE_DEVICE" = "Rotate your device to view this video in full screen."; "ALERT.ACCEPT" = "Accept"; "ALERT.DELETE_ALL_VIDEOS" = "Are you sure you want to delete all video(s) for"; "ALERT.DELETE_VIDEOS" = "Are you sure you want to delete video(s) for"; "ALERT.STOP_DOWNLOADING" = "Turning off the switch will stop downloading and delete all downloaded videos for"; +"ALERT.WARNING" = "Warning"; "COURSE_CONTAINER.HOME" = "Home"; "COURSE_CONTAINER.VIDEOS" = "Videos"; +"COURSE_CONTAINER.OFFLINE" = "Offline"; "COURSE_CONTAINER.DATES" = "Dates"; "COURSE_CONTAINER.DISCUSSIONS" = "Discussions"; "COURSE_CONTAINER.HANDOUTS" = "More"; @@ -102,21 +108,67 @@ "COURSE_DATES.OPEN_SETTINGS"="Open Settings"; "COURSE_DATES.SETTINGS" = "Settings"; -"COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.BODY" = "Don't worry - shift our suggested schedule to complete past due assignments without losing any progress."; -"COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.BUTTON" = "Shift due dates"; -"COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.HEADER" = "Missed some deadlines?"; +"COURSE_DATES.RESET_DATE.ERROR_MESSAGE" = "Your dates could not be shifted. Please try again."; +"COURSE_DATES.RESET_DATE.SUCCESS_MESSAGE" = "Your dates have been successfully shifted."; +"COURSE_DATES.RESET_DATE.TITLE" = "Course Dates"; -"COURSE_DATES.RESET_DATE.TAB_INFO_BANNER.BODY" = "We built a suggested schedule to help you stay on track. But don’t worry – it’s flexible so you can learn at your own pace. If you happen to fall behind, you’ll be able to adjust the dates to keep yourself on track."; -"COURSE_DATES.RESET_DATE.TAB_INFO_BANNER.HEADER" = ""; +"COURSE.DUE_TODAY" = "Due Today"; +"COURSE.DUE_TOMORROW" = "Due Tomorrow"; -"COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.BODY" = "To complete graded assignments as part of this course, you can upgrade today."; -"COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.BUTTON" = ""; -"COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.HEADER" = ""; +"COURSE.PROGRESS_COMPLETED" = "%@ of %@ assignments complete"; -"COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.BODY" = "You are auditing this course, which means that you are unable to participate in graded assignments. It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today."; -"COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.BUTTON" = ""; -"COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.HEADER" = ""; +"COURSE.ALERT.CANCEL" = "Cancel"; +"COURSE.ALERT.CLOSE" = "Close"; +"COURSE.ALERT.REMOVE" = "Remove"; +"COURSE.ALERT.DOWNLOAD" = "Download"; +"COURSE.ALERT.TRY_AGAIN" = "Try again"; -"COURSE_DATES.RESET_DATE.ERROR_MESSAGE" = "Your dates could not be shifted. Please try again."; -"COURSE_DATES.RESET_DATE.SUCCESS_MESSAGE" = "Your dates have been successfully shifted."; -"COURSE_DATES.RESET_DATE.TITLE" = "Course Dates"; +"COURSE.ALERT.REMOVE_TITLE" = "Remove Offline Content?"; +"COURSE.ALERT.CONFIRM_DOWNLOAD_TITLE" = "Confirm Download"; +"COURSE.ALERT.CONFIRM_DOWNLOAD_CELLULAR_TITLE" = "Download on Cellular?"; + +"COURSE.ALERT.REMOVE_DESCRIPTION" = "Removing this content will free up %@."; +"COURSE.ALERT.CONFIRM_DOWNLOAD_DESCRIPTION" = "Downloading this %@ of content will save available blocks offline."; +"COURSE.ALERT.CONFIRM_DOWNLOAD_CELLULAR_DESCRIPTION" = "Downloading this content will use %@ of cellular data."; + +"COURSE.ERROR.DOWNLOAD_FAILED_TITLE" = "Download Failed"; +"COURSE.ERROR.NO_INTERNET_CONNECTION_TITLE" = "No Internet Connection"; +"COURSE.ERROR.WIFI_REQUIRED_TITLE" = "Wi-Fi Required"; + +"COURSE.ERROR.DOWNLOAD_FAILED_DESCRIPTION" = "Unfortunately, this content failed to download. Please try again later or report this issue."; +"COURSE.ERROR.NO_INTERNET_CONNECTION_DESCRIPTION" = "Downloading this content requires an active internet connection. Please connect to the internet and try again."; +"COURSE.ERROR.WIFI_REQUIRED_DESCRIPTION" = "Downloading this content requires an active WiFi connection. Please connect to a WiFi network and try again."; + +"COURSE.STORAGE_ALERT.TITLE" = "Device Storage Full"; +"COURSE.STORAGE_ALERT.DESCRIPTION" = "Your device does not have enough free space to download this content. Please free up some space and try again."; +"COURSE.STORAGE_ALERT.USED_AND_FREE" = "%@ used, %@ free"; + +"COURSE.LARGEST_DOWNLOADS.TITLE" = "Largest Downloads"; +"COURSE.LARGEST_DOWNLOADS.DONE" = "Done"; +"COURSE.LARGEST_DOWNLOADS.EDIT" = "Edit"; +"COURSE.LARGEST_DOWNLOADS.REMOVE_DOWNLOADS" = "Remove all downloads"; + +"COURSE.OFFLINE.VISIBLE" = "%@%% of this course is visible on mobile."; +"COURSE.OFFLINE.DOWNLOADABLE" = "%@%% of this course is downloadable."; +"COURSE.OFFLINE.CAN_BE_COMPLETED" = "%@%% of this course can be completed offline."; + +"COURSE.TOTAL_PROGRESS.DOWNLOADED" = "Downloaded"; +"COURSE.TOTAL_PROGRESS.DOWNLOADING" = "Downloading"; +"COURSE.TOTAL_PROGRESS.AVALIABLE_TO_DOWNLOAD" = "Available to Download"; +"COURSE.TOTAL_PROGRESS.READY_TO_DOWNLOAD" = "Ready to Download"; + + +"COURSE.OFFLINE.DOWNLOAD_ALL" = "Download all"; +"COURSE.OFFLINE.CANCEL_COURSE_DOWNLOAD" = "Cancel Course Download"; + +"COURSE.OFFLINE.YOU_CAN_DOWNLOAD" = "You can download course content offline to learn on the go, without requiring an active internet connection or using mobile data."; +"COURSE.OFFLINE.YOU_CANT_DOWNLOAD" = "None of this course’s content is currently avaliable to download offline."; + +"OFFLINE.NOT_DOWNLOADED.TITLE" = "This component is not downloaded"; +"OFFLINE.NOT_DOWNLOADED.DESCRIPTION" = "Explore other parts of this course or download this when you reconnect."; +"OFFLINE.NOT_AVALIABLE.TITLE" = "This component is not yet available offline"; +"OFFLINE.NOT_AVALIABLE.DESCRIPTION" = "Explore other parts of this course or view this when you reconnect."; + +"CALENDAR_SYNC_STATUS.SYNCED" = "Synced to Calendar"; +"CALENDAR_SYNC_STATUS.FAILED" = "Calendar Sync Failed"; +"CALENDAR_SYNC_STATUS.OFFLINE" = "Offline"; diff --git a/Course/Course/en.lproj/Localizable.stringsdict b/Course/Course/en.lproj/Localizable.stringsdict new file mode 100644 index 000000000..ccfae8233 --- /dev/null +++ b/Course/Course/en.lproj/Localizable.stringsdict @@ -0,0 +1,42 @@ + + + + + due_in + + NSStringLocalizedFormatKey + %#@due_in@ + due_in + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Today + one + Due in %d day + other + Due in %d days + + + past_due + + NSStringLocalizedFormatKey + %#@past_due@ + past_due + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Today + one + Due %d day ago + other + Due %d days ago + + + + diff --git a/Course/Course/uk.lproj/Localizable.strings b/Course/Course/uk.lproj/Localizable.strings deleted file mode 100644 index 59a57991a..000000000 --- a/Course/Course/uk.lproj/Localizable.strings +++ /dev/null @@ -1,121 +0,0 @@ -/* - Localizable.strings - Course - - Created by  Stepanok Ivan on 26.09.2022. - -*/ - -"OUTLINE.PASSED_THE_COURSE" = "Вітаємо, ви отримали сертифікат курсу “%@\.“"; -"OUTLINE.VIEW_CERTIFICATE" = "Переглянути сертифікат"; -"OUTLINE.CERTIFICATE" = "Сертифікат"; -"OUTLINE.COURSE_VIDEOS" = "Відео з курсу"; - -"COURSEWARE.COURSE_CONTENT" = "Зміст курсу"; -"COURSEWARE.COURSE_UNITS" = "Модулі"; -"COURSEWARE.NEXT" = "Далі"; -"COURSEWARE.PREVIOUS" = "Назад"; -"COURSEWARE.FINISH" = "Завершити"; -"COURSEWARE.GOOD_WORK" = "Гарна робота!"; -"COURSEWARE.BACK_TO_OUTLINE" = "Повернутись до модуля"; -"COURSEWARE.SECTION" = "Секція “"; -"COURSEWARE.IS_FINISHED" = "“ завершена."; -"COURSEWARE.CONTINUE" = "Продовжити"; -"COURSEWARE.RESUME_WITH" = "Продовжити далі:"; - -"ERROR.NO_INTERNET" = "Ви не підключені до Інтернету. Перевірте підключення до Інтернету і спробуйте ще."; -"ERROR.RELOAD" = "Перезавантажити"; -"ERROR.COMPONENT_NOT_FOUNT" = "Course component not found, please reload"; -"ERROR.NO_HANDOUTS" = "There are currently no handouts for this course"; - -"ALERT.ROTATE_DEVICE" = "Поверніть пристрій, щоб переглянути це відео на весь екран."; -"ALERT.ACCEPT" = "Accept"; -"ALERT.DELETE_ALL_VIDEOS" = "Are you sure you want to delete all video(s) for"; -"ALERT.DELETE_VIDEOS" = "Are you sure you want to delete video(s) for"; -"ALERT.STOP_DOWNLOADING" = "Turning off the switch will stop downloading and delete all downloaded videos for"; - -"COURSE_CONTAINER.COURSE" = "Курс"; -"COURSE_CONTAINER.VIDEOS" = "Всі відео"; -"COURSE_CONTAINER.DATES" = "Dates"; -"COURSE_CONTAINER.DISCUSSIONS" = "Дискусії"; -"COURSE_CONTAINER.HANDOUTS" = "Матеріали"; -"COURSE_CONTAINER.HANDOUTS_IN_DEVELOPING" = "Матеріали в процесі розробки"; - -"HANDOUTS_CELL_HANDOUTS.TITLE" = "Нотатки"; -"HANDOUTS_CELL_ANNOUNCEMENTS.TITLE" = "Оголошення"; -"HANDOUTS_CELL_HANDOUTS.DESCRIPTION" = "Знайдіть важливу інформацію про курс"; -"HANDOUTS_CELL_ANNOUNCEMENTS.DESCRIPTION" = "Будьте в курсі останніх новин"; - -"NOT_AVALIABLE.TITLE" = "Цей інтерактивний компонент не доступний"; -"NOT_AVALIABLE.DESCRIPTION" = "Досліджуйте інші частини цього курсу або перегляньте цю в Браузері."; -"NOT_AVALIABLE.BUTTON" = "Відкрити в браузері"; - -"SUBTITLES.TITLE" = "Субтитри"; - -"ACCESSIBILITY.DOWNLOAD" = "Скачати"; -"ACCESSIBILITY.CANCEL_DOWNLOAD" = "Скасувати завантаження"; -"ACCESSIBILITY.DELETE_DOWNLOAD" = "Видалити файл"; - -"DOWNLOAD.DOWNLOADS" = "Downloads"; -"DOWNLOAD.DOWNLOAD" = "Download"; -"DOWNLOAD.ALL_VIDEOS_DOWNLOADED" = "All videos downloaded"; -"DOWNLOAD.DOWNLOADING_VIDEOS" = "Downloading videos..."; -"DOWNLOAD.DOWNLOAD_TO_DEVICE" = "Download to device"; -"DOWNLOAD.VIDEOS" = "Videos"; -"DOWNLOAD.REMAINING" = "Remaining"; -"DOWNLOAD.UNTITLED"= "Untitled"; -"DOWNLOAD.TOTAL"= "Total"; - -"DOWNLOAD.CHANGE_QUALITY_ALERT" = "You cannot change the download video quality when all videos are downloading"; -"DOWNLOAD.DOWNLOAD_LARGE_FILE_MESSAGE" = "The videos you've selected are larger than 1 GB. Do you want to download these videos?"; -"DOWNLOAD.NO_WIFI_MESSAGE" = "Your current download settings only allow downloads over Wi-Fi.\nPlease connect to a Wi-Fi network or change your download settings."; - -"COURSE_DATES.TODAY" = "Today"; -"COURSE_DATES.COMPLETED" = "Completed"; -"COURSE_DATES.PAST_DUE" = "Past due"; -"COURSE_DATES.DUE_NEXT" = "Due next"; -"COURSE_DATES.UNRELEASED" = "Unreleased"; -"COURSE_DATES.VERIFIED_ONLY" = "Verified Only"; -"COURSE_DATES.ITEMS_HIDDEN" = "Items Hidden"; -"COURSE_DATES.ITEM_HIDDEN" = "Item Hidden"; -"COURSE_DATES.TOAST_SUCCESS_TITLE" = "Due dates shifted"; -"COURSE_DATES.TOAST_SUCCESS_MESSAGE" = "Your due dates have been successfully shifted to help you stay on track."; -"COURSE_DATES.VIEW_ALL_DATES" = "View all dates"; -"COURSE_DATES.SYNC_TO_CALENDAR" = "Sync to calendar"; -"COURSE_DATES.SYNC_TO_CALENDAR_MESSAGE" = "Automatically sync all deadlines and due dates for this course to your calendar."; -"COURSE_DATES.ADD_CALENDAR_TITLE"="Add calendar"; -"COURSE_DATES.REMOVE_CALENDAR_TITLE"="Remove calendar"; -"COURSE_DATES.ADD_CALENDAR_PROMPT"="Would you like to add the %@ calendar \"%@\" ? \n You can edit or remove the course calendar any time in Calendar or Settings"; -"COURSE_DATES.REMOVE_CALENDAR_PROMPT"="Would you like to remove the %@ calendar \"%@\" ?"; -"COURSE_DATES.DATES_ADDED_ALERT_MESSAGE" = "\"%@\" has been added to your calendar."; -"COURSE_DATES.CALENDAR_SYNC_MESSAGE"="Syncing calendar..."; -"COURSE_DATES.CALENDAR_VIEW_EVENTS"="View Events"; -"COURSE_DATES.CALENDAR_EVENTS_ADDED"="Your course calendar has been added."; -"COURSE_DATES.CALENDAR_EVENTS_REMOVED"="Your course calendar has been removed."; -"COURSE_DATES.CALENDAR_EVENTS"="Calendar events"; -"COURSE_DATES.CALENDAR_OUT_OF_DATE"="Your course calendar is out of date"; -"COURSE_DATES.CALENDAR_SHIFT_MESSAGE"="Your course dates have been shifted and your course calendar is no longer up to date with your new schedule."; -"COURSE_DATES.CALENDAR_SHIFT_PROMPT_UPDATE_NOW"="Update now"; -"COURSE_DATES.CALENDAR_EVENTS_UPDATED"="Your course calendar has been updated."; -"COURSE_DATES.CALENDAR_PERMISSION_NOT_DETERMINED"="%@ does not have calendar permission. Please go to settings and give calender permission."; -"COURSE_DATES.OPEN_SETTINGS"="Open Settings"; -"COURSE_DATES.SETTINGS" = "Settings"; - -"COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.BODY" = "Don't worry - shift our suggested schedule to complete past due assignments without losing any progress."; -"COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.BUTTON" = "Shift due dates"; -"COURSE_DATES.RESET_DATE.RESET_DATE_BANNER.HEADER" = "Missed some deadlines?"; - -"COURSE_DATES.RESET_DATE.TAB_INFO_BANNER.BODY" = "We built a suggested schedule to help you stay on track. But don’t worry – it’s flexible so you can learn at your own pace. If you happen to fall behind, you’ll be able to adjust the dates to keep yourself on track."; -"COURSE_DATES.RESET_DATE.TAB_INFO_BANNER.HEADER" = ""; - -"COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.BODY" = "To complete graded assignments as part of this course, you can upgrade today."; -"COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.BUTTON" = ""; -"COURSE_DATES.RESET_DATE.UPGRADE_TO_COMPLETE_GRADED_BANNER.HEADER" = ""; - -"COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.BODY" = "You are auditing this course, which means that you are unable to participate in graded assignments. It looks like you missed some important deadlines based on our suggested schedule. To complete graded assignments as part of this course and shift the past due assignments into the future, you can upgrade today."; -"COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.BUTTON" = ""; -"COURSE_DATES.RESET_DATE.UPGRADE_TO_RESET_BANNER.HEADER" = ""; - -"COURSE_DATES.RESET_DATE.ERROR_MESSAGE" = "Your dates could not be shifted. Please try again."; -"COURSE_DATES.RESET_DATE.SUCCESS_MESSAGE" = "Your dates have been successfully shifted."; -"COURSE_DATES.RESET_DATE.TITLE" = "Course Dates"; diff --git a/Course/Course/uk.lproj/Localizable.stringsdict b/Course/Course/uk.lproj/Localizable.stringsdict new file mode 100644 index 000000000..0b7ac9460 --- /dev/null +++ b/Course/Course/uk.lproj/Localizable.stringsdict @@ -0,0 +1,42 @@ + + + + + due_in + + NSStringLocalizedFormatKey + %#@due_in@ + due_in + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Today + one + Прострочено на %d день + other + Прострочено на %d днів + + + past_due + + NSStringLocalizedFormatKey + %#@past_due@ + past_due + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + Today + one + Залишився %d день + other + Залишилося %d днів + + + + diff --git a/Course/CourseTests/CourseMock.generated.swift b/Course/CourseTests/CourseMock.generated.swift index 1b68b62f7..b30530b11 100644 --- a/Course/CourseTests/CourseMock.generated.swift +++ b/Course/CourseTests/CourseMock.generated.swift @@ -13,6 +13,7 @@ import Course import Foundation import SwiftUI import Combine +import OEXFoundation // MARK: - AuthInteractorProtocol @@ -93,6 +94,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + open func login(ssoToken: String) throws -> User { + addInvocation(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) + let perform = methodPerformValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) as? (String) -> Void + perform?(`ssoToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(ssoToken: String). Use given") + Failure("Stub return value not specified for login(ssoToken: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -174,6 +191,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) + case m_login__ssoToken_ssoToken(Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields @@ -194,6 +212,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) return Matcher.ComparisonResult(results) + case (.m_login__ssoToken_ssoToken(let lhsSsotoken), .m_login__ssoToken_ssoToken(let rhsSsotoken)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSsotoken, rhs: rhsSsotoken, with: matcher), lhsSsotoken, rhsSsotoken, "ssoToken")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -224,6 +247,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue + case let .m_login__ssoToken_ssoToken(p0): return p0.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 @@ -235,6 +259,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" + case .m_login__ssoToken_ssoToken: return ".login(ssoToken:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" @@ -261,6 +286,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func login(ssoToken: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -297,6 +325,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + public static func login(ssoToken: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func login(ssoToken: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -356,6 +394,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} @discardableResult public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} + public static func login(ssoToken: Parameter) -> Verify { return Verify(method: .m_login__ssoToken_ssoToken(`ssoToken`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} @@ -375,6 +414,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) } + public static func login(ssoToken: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_login__ssoToken_ssoToken(`ssoToken`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -581,6 +623,12 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -619,6 +667,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showSSOWebBrowser__title_title(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -679,6 +728,11 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) return Matcher.ComparisonResult(results) + case (.m_showSSOWebBrowser__title_title(let lhsTitle), .m_showSSOWebBrowser__title_title(let rhsTitle)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -732,6 +786,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showSSOWebBrowser__title_title(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -752,6 +807,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showSSOWebBrowser__title_title: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -786,6 +842,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} + public static func showSSOWebBrowser(title: Parameter) -> Verify { return Verify(method: .m_showSSOWebBrowser__title_title(`title`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`))} @@ -832,6 +889,9 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showSSOWebBrowser__title_title(`title`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -919,9 +979,9 @@ open class BaseRouterMock: BaseRouter, Mock { } } -// MARK: - ConnectivityProtocol +// MARK: - CalendarManagerProtocol -open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { +open class CalendarManagerProtocolMock: CalendarManagerProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -959,51 +1019,176 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } - public var isInternetAvaliable: Bool { - get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } - } - private var __p_isInternetAvaliable: (Bool)? - public var isMobileData: Bool { - get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } - } - private var __p_isMobileData: (Bool)? - public var internetReachableSubject: CurrentValueSubject { - get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } - } - private var __p_internetReachableSubject: (CurrentValueSubject)? + open func createCalendarIfNeeded() { + addInvocation(.m_createCalendarIfNeeded) + let perform = methodPerformValue(.m_createCalendarIfNeeded) as? () -> Void + perform?() + } + + open func filterCoursesBySelected(fetchedCourses: [CourseForSync]) -> [CourseForSync] { + addInvocation(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) + let perform = methodPerformValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) as? ([CourseForSync]) -> Void + perform?(`fetchedCourses`) + var __value: [CourseForSync] + do { + __value = try methodReturnValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))).casted() + } catch { + onFatalFailure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + Failure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + } + return __value + } + + open func removeOldCalendar() { + addInvocation(.m_removeOldCalendar) + let perform = methodPerformValue(.m_removeOldCalendar) as? () -> Void + perform?() + } + + open func removeOutdatedEvents(courseID: String) { + addInvocation(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + } + + open func syncCourse(courseID: String, courseName: String, dates: CourseDates) { + addInvocation(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) + let perform = methodPerformValue(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) as? (String, String, CourseDates) -> Void + perform?(`courseID`, `courseName`, `dates`) + } + + open func requestAccess() -> Bool { + addInvocation(.m_requestAccess) + let perform = methodPerformValue(.m_requestAccess) as? () -> Void + perform?() + var __value: Bool + do { + __value = try methodReturnValue(.m_requestAccess).casted() + } catch { + onFatalFailure("Stub return value not specified for requestAccess(). Use given") + Failure("Stub return value not specified for requestAccess(). Use given") + } + return __value + } + + open func courseStatus(courseID: String) -> SyncStatus { + addInvocation(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: SyncStatus + do { + __value = try methodReturnValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch { + onFatalFailure("Stub return value not specified for courseStatus(courseID: String). Use given") + Failure("Stub return value not specified for courseStatus(courseID: String). Use given") + } + return __value + } + open func clearAllData(removeCalendar: Bool) { + addInvocation(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) + let perform = methodPerformValue(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) as? (Bool) -> Void + perform?(`removeCalendar`) + } + open func isDatesChanged(courseID: String, checksum: String) -> Bool { + addInvocation(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) + let perform = methodPerformValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) as? (String, String) -> Void + perform?(`courseID`, `checksum`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + Failure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + } + return __value + } fileprivate enum MethodType { - case p_isInternetAvaliable_get - case p_isMobileData_get - case p_internetReachableSubject_get + case m_createCalendarIfNeeded + case m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>) + case m_removeOldCalendar + case m_removeOutdatedEvents__courseID_courseID(Parameter) + case m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter, Parameter, Parameter) + case m_requestAccess + case m_courseStatus__courseID_courseID(Parameter) + case m_clearAllData__removeCalendar_removeCalendar(Parameter) + case m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match - case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match - case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + switch (lhs, rhs) { + case (.m_createCalendarIfNeeded, .m_createCalendarIfNeeded): return .match + + case (.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let lhsFetchedcourses), .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let rhsFetchedcourses)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFetchedcourses, rhs: rhsFetchedcourses, with: matcher), lhsFetchedcourses, rhsFetchedcourses, "fetchedCourses")) + return Matcher.ComparisonResult(results) + + case (.m_removeOldCalendar, .m_removeOldCalendar): return .match + + case (.m_removeOutdatedEvents__courseID_courseID(let lhsCourseid), .m_removeOutdatedEvents__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let lhsCourseid, let lhsCoursename, let lhsDates), .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let rhsCourseid, let rhsCoursename, let rhsDates)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDates, rhs: rhsDates, with: matcher), lhsDates, rhsDates, "dates")) + return Matcher.ComparisonResult(results) + + case (.m_requestAccess, .m_requestAccess): return .match + + case (.m_courseStatus__courseID_courseID(let lhsCourseid), .m_courseStatus__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_clearAllData__removeCalendar_removeCalendar(let lhsRemovecalendar), .m_clearAllData__removeCalendar_removeCalendar(let rhsRemovecalendar)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRemovecalendar, rhs: rhsRemovecalendar, with: matcher), lhsRemovecalendar, rhsRemovecalendar, "removeCalendar")) + return Matcher.ComparisonResult(results) + + case (.m_isDatesChanged__courseID_courseIDchecksum_checksum(let lhsCourseid, let lhsChecksum), .m_isDatesChanged__courseID_courseIDchecksum_checksum(let rhsCourseid, let rhsChecksum)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsChecksum, rhs: rhsChecksum, with: matcher), lhsChecksum, rhsChecksum, "checksum")) + return Matcher.ComparisonResult(results) default: return .none } } func intValue() -> Int { switch self { - case .p_isInternetAvaliable_get: return 0 - case .p_isMobileData_get: return 0 - case .p_internetReachableSubject_get: return 0 + case .m_createCalendarIfNeeded: return 0 + case let .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(p0): return p0.intValue + case .m_removeOldCalendar: return 0 + case let .m_removeOutdatedEvents__courseID_courseID(p0): return p0.intValue + case let .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case .m_requestAccess: return 0 + case let .m_courseStatus__courseID_courseID(p0): return p0.intValue + case let .m_clearAllData__removeCalendar_removeCalendar(p0): return p0.intValue + case let .m_isDatesChanged__courseID_courseIDchecksum_checksum(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { - case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" - case .p_isMobileData_get: return "[get] .isMobileData" - case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + case .m_createCalendarIfNeeded: return ".createCalendarIfNeeded()" + case .m_filterCoursesBySelected__fetchedCourses_fetchedCourses: return ".filterCoursesBySelected(fetchedCourses:)" + case .m_removeOldCalendar: return ".removeOldCalendar()" + case .m_removeOutdatedEvents__courseID_courseID: return ".removeOutdatedEvents(courseID:)" + case .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates: return ".syncCourse(courseID:courseName:dates:)" + case .m_requestAccess: return ".requestAccess()" + case .m_courseStatus__courseID_courseID: return ".courseStatus(courseID:)" + case .m_clearAllData__removeCalendar_removeCalendar: return ".clearAllData(removeCalendar:)" + case .m_isDatesChanged__courseID_courseIDchecksum_checksum: return ".isDatesChanged(courseID:checksum:)" } } } @@ -1016,30 +1201,94 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { super.init(products) } - public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willReturn: [CourseForSync]...) -> MethodStub { + return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func requestAccess(willReturn: Bool...) -> MethodStub { + return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { - return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func courseStatus(courseID: Parameter, willReturn: SyncStatus...) -> MethodStub { + return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willProduce: (Stubber<[CourseForSync]>) -> Void) -> MethodStub { + let willReturn: [[CourseForSync]] = [] + let given: Given = { return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseForSync]).self) + willProduce(stubber) + return given + } + public static func requestAccess(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given + } + public static func courseStatus(courseID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [SyncStatus] = [] + let given: Given = { return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (SyncStatus).self) + willProduce(stubber) + return given + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given } - } public struct Verify { fileprivate var method: MethodType - public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } - public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } - public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + public static func createCalendarIfNeeded() -> Verify { return Verify(method: .m_createCalendarIfNeeded)} + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>) -> Verify { return Verify(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`))} + public static func removeOldCalendar() -> Verify { return Verify(method: .m_removeOldCalendar)} + public static func removeOutdatedEvents(courseID: Parameter) -> Verify { return Verify(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`))} + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter) -> Verify { return Verify(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`))} + public static func requestAccess() -> Verify { return Verify(method: .m_requestAccess)} + public static func courseStatus(courseID: Parameter) -> Verify { return Verify(method: .m_courseStatus__courseID_courseID(`courseID`))} + public static func clearAllData(removeCalendar: Parameter) -> Verify { return Verify(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`))} + public static func isDatesChanged(courseID: Parameter, checksum: Parameter) -> Verify { return Verify(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`))} } public struct Perform { fileprivate var method: MethodType var performs: Any + public static func createCalendarIfNeeded(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_createCalendarIfNeeded, performs: perform) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, perform: @escaping ([CourseForSync]) -> Void) -> Perform { + return Perform(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), performs: perform) + } + public static func removeOldCalendar(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeOldCalendar, performs: perform) + } + public static func removeOutdatedEvents(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`), performs: perform) + } + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter, perform: @escaping (String, String, CourseDates) -> Void) -> Perform { + return Perform(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`), performs: perform) + } + public static func requestAccess(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_requestAccess, performs: perform) + } + public static func courseStatus(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_courseStatus__courseID_courseID(`courseID`), performs: perform) + } + public static func clearAllData(removeCalendar: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`), performs: perform) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), performs: perform) + } } public func given(_ method: Given) { @@ -1115,9 +1364,9 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } -// MARK: - CoreAnalytics +// MARK: - ConfigProtocol -open class CoreAnalyticsMock: CoreAnalytics, Mock { +open class ConfigProtocolMock: ConfigProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -1155,118 +1404,1837 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var baseURL: URL { + get { invocations.append(.p_baseURL_get); return __p_baseURL ?? givenGetterValue(.p_baseURL_get, "ConfigProtocolMock - stub value for baseURL was not defined") } + } + private var __p_baseURL: (URL)? + public var baseSSOURL: URL { + get { invocations.append(.p_baseSSOURL_get); return __p_baseSSOURL ?? givenGetterValue(.p_baseSSOURL_get, "ConfigProtocolMock - stub value for baseSSOURL was not defined") } + } + private var __p_baseSSOURL: (URL)? + public var ssoFinishedURL: URL { + get { invocations.append(.p_ssoFinishedURL_get); return __p_ssoFinishedURL ?? givenGetterValue(.p_ssoFinishedURL_get, "ConfigProtocolMock - stub value for ssoFinishedURL was not defined") } + } + private var __p_ssoFinishedURL: (URL)? + public var ssoButtonTitle: [String: Any] { + get { invocations.append(.p_ssoButtonTitle_get); return __p_ssoButtonTitle ?? givenGetterValue(.p_ssoButtonTitle_get, "ConfigProtocolMock - stub value for ssoButtonTitle was not defined") } + } + private var __p_ssoButtonTitle: ([String: Any])? - open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void - perform?(`event`, `parameters`) - } + public var oAuthClientId: String { + get { invocations.append(.p_oAuthClientId_get); return __p_oAuthClientId ?? givenGetterValue(.p_oAuthClientId_get, "ConfigProtocolMock - stub value for oAuthClientId was not defined") } + } + private var __p_oAuthClientId: (String)? - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void - perform?(`event`, `biValue`, `parameters`) - } + public var tokenType: TokenType { + get { invocations.append(.p_tokenType_get); return __p_tokenType ?? givenGetterValue(.p_tokenType_get, "ConfigProtocolMock - stub value for tokenType was not defined") } + } + private var __p_tokenType: (TokenType)? - open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { - addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) - let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void - perform?(`event`, `biValue`, `action`, `rating`) - } + public var feedbackEmail: String { + get { invocations.append(.p_feedbackEmail_get); return __p_feedbackEmail ?? givenGetterValue(.p_feedbackEmail_get, "ConfigProtocolMock - stub value for feedbackEmail was not defined") } + } + private var __p_feedbackEmail: (String)? - open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { - addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) - let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void - perform?(`event`, `bivalue`, `value`, `oldValue`) - } + public var appStoreLink: String { + get { invocations.append(.p_appStoreLink_get); return __p_appStoreLink ?? givenGetterValue(.p_appStoreLink_get, "ConfigProtocolMock - stub value for appStoreLink was not defined") } + } + private var __p_appStoreLink: (String)? - open func trackEvent(_ event: AnalyticsEvent) { - addInvocation(.m_trackEvent__event(Parameter.value(`event`))) - let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void - perform?(`event`) - } + public var faq: URL? { + get { invocations.append(.p_faq_get); return __p_faq ?? optionalGivenGetterValue(.p_faq_get, "ConfigProtocolMock - stub value for faq was not defined") } + } + private var __p_faq: (URL)? - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void - perform?(`event`, `biValue`) - } + public var platformName: String { + get { invocations.append(.p_platformName_get); return __p_platformName ?? givenGetterValue(.p_platformName_get, "ConfigProtocolMock - stub value for platformName was not defined") } + } + private var __p_platformName: (String)? + public var agreement: AgreementConfig { + get { invocations.append(.p_agreement_get); return __p_agreement ?? givenGetterValue(.p_agreement_get, "ConfigProtocolMock - stub value for agreement was not defined") } + } + private var __p_agreement: (AgreementConfig)? - fileprivate enum MethodType { - case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) - case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) - case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) - case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) - case m_trackEvent__event(Parameter) - case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + public var firebase: FirebaseConfig { + get { invocations.append(.p_firebase_get); return __p_firebase ?? givenGetterValue(.p_firebase_get, "ConfigProtocolMock - stub value for firebase was not defined") } + } + private var __p_firebase: (FirebaseConfig)? - static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { - case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + public var facebook: FacebookConfig { + get { invocations.append(.p_facebook_get); return __p_facebook ?? givenGetterValue(.p_facebook_get, "ConfigProtocolMock - stub value for facebook was not defined") } + } + private var __p_facebook: (FacebookConfig)? - case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + public var microsoft: MicrosoftConfig { + get { invocations.append(.p_microsoft_get); return __p_microsoft ?? givenGetterValue(.p_microsoft_get, "ConfigProtocolMock - stub value for microsoft was not defined") } + } + private var __p_microsoft: (MicrosoftConfig)? - case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) - return Matcher.ComparisonResult(results) + public var google: GoogleConfig { + get { invocations.append(.p_google_get); return __p_google ?? givenGetterValue(.p_google_get, "ConfigProtocolMock - stub value for google was not defined") } + } + private var __p_google: (GoogleConfig)? - case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) - return Matcher.ComparisonResult(results) + public var appleSignIn: AppleSignInConfig { + get { invocations.append(.p_appleSignIn_get); return __p_appleSignIn ?? givenGetterValue(.p_appleSignIn_get, "ConfigProtocolMock - stub value for appleSignIn was not defined") } + } + private var __p_appleSignIn: (AppleSignInConfig)? - case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - return Matcher.ComparisonResult(results) + public var features: FeaturesConfig { + get { invocations.append(.p_features_get); return __p_features ?? givenGetterValue(.p_features_get, "ConfigProtocolMock - stub value for features was not defined") } + } + private var __p_features: (FeaturesConfig)? - case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - return Matcher.ComparisonResult(results) - default: return .none - } - } + public var theme: ThemeConfig { + get { invocations.append(.p_theme_get); return __p_theme ?? givenGetterValue(.p_theme_get, "ConfigProtocolMock - stub value for theme was not defined") } + } + private var __p_theme: (ThemeConfig)? - func intValue() -> Int { - switch self { - case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue - case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue - case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue - case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + public var uiComponents: UIComponentsConfig { + get { invocations.append(.p_uiComponents_get); return __p_uiComponents ?? givenGetterValue(.p_uiComponents_get, "ConfigProtocolMock - stub value for uiComponents was not defined") } + } + private var __p_uiComponents: (UIComponentsConfig)? + + public var discovery: DiscoveryConfig { + get { invocations.append(.p_discovery_get); return __p_discovery ?? givenGetterValue(.p_discovery_get, "ConfigProtocolMock - stub value for discovery was not defined") } + } + private var __p_discovery: (DiscoveryConfig)? + + public var dashboard: DashboardConfig { + get { invocations.append(.p_dashboard_get); return __p_dashboard ?? givenGetterValue(.p_dashboard_get, "ConfigProtocolMock - stub value for dashboard was not defined") } + } + private var __p_dashboard: (DashboardConfig)? + + public var braze: BrazeConfig { + get { invocations.append(.p_braze_get); return __p_braze ?? givenGetterValue(.p_braze_get, "ConfigProtocolMock - stub value for braze was not defined") } + } + private var __p_braze: (BrazeConfig)? + + public var branch: BranchConfig { + get { invocations.append(.p_branch_get); return __p_branch ?? givenGetterValue(.p_branch_get, "ConfigProtocolMock - stub value for branch was not defined") } + } + private var __p_branch: (BranchConfig)? + + public var program: DiscoveryConfig { + get { invocations.append(.p_program_get); return __p_program ?? givenGetterValue(.p_program_get, "ConfigProtocolMock - stub value for program was not defined") } + } + private var __p_program: (DiscoveryConfig)? + + public var URIScheme: String { + get { invocations.append(.p_URIScheme_get); return __p_URIScheme ?? givenGetterValue(.p_URIScheme_get, "ConfigProtocolMock - stub value for URIScheme was not defined") } + } + private var __p_URIScheme: (String)? + + + + + + + fileprivate enum MethodType { + case p_baseURL_get + case p_baseSSOURL_get + case p_ssoFinishedURL_get + case p_ssoButtonTitle_get + case p_oAuthClientId_get + case p_tokenType_get + case p_feedbackEmail_get + case p_appStoreLink_get + case p_faq_get + case p_platformName_get + case p_agreement_get + case p_firebase_get + case p_facebook_get + case p_microsoft_get + case p_google_get + case p_appleSignIn_get + case p_features_get + case p_theme_get + case p_uiComponents_get + case p_discovery_get + case p_dashboard_get + case p_braze_get + case p_branch_get + case p_program_get + case p_URIScheme_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_baseURL_get,.p_baseURL_get): return Matcher.ComparisonResult.match + case (.p_baseSSOURL_get,.p_baseSSOURL_get): return Matcher.ComparisonResult.match + case (.p_ssoFinishedURL_get,.p_ssoFinishedURL_get): return Matcher.ComparisonResult.match + case (.p_ssoButtonTitle_get,.p_ssoButtonTitle_get): return Matcher.ComparisonResult.match + case (.p_oAuthClientId_get,.p_oAuthClientId_get): return Matcher.ComparisonResult.match + case (.p_tokenType_get,.p_tokenType_get): return Matcher.ComparisonResult.match + case (.p_feedbackEmail_get,.p_feedbackEmail_get): return Matcher.ComparisonResult.match + case (.p_appStoreLink_get,.p_appStoreLink_get): return Matcher.ComparisonResult.match + case (.p_faq_get,.p_faq_get): return Matcher.ComparisonResult.match + case (.p_platformName_get,.p_platformName_get): return Matcher.ComparisonResult.match + case (.p_agreement_get,.p_agreement_get): return Matcher.ComparisonResult.match + case (.p_firebase_get,.p_firebase_get): return Matcher.ComparisonResult.match + case (.p_facebook_get,.p_facebook_get): return Matcher.ComparisonResult.match + case (.p_microsoft_get,.p_microsoft_get): return Matcher.ComparisonResult.match + case (.p_google_get,.p_google_get): return Matcher.ComparisonResult.match + case (.p_appleSignIn_get,.p_appleSignIn_get): return Matcher.ComparisonResult.match + case (.p_features_get,.p_features_get): return Matcher.ComparisonResult.match + case (.p_theme_get,.p_theme_get): return Matcher.ComparisonResult.match + case (.p_uiComponents_get,.p_uiComponents_get): return Matcher.ComparisonResult.match + case (.p_discovery_get,.p_discovery_get): return Matcher.ComparisonResult.match + case (.p_dashboard_get,.p_dashboard_get): return Matcher.ComparisonResult.match + case (.p_braze_get,.p_braze_get): return Matcher.ComparisonResult.match + case (.p_branch_get,.p_branch_get): return Matcher.ComparisonResult.match + case (.p_program_get,.p_program_get): return Matcher.ComparisonResult.match + case (.p_URIScheme_get,.p_URIScheme_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_baseURL_get: return 0 + case .p_baseSSOURL_get: return 0 + case .p_ssoFinishedURL_get: return 0 + case .p_ssoButtonTitle_get: return 0 + case .p_oAuthClientId_get: return 0 + case .p_tokenType_get: return 0 + case .p_feedbackEmail_get: return 0 + case .p_appStoreLink_get: return 0 + case .p_faq_get: return 0 + case .p_platformName_get: return 0 + case .p_agreement_get: return 0 + case .p_firebase_get: return 0 + case .p_facebook_get: return 0 + case .p_microsoft_get: return 0 + case .p_google_get: return 0 + case .p_appleSignIn_get: return 0 + case .p_features_get: return 0 + case .p_theme_get: return 0 + case .p_uiComponents_get: return 0 + case .p_discovery_get: return 0 + case .p_dashboard_get: return 0 + case .p_braze_get: return 0 + case .p_branch_get: return 0 + case .p_program_get: return 0 + case .p_URIScheme_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_baseURL_get: return "[get] .baseURL" + case .p_baseSSOURL_get: return "[get] .baseSSOURL" + case .p_ssoFinishedURL_get: return "[get] .ssoFinishedURL" + case .p_ssoButtonTitle_get: return "[get] .ssoButtonTitle" + case .p_oAuthClientId_get: return "[get] .oAuthClientId" + case .p_tokenType_get: return "[get] .tokenType" + case .p_feedbackEmail_get: return "[get] .feedbackEmail" + case .p_appStoreLink_get: return "[get] .appStoreLink" + case .p_faq_get: return "[get] .faq" + case .p_platformName_get: return "[get] .platformName" + case .p_agreement_get: return "[get] .agreement" + case .p_firebase_get: return "[get] .firebase" + case .p_facebook_get: return "[get] .facebook" + case .p_microsoft_get: return "[get] .microsoft" + case .p_google_get: return "[get] .google" + case .p_appleSignIn_get: return "[get] .appleSignIn" + case .p_features_get: return "[get] .features" + case .p_theme_get: return "[get] .theme" + case .p_uiComponents_get: return "[get] .uiComponents" + case .p_discovery_get: return "[get] .discovery" + case .p_dashboard_get: return "[get] .dashboard" + case .p_braze_get: return "[get] .braze" + case .p_branch_get: return "[get] .branch" + case .p_program_get: return "[get] .program" + case .p_URIScheme_get: return "[get] .URIScheme" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func baseURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func baseSSOURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseSSOURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoFinishedURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_ssoFinishedURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoButtonTitle(getter defaultValue: [String: Any]...) -> PropertyStub { + return Given(method: .p_ssoButtonTitle_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func oAuthClientId(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_oAuthClientId_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func tokenType(getter defaultValue: TokenType...) -> PropertyStub { + return Given(method: .p_tokenType_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func feedbackEmail(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_feedbackEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appStoreLink(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_appStoreLink_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func faq(getter defaultValue: URL?...) -> PropertyStub { + return Given(method: .p_faq_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func platformName(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_platformName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func agreement(getter defaultValue: AgreementConfig...) -> PropertyStub { + return Given(method: .p_agreement_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func firebase(getter defaultValue: FirebaseConfig...) -> PropertyStub { + return Given(method: .p_firebase_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func facebook(getter defaultValue: FacebookConfig...) -> PropertyStub { + return Given(method: .p_facebook_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func microsoft(getter defaultValue: MicrosoftConfig...) -> PropertyStub { + return Given(method: .p_microsoft_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func google(getter defaultValue: GoogleConfig...) -> PropertyStub { + return Given(method: .p_google_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignIn(getter defaultValue: AppleSignInConfig...) -> PropertyStub { + return Given(method: .p_appleSignIn_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func features(getter defaultValue: FeaturesConfig...) -> PropertyStub { + return Given(method: .p_features_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func theme(getter defaultValue: ThemeConfig...) -> PropertyStub { + return Given(method: .p_theme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func uiComponents(getter defaultValue: UIComponentsConfig...) -> PropertyStub { + return Given(method: .p_uiComponents_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func discovery(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_discovery_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func dashboard(getter defaultValue: DashboardConfig...) -> PropertyStub { + return Given(method: .p_dashboard_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func braze(getter defaultValue: BrazeConfig...) -> PropertyStub { + return Given(method: .p_braze_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func branch(getter defaultValue: BranchConfig...) -> PropertyStub { + return Given(method: .p_branch_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func program(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_program_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func URIScheme(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_URIScheme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var baseURL: Verify { return Verify(method: .p_baseURL_get) } + public static var baseSSOURL: Verify { return Verify(method: .p_baseSSOURL_get) } + public static var ssoFinishedURL: Verify { return Verify(method: .p_ssoFinishedURL_get) } + public static var ssoButtonTitle: Verify { return Verify(method: .p_ssoButtonTitle_get) } + public static var oAuthClientId: Verify { return Verify(method: .p_oAuthClientId_get) } + public static var tokenType: Verify { return Verify(method: .p_tokenType_get) } + public static var feedbackEmail: Verify { return Verify(method: .p_feedbackEmail_get) } + public static var appStoreLink: Verify { return Verify(method: .p_appStoreLink_get) } + public static var faq: Verify { return Verify(method: .p_faq_get) } + public static var platformName: Verify { return Verify(method: .p_platformName_get) } + public static var agreement: Verify { return Verify(method: .p_agreement_get) } + public static var firebase: Verify { return Verify(method: .p_firebase_get) } + public static var facebook: Verify { return Verify(method: .p_facebook_get) } + public static var microsoft: Verify { return Verify(method: .p_microsoft_get) } + public static var google: Verify { return Verify(method: .p_google_get) } + public static var appleSignIn: Verify { return Verify(method: .p_appleSignIn_get) } + public static var features: Verify { return Verify(method: .p_features_get) } + public static var theme: Verify { return Verify(method: .p_theme_get) } + public static var uiComponents: Verify { return Verify(method: .p_uiComponents_get) } + public static var discovery: Verify { return Verify(method: .p_discovery_get) } + public static var dashboard: Verify { return Verify(method: .p_dashboard_get) } + public static var braze: Verify { return Verify(method: .p_braze_get) } + public static var branch: Verify { return Verify(method: .p_branch_get) } + public static var program: Verify { return Verify(method: .p_program_get) } + public static var URIScheme: Verify { return Verify(method: .p_URIScheme_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - ConnectivityProtocol + +open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var isInternetAvaliable: Bool { + get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } + } + private var __p_isInternetAvaliable: (Bool)? + + public var isMobileData: Bool { + get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } + } + private var __p_isMobileData: (Bool)? + + public var internetReachableSubject: CurrentValueSubject { + get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } + } + private var __p_internetReachableSubject: (CurrentValueSubject)? + + + + + + + fileprivate enum MethodType { + case p_isInternetAvaliable_get + case p_isMobileData_get + case p_internetReachableSubject_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match + case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match + case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_isInternetAvaliable_get: return 0 + case .p_isMobileData_get: return 0 + case .p_internetReachableSubject_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" + case .p_isMobileData_get: return "[get] .isMobileData" + case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { + return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } + public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } + public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreAnalytics + +open class CoreAnalyticsMock: CoreAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { + addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) + let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void + perform?(`event`, `biValue`, `action`, `rating`) + } + + open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { + addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) + let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void + perform?(`event`, `bivalue`, `value`, `oldValue`) + } + + open func trackEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + + fileprivate enum MethodType { + case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) + case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) + case m_trackEvent__event(Parameter) + case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) + return Matcher.ComparisonResult(results) + + case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_trackEvent__event(p0): return p0.intValue case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" + case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" + case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" + case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" + case .m_trackEvent__event: return ".trackEvent(_:)" + case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} + public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { + return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) + } + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { + return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) + } + public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackEvent__event(`event`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CorePersistenceProtocol + +open class CorePersistenceProtocolMock: CorePersistenceProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func set(userId: Int) { + addInvocation(.m_set__userId_userId(Parameter.value(`userId`))) + let perform = methodPerformValue(.m_set__userId_userId(Parameter.value(`userId`))) as? (Int) -> Void + perform?(`userId`) + } + + open func getUserID() -> Int? { + addInvocation(.m_getUserID) + let perform = methodPerformValue(.m_getUserID) as? () -> Void + perform?() + var __value: Int? = nil + do { + __value = try methodReturnValue(.m_getUserID).casted() + } catch { + // do nothing + } + return __value + } + + open func publisher() -> AnyPublisher { + addInvocation(.m_publisher) + let perform = methodPerformValue(.m_publisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_publisher).casted() + } catch { + onFatalFailure("Stub return value not specified for publisher(). Use given") + Failure("Stub return value not specified for publisher(). Use given") + } + return __value + } + + open func addToDownloadQueue(tasks: [DownloadDataTask]) { + addInvocation(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) as? ([DownloadDataTask]) -> Void + perform?(`tasks`) + } + + open func saveOfflineProgress(progress: OfflineProgress) { + addInvocation(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) + let perform = methodPerformValue(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) as? (OfflineProgress) -> Void + perform?(`progress`) + } + + open func loadProgress(for blockID: String) -> OfflineProgress? { + addInvocation(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + var __value: OfflineProgress? = nil + do { + __value = try methodReturnValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))).casted() + } catch { + // do nothing + } + return __value + } + + open func loadAllOfflineProgress() -> [OfflineProgress] { + addInvocation(.m_loadAllOfflineProgress) + let perform = methodPerformValue(.m_loadAllOfflineProgress) as? () -> Void + perform?() + var __value: [OfflineProgress] + do { + __value = try methodReturnValue(.m_loadAllOfflineProgress).casted() + } catch { + onFatalFailure("Stub return value not specified for loadAllOfflineProgress(). Use given") + Failure("Stub return value not specified for loadAllOfflineProgress(). Use given") + } + return __value + } + + open func deleteProgress(for blockID: String) { + addInvocation(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + } + + open func deleteAllProgress() { + addInvocation(.m_deleteAllProgress) + let perform = methodPerformValue(.m_deleteAllProgress) as? () -> Void + perform?() + } + + open func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) { + addInvocation(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) as? ([CourseBlock], DownloadQuality) -> Void + perform?(`blocks`, `downloadQuality`) + } + + open func nextBlockForDownloading() -> DownloadDataTask? { + addInvocation(.m_nextBlockForDownloading) + let perform = methodPerformValue(.m_nextBlockForDownloading) as? () -> Void + perform?() + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_nextBlockForDownloading).casted() + } catch { + // do nothing + } + return __value + } + + open func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) { + addInvocation(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) + let perform = methodPerformValue(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) as? (String, DownloadState, Data?) -> Void + perform?(`id`, `state`, `resumeData`) + } + + open func deleteDownloadDataTask(id: String) throws { + addInvocation(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) + let perform = methodPerformValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) as? (String) -> Void + perform?(`id`) + do { + _ = try methodReturnValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func saveDownloadDataTask(_ task: DownloadDataTask) { + addInvocation(.m_saveDownloadDataTask__task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_saveDownloadDataTask__task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) + } + + open func downloadDataTask(for blockId: String) -> DownloadDataTask? { + addInvocation(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) + let perform = methodPerformValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void + perform?(`blockId`) + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))).casted() + } catch { + // do nothing + } + return __value + } + + open func getDownloadDataTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasks) + let perform = methodPerformValue(.m_getDownloadDataTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasks(). Use given") + Failure("Stub return value not specified for getDownloadDataTasks(). Use given") + } + return __value + } + + open func getDownloadDataTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + } + return __value + } + + + fileprivate enum MethodType { + case m_set__userId_userId(Parameter) + case m_getUserID + case m_publisher + case m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>) + case m_saveOfflineProgress__progress_progress(Parameter) + case m_loadProgress__for_blockID(Parameter) + case m_loadAllOfflineProgress + case m_deleteProgress__for_blockID(Parameter) + case m_deleteAllProgress + case m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>, Parameter) + case m_nextBlockForDownloading + case m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter, Parameter, Parameter) + case m_deleteDownloadDataTask__id_id(Parameter) + case m_saveDownloadDataTask__task(Parameter) + case m_downloadDataTask__for_blockId(Parameter) + case m_getDownloadDataTasks + case m_getDownloadDataTasksForCourse__courseId(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_set__userId_userId(let lhsUserid), .m_set__userId_userId(let rhsUserid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUserid, rhs: rhsUserid, with: matcher), lhsUserid, rhsUserid, "userId")) + return Matcher.ComparisonResult(results) + + case (.m_getUserID, .m_getUserID): return .match + + case (.m_publisher, .m_publisher): return .match + + case (.m_addToDownloadQueue__tasks_tasks(let lhsTasks), .m_addToDownloadQueue__tasks_tasks(let rhsTasks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTasks, rhs: rhsTasks, with: matcher), lhsTasks, rhsTasks, "tasks")) + return Matcher.ComparisonResult(results) + + case (.m_saveOfflineProgress__progress_progress(let lhsProgress), .m_saveOfflineProgress__progress_progress(let rhsProgress)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsProgress, rhs: rhsProgress, with: matcher), lhsProgress, rhsProgress, "progress")) + return Matcher.ComparisonResult(results) + + case (.m_loadProgress__for_blockID(let lhsBlockid), .m_loadProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_loadAllOfflineProgress, .m_loadAllOfflineProgress): return .match + + case (.m_deleteProgress__for_blockID(let lhsBlockid), .m_deleteProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_deleteAllProgress, .m_deleteAllProgress): return .match + + case (.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let lhsBlocks, let lhsDownloadquality), .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let rhsBlocks, let rhsDownloadquality)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDownloadquality, rhs: rhsDownloadquality, with: matcher), lhsDownloadquality, rhsDownloadquality, "downloadQuality")) + return Matcher.ComparisonResult(results) + + case (.m_nextBlockForDownloading, .m_nextBlockForDownloading): return .match + + case (.m_updateDownloadState__id_idstate_stateresumeData_resumeData(let lhsId, let lhsState, let lhsResumedata), .m_updateDownloadState__id_idstate_stateresumeData_resumeData(let rhsId, let rhsState, let rhsResumedata)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsState, rhs: rhsState, with: matcher), lhsState, rhsState, "state")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResumedata, rhs: rhsResumedata, with: matcher), lhsResumedata, rhsResumedata, "resumeData")) + return Matcher.ComparisonResult(results) + + case (.m_deleteDownloadDataTask__id_id(let lhsId), .m_deleteDownloadDataTask__id_id(let rhsId)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + return Matcher.ComparisonResult(results) + + case (.m_saveDownloadDataTask__task(let lhsTask), .m_saveDownloadDataTask__task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "_ task")) + return Matcher.ComparisonResult(results) + + case (.m_downloadDataTask__for_blockId(let lhsBlockid), .m_downloadDataTask__for_blockId(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) + return Matcher.ComparisonResult(results) + + case (.m_getDownloadDataTasks, .m_getDownloadDataTasks): return .match + + case (.m_getDownloadDataTasksForCourse__courseId(let lhsCourseid), .m_getDownloadDataTasksForCourse__courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_set__userId_userId(p0): return p0.intValue + case .m_getUserID: return 0 + case .m_publisher: return 0 + case let .m_addToDownloadQueue__tasks_tasks(p0): return p0.intValue + case let .m_saveOfflineProgress__progress_progress(p0): return p0.intValue + case let .m_loadProgress__for_blockID(p0): return p0.intValue + case .m_loadAllOfflineProgress: return 0 + case let .m_deleteProgress__for_blockID(p0): return p0.intValue + case .m_deleteAllProgress: return 0 + case let .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(p0, p1): return p0.intValue + p1.intValue + case .m_nextBlockForDownloading: return 0 + case let .m_updateDownloadState__id_idstate_stateresumeData_resumeData(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_deleteDownloadDataTask__id_id(p0): return p0.intValue + case let .m_saveDownloadDataTask__task(p0): return p0.intValue + case let .m_downloadDataTask__for_blockId(p0): return p0.intValue + case .m_getDownloadDataTasks: return 0 + case let .m_getDownloadDataTasksForCourse__courseId(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_set__userId_userId: return ".set(userId:)" + case .m_getUserID: return ".getUserID()" + case .m_publisher: return ".publisher()" + case .m_addToDownloadQueue__tasks_tasks: return ".addToDownloadQueue(tasks:)" + case .m_saveOfflineProgress__progress_progress: return ".saveOfflineProgress(progress:)" + case .m_loadProgress__for_blockID: return ".loadProgress(for:)" + case .m_loadAllOfflineProgress: return ".loadAllOfflineProgress()" + case .m_deleteProgress__for_blockID: return ".deleteProgress(for:)" + case .m_deleteAllProgress: return ".deleteAllProgress()" + case .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality: return ".addToDownloadQueue(blocks:downloadQuality:)" + case .m_nextBlockForDownloading: return ".nextBlockForDownloading()" + case .m_updateDownloadState__id_idstate_stateresumeData_resumeData: return ".updateDownloadState(id:state:resumeData:)" + case .m_deleteDownloadDataTask__id_id: return ".deleteDownloadDataTask(id:)" + case .m_saveDownloadDataTask__task: return ".saveDownloadDataTask(_:)" + case .m_downloadDataTask__for_blockId: return ".downloadDataTask(for:)" + case .m_getDownloadDataTasks: return ".getDownloadDataTasks()" + case .m_getDownloadDataTasksForCourse__courseId: return ".getDownloadDataTasksForCourse(_:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func getUserID(willReturn: Int?...) -> MethodStub { + return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func publisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadProgress(for blockID: Parameter, willReturn: OfflineProgress?...) -> MethodStub { + return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadAllOfflineProgress(willReturn: [OfflineProgress]...) -> MethodStub { + return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func nextBlockForDownloading(willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func downloadDataTask(for blockId: Parameter, willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getUserID(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Int?] = [] + let given: Given = { return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Int?).self) + willProduce(stubber) + return given + } + public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func loadProgress(for blockID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [OfflineProgress?] = [] + let given: Given = { return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (OfflineProgress?).self) + willProduce(stubber) + return given + } + public static func loadAllOfflineProgress(willProduce: (Stubber<[OfflineProgress]>) -> Void) -> MethodStub { + let willReturn: [[OfflineProgress]] = [] + let given: Given = { return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([OfflineProgress]).self) + willProduce(stubber) + return given + } + public static func nextBlockForDownloading(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func downloadDataTask(for blockId: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func deleteDownloadDataTask(id: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func deleteDownloadDataTask(id: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func set(userId: Parameter) -> Verify { return Verify(method: .m_set__userId_userId(`userId`))} + public static func getUserID() -> Verify { return Verify(method: .m_getUserID)} + public static func publisher() -> Verify { return Verify(method: .m_publisher)} + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>) -> Verify { return Verify(method: .m_addToDownloadQueue__tasks_tasks(`tasks`))} + public static func saveOfflineProgress(progress: Parameter) -> Verify { return Verify(method: .m_saveOfflineProgress__progress_progress(`progress`))} + public static func loadProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_loadProgress__for_blockID(`blockID`))} + public static func loadAllOfflineProgress() -> Verify { return Verify(method: .m_loadAllOfflineProgress)} + public static func deleteProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_deleteProgress__for_blockID(`blockID`))} + public static func deleteAllProgress() -> Verify { return Verify(method: .m_deleteAllProgress)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`))} + public static func nextBlockForDownloading() -> Verify { return Verify(method: .m_nextBlockForDownloading)} + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter) -> Verify { return Verify(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`))} + public static func deleteDownloadDataTask(id: Parameter) -> Verify { return Verify(method: .m_deleteDownloadDataTask__id_id(`id`))} + public static func saveDownloadDataTask(_ task: Parameter) -> Verify { return Verify(method: .m_saveDownloadDataTask__task(`task`))} + public static func downloadDataTask(for blockId: Parameter) -> Verify { return Verify(method: .m_downloadDataTask__for_blockId(`blockId`))} + public static func getDownloadDataTasks() -> Verify { return Verify(method: .m_getDownloadDataTasks)} + public static func getDownloadDataTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func set(userId: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_set__userId_userId(`userId`), performs: perform) + } + public static func getUserID(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getUserID, performs: perform) + } + public static func publisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_publisher, performs: perform) + } + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>, perform: @escaping ([DownloadDataTask]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__tasks_tasks(`tasks`), performs: perform) + } + public static func saveOfflineProgress(progress: Parameter, perform: @escaping (OfflineProgress) -> Void) -> Perform { + return Perform(method: .m_saveOfflineProgress__progress_progress(`progress`), performs: perform) + } + public static func loadProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_loadProgress__for_blockID(`blockID`), performs: perform) + } + public static func loadAllOfflineProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_loadAllOfflineProgress, performs: perform) + } + public static func deleteProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteProgress__for_blockID(`blockID`), performs: perform) + } + public static func deleteAllProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteAllProgress, performs: perform) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter, perform: @escaping ([CourseBlock], DownloadQuality) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`), performs: perform) + } + public static func nextBlockForDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_nextBlockForDownloading, performs: perform) + } + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter, perform: @escaping (String, DownloadState, Data?) -> Void) -> Perform { + return Perform(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`), performs: perform) + } + public static func deleteDownloadDataTask(id: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteDownloadDataTask__id_id(`id`), performs: perform) + } + public static func saveDownloadDataTask(_ task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_saveDownloadDataTask__task(`task`), performs: perform) + } + public static func downloadDataTask(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_downloadDataTask__for_blockId(`blockId`), performs: perform) + } + public static func getDownloadDataTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasks, performs: perform) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreStorage + +open class CoreStorageMock: CoreStorage, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var accessToken: String? { + get { invocations.append(.p_accessToken_get); return __p_accessToken ?? optionalGivenGetterValue(.p_accessToken_get, "CoreStorageMock - stub value for accessToken was not defined") } + set { invocations.append(.p_accessToken_set(.value(newValue))); __p_accessToken = newValue } + } + private var __p_accessToken: (String)? + + public var refreshToken: String? { + get { invocations.append(.p_refreshToken_get); return __p_refreshToken ?? optionalGivenGetterValue(.p_refreshToken_get, "CoreStorageMock - stub value for refreshToken was not defined") } + set { invocations.append(.p_refreshToken_set(.value(newValue))); __p_refreshToken = newValue } + } + private var __p_refreshToken: (String)? + + public var pushToken: String? { + get { invocations.append(.p_pushToken_get); return __p_pushToken ?? optionalGivenGetterValue(.p_pushToken_get, "CoreStorageMock - stub value for pushToken was not defined") } + set { invocations.append(.p_pushToken_set(.value(newValue))); __p_pushToken = newValue } + } + private var __p_pushToken: (String)? + + public var appleSignFullName: String? { + get { invocations.append(.p_appleSignFullName_get); return __p_appleSignFullName ?? optionalGivenGetterValue(.p_appleSignFullName_get, "CoreStorageMock - stub value for appleSignFullName was not defined") } + set { invocations.append(.p_appleSignFullName_set(.value(newValue))); __p_appleSignFullName = newValue } + } + private var __p_appleSignFullName: (String)? + + public var appleSignEmail: String? { + get { invocations.append(.p_appleSignEmail_get); return __p_appleSignEmail ?? optionalGivenGetterValue(.p_appleSignEmail_get, "CoreStorageMock - stub value for appleSignEmail was not defined") } + set { invocations.append(.p_appleSignEmail_set(.value(newValue))); __p_appleSignEmail = newValue } + } + private var __p_appleSignEmail: (String)? + + public var cookiesDate: Date? { + get { invocations.append(.p_cookiesDate_get); return __p_cookiesDate ?? optionalGivenGetterValue(.p_cookiesDate_get, "CoreStorageMock - stub value for cookiesDate was not defined") } + set { invocations.append(.p_cookiesDate_set(.value(newValue))); __p_cookiesDate = newValue } + } + private var __p_cookiesDate: (Date)? + + public var reviewLastShownVersion: String? { + get { invocations.append(.p_reviewLastShownVersion_get); return __p_reviewLastShownVersion ?? optionalGivenGetterValue(.p_reviewLastShownVersion_get, "CoreStorageMock - stub value for reviewLastShownVersion was not defined") } + set { invocations.append(.p_reviewLastShownVersion_set(.value(newValue))); __p_reviewLastShownVersion = newValue } + } + private var __p_reviewLastShownVersion: (String)? + + public var lastReviewDate: Date? { + get { invocations.append(.p_lastReviewDate_get); return __p_lastReviewDate ?? optionalGivenGetterValue(.p_lastReviewDate_get, "CoreStorageMock - stub value for lastReviewDate was not defined") } + set { invocations.append(.p_lastReviewDate_set(.value(newValue))); __p_lastReviewDate = newValue } + } + private var __p_lastReviewDate: (Date)? + + public var user: DataLayer.User? { + get { invocations.append(.p_user_get); return __p_user ?? optionalGivenGetterValue(.p_user_get, "CoreStorageMock - stub value for user was not defined") } + set { invocations.append(.p_user_set(.value(newValue))); __p_user = newValue } + } + private var __p_user: (DataLayer.User)? + + public var userSettings: UserSettings? { + get { invocations.append(.p_userSettings_get); return __p_userSettings ?? optionalGivenGetterValue(.p_userSettings_get, "CoreStorageMock - stub value for userSettings was not defined") } + set { invocations.append(.p_userSettings_set(.value(newValue))); __p_userSettings = newValue } + } + private var __p_userSettings: (UserSettings)? + + public var resetAppSupportDirectoryUserData: Bool? { + get { invocations.append(.p_resetAppSupportDirectoryUserData_get); return __p_resetAppSupportDirectoryUserData ?? optionalGivenGetterValue(.p_resetAppSupportDirectoryUserData_get, "CoreStorageMock - stub value for resetAppSupportDirectoryUserData was not defined") } + set { invocations.append(.p_resetAppSupportDirectoryUserData_set(.value(newValue))); __p_resetAppSupportDirectoryUserData = newValue } + } + private var __p_resetAppSupportDirectoryUserData: (Bool)? + + public var useRelativeDates: Bool { + get { invocations.append(.p_useRelativeDates_get); return __p_useRelativeDates ?? givenGetterValue(.p_useRelativeDates_get, "CoreStorageMock - stub value for useRelativeDates was not defined") } + set { invocations.append(.p_useRelativeDates_set(.value(newValue))); __p_useRelativeDates = newValue } + } + private var __p_useRelativeDates: (Bool)? + + + + + + open func clear() { + addInvocation(.m_clear) + let perform = methodPerformValue(.m_clear) as? () -> Void + perform?() + } + + + fileprivate enum MethodType { + case m_clear + case p_accessToken_get + case p_accessToken_set(Parameter) + case p_refreshToken_get + case p_refreshToken_set(Parameter) + case p_pushToken_get + case p_pushToken_set(Parameter) + case p_appleSignFullName_get + case p_appleSignFullName_set(Parameter) + case p_appleSignEmail_get + case p_appleSignEmail_set(Parameter) + case p_cookiesDate_get + case p_cookiesDate_set(Parameter) + case p_reviewLastShownVersion_get + case p_reviewLastShownVersion_set(Parameter) + case p_lastReviewDate_get + case p_lastReviewDate_set(Parameter) + case p_user_get + case p_user_set(Parameter) + case p_userSettings_get + case p_userSettings_set(Parameter) + case p_resetAppSupportDirectoryUserData_get + case p_resetAppSupportDirectoryUserData_set(Parameter) + case p_useRelativeDates_get + case p_useRelativeDates_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_clear, .m_clear): return .match + case (.p_accessToken_get,.p_accessToken_get): return Matcher.ComparisonResult.match + case (.p_accessToken_set(let left),.p_accessToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_refreshToken_get,.p_refreshToken_get): return Matcher.ComparisonResult.match + case (.p_refreshToken_set(let left),.p_refreshToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_pushToken_get,.p_pushToken_get): return Matcher.ComparisonResult.match + case (.p_pushToken_set(let left),.p_pushToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignFullName_get,.p_appleSignFullName_get): return Matcher.ComparisonResult.match + case (.p_appleSignFullName_set(let left),.p_appleSignFullName_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignEmail_get,.p_appleSignEmail_get): return Matcher.ComparisonResult.match + case (.p_appleSignEmail_set(let left),.p_appleSignEmail_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_cookiesDate_get,.p_cookiesDate_get): return Matcher.ComparisonResult.match + case (.p_cookiesDate_set(let left),.p_cookiesDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_reviewLastShownVersion_get,.p_reviewLastShownVersion_get): return Matcher.ComparisonResult.match + case (.p_reviewLastShownVersion_set(let left),.p_reviewLastShownVersion_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_lastReviewDate_get,.p_lastReviewDate_get): return Matcher.ComparisonResult.match + case (.p_lastReviewDate_set(let left),.p_lastReviewDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_user_get,.p_user_get): return Matcher.ComparisonResult.match + case (.p_user_set(let left),.p_user_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_userSettings_get,.p_userSettings_get): return Matcher.ComparisonResult.match + case (.p_userSettings_set(let left),.p_userSettings_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_resetAppSupportDirectoryUserData_get,.p_resetAppSupportDirectoryUserData_get): return Matcher.ComparisonResult.match + case (.p_resetAppSupportDirectoryUserData_set(let left),.p_resetAppSupportDirectoryUserData_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_useRelativeDates_get,.p_useRelativeDates_get): return Matcher.ComparisonResult.match + case (.p_useRelativeDates_set(let left),.p_useRelativeDates_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .m_clear: return 0 + case .p_accessToken_get: return 0 + case .p_accessToken_set(let newValue): return newValue.intValue + case .p_refreshToken_get: return 0 + case .p_refreshToken_set(let newValue): return newValue.intValue + case .p_pushToken_get: return 0 + case .p_pushToken_set(let newValue): return newValue.intValue + case .p_appleSignFullName_get: return 0 + case .p_appleSignFullName_set(let newValue): return newValue.intValue + case .p_appleSignEmail_get: return 0 + case .p_appleSignEmail_set(let newValue): return newValue.intValue + case .p_cookiesDate_get: return 0 + case .p_cookiesDate_set(let newValue): return newValue.intValue + case .p_reviewLastShownVersion_get: return 0 + case .p_reviewLastShownVersion_set(let newValue): return newValue.intValue + case .p_lastReviewDate_get: return 0 + case .p_lastReviewDate_set(let newValue): return newValue.intValue + case .p_user_get: return 0 + case .p_user_set(let newValue): return newValue.intValue + case .p_userSettings_get: return 0 + case .p_userSettings_set(let newValue): return newValue.intValue + case .p_resetAppSupportDirectoryUserData_get: return 0 + case .p_resetAppSupportDirectoryUserData_set(let newValue): return newValue.intValue + case .p_useRelativeDates_get: return 0 + case .p_useRelativeDates_set(let newValue): return newValue.intValue } } func assertionName() -> String { switch self { - case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" - case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" - case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" - case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" - case .m_trackEvent__event: return ".trackEvent(_:)" - case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_clear: return ".clear()" + case .p_accessToken_get: return "[get] .accessToken" + case .p_accessToken_set: return "[set] .accessToken" + case .p_refreshToken_get: return "[get] .refreshToken" + case .p_refreshToken_set: return "[set] .refreshToken" + case .p_pushToken_get: return "[get] .pushToken" + case .p_pushToken_set: return "[set] .pushToken" + case .p_appleSignFullName_get: return "[get] .appleSignFullName" + case .p_appleSignFullName_set: return "[set] .appleSignFullName" + case .p_appleSignEmail_get: return "[get] .appleSignEmail" + case .p_appleSignEmail_set: return "[set] .appleSignEmail" + case .p_cookiesDate_get: return "[get] .cookiesDate" + case .p_cookiesDate_set: return "[set] .cookiesDate" + case .p_reviewLastShownVersion_get: return "[get] .reviewLastShownVersion" + case .p_reviewLastShownVersion_set: return "[set] .reviewLastShownVersion" + case .p_lastReviewDate_get: return "[get] .lastReviewDate" + case .p_lastReviewDate_set: return "[set] .lastReviewDate" + case .p_user_get: return "[get] .user" + case .p_user_set: return "[set] .user" + case .p_userSettings_get: return "[get] .userSettings" + case .p_userSettings_set: return "[set] .userSettings" + case .p_resetAppSupportDirectoryUserData_get: return "[get] .resetAppSupportDirectoryUserData" + case .p_resetAppSupportDirectoryUserData_set: return "[set] .resetAppSupportDirectoryUserData" + case .p_useRelativeDates_get: return "[get] .useRelativeDates" + case .p_useRelativeDates_set: return "[set] .useRelativeDates" } } } @@ -1279,41 +3247,81 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { super.init(products) } - + public static func accessToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_accessToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func refreshToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_refreshToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func pushToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_pushToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignFullName(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignFullName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignEmail(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_cookiesDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func reviewLastShownVersion(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_reviewLastShownVersion_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func lastReviewDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_lastReviewDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func user(getter defaultValue: DataLayer.User?...) -> PropertyStub { + return Given(method: .p_user_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func userSettings(getter defaultValue: UserSettings?...) -> PropertyStub { + return Given(method: .p_userSettings_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func resetAppSupportDirectoryUserData(getter defaultValue: Bool?...) -> PropertyStub { + return Given(method: .p_resetAppSupportDirectoryUserData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func useRelativeDates(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_useRelativeDates_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + } public struct Verify { fileprivate var method: MethodType - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} - public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func clear() -> Verify { return Verify(method: .m_clear)} + public static var accessToken: Verify { return Verify(method: .p_accessToken_get) } + public static func accessToken(set newValue: Parameter) -> Verify { return Verify(method: .p_accessToken_set(newValue)) } + public static var refreshToken: Verify { return Verify(method: .p_refreshToken_get) } + public static func refreshToken(set newValue: Parameter) -> Verify { return Verify(method: .p_refreshToken_set(newValue)) } + public static var pushToken: Verify { return Verify(method: .p_pushToken_get) } + public static func pushToken(set newValue: Parameter) -> Verify { return Verify(method: .p_pushToken_set(newValue)) } + public static var appleSignFullName: Verify { return Verify(method: .p_appleSignFullName_get) } + public static func appleSignFullName(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignFullName_set(newValue)) } + public static var appleSignEmail: Verify { return Verify(method: .p_appleSignEmail_get) } + public static func appleSignEmail(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignEmail_set(newValue)) } + public static var cookiesDate: Verify { return Verify(method: .p_cookiesDate_get) } + public static func cookiesDate(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesDate_set(newValue)) } + public static var reviewLastShownVersion: Verify { return Verify(method: .p_reviewLastShownVersion_get) } + public static func reviewLastShownVersion(set newValue: Parameter) -> Verify { return Verify(method: .p_reviewLastShownVersion_set(newValue)) } + public static var lastReviewDate: Verify { return Verify(method: .p_lastReviewDate_get) } + public static func lastReviewDate(set newValue: Parameter) -> Verify { return Verify(method: .p_lastReviewDate_set(newValue)) } + public static var user: Verify { return Verify(method: .p_user_get) } + public static func user(set newValue: Parameter) -> Verify { return Verify(method: .p_user_set(newValue)) } + public static var userSettings: Verify { return Verify(method: .p_userSettings_get) } + public static func userSettings(set newValue: Parameter) -> Verify { return Verify(method: .p_userSettings_set(newValue)) } + public static var resetAppSupportDirectoryUserData: Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_get) } + public static func resetAppSupportDirectoryUserData(set newValue: Parameter) -> Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_set(newValue)) } + public static var useRelativeDates: Verify { return Verify(method: .p_useRelativeDates_get) } + public static func useRelativeDates(set newValue: Parameter) -> Verify { return Verify(method: .p_useRelativeDates_set(newValue)) } } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) - } - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) - } - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { - return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) - } - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { - return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) - } - public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { - return Perform(method: .m_trackEvent__event(`event`), performs: perform) - } - public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + public static func clear(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_clear, performs: perform) } } @@ -1494,6 +3502,12 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { perform?(`courseId`, `courseName`) } + open func courseOutlineOfflineTabClicked(courseId: String, courseName: String) { + addInvocation(.m_courseOutlineOfflineTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) + let perform = methodPerformValue(.m_courseOutlineOfflineTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void + perform?(`courseId`, `courseName`) + } + open func courseOutlineDatesTabClicked(courseId: String, courseName: String) { addInvocation(.m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) let perform = methodPerformValue(.m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(Parameter.value(`courseId`), Parameter.value(`courseName`))) as? (String, String) -> Void @@ -1542,6 +3556,12 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { perform?(`event`, `biValue`, `courseID`) } + open func trackCourseScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) { + addInvocation(.m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`courseID`))) as? (AnalyticsEvent, EventBIValue, String) -> Void + perform?(`event`, `biValue`, `courseID`) + } + open func plsEvent(_ event: AnalyticsEvent, bivalue: EventBIValue, courseID: String, screenName: String, type: String) { addInvocation(.m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`courseID`), Parameter.value(`screenName`), Parameter.value(`type`))) let perform = methodPerformValue(.m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`courseID`), Parameter.value(`screenName`), Parameter.value(`type`))) as? (AnalyticsEvent, EventBIValue, String, String, String) -> Void @@ -1584,6 +3604,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) + case m_courseOutlineOfflineTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(Parameter, Parameter) @@ -1592,6 +3613,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case m_calendarSyncDialogAction__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIddialog_dialogaction_action(Parameter, Parameter, Parameter, Parameter, Parameter) case m_calendarSyncSnackbar__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIdsnackbar_snackbar(Parameter, Parameter, Parameter, Parameter) case m_trackCourseEvent__eventbiValue_biValuecourseID_courseID(Parameter, Parameter, Parameter) + case m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(Parameter, Parameter, Parameter) case m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(Parameter, Parameter, Parameter, Parameter, Parameter) case m_plsSuccessEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_typesuccess_success(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter) case m_bulkDownloadVideosToggle__courseID_courseIDaction_action(Parameter, Parameter) @@ -1673,6 +3695,12 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) return Matcher.ComparisonResult(results) + case (.m_courseOutlineOfflineTabClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseOutlineOfflineTabClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + return Matcher.ComparisonResult(results) + case (.m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(let lhsCourseid, let lhsCoursename), .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(let rhsCourseid, let rhsCoursename)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) @@ -1731,6 +3759,13 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) return Matcher.ComparisonResult(results) + case (.m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(let lhsEvent, let lhsBivalue, let lhsCourseid), .m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(let rhsEvent, let rhsBivalue, let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + case (.m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(let lhsEvent, let lhsBivalue, let lhsCourseid, let lhsScreenname, let lhsType), .m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(let rhsEvent, let rhsBivalue, let rhsCourseid, let rhsScreenname, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) @@ -1786,6 +3821,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case let .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue + case let .m_courseOutlineOfflineTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue @@ -1794,6 +3830,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case let .m_calendarSyncDialogAction__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIddialog_dialogaction_action(p0, p1, p2, p3, p4): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue case let .m_calendarSyncSnackbar__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIdsnackbar_snackbar(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue case let .m_trackCourseEvent__eventbiValue_biValuecourseID_courseID(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case let .m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(p0, p1, p2, p3, p4): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue case let .m_plsSuccessEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_typesuccess_success(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_bulkDownloadVideosToggle__courseID_courseIDaction_action(p0, p1): return p0.intValue + p1.intValue @@ -1813,6 +3850,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName: return ".finishVerticalBackToOutlineClicked(courseId:courseName:)" case .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineCourseTabClicked(courseId:courseName:)" case .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineVideosTabClicked(courseId:courseName:)" + case .m_courseOutlineOfflineTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineOfflineTabClicked(courseId:courseName:)" case .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineDatesTabClicked(courseId:courseName:)" case .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineDiscussionTabClicked(courseId:courseName:)" case .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName: return ".courseOutlineHandoutsTabClicked(courseId:courseName:)" @@ -1821,6 +3859,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { case .m_calendarSyncDialogAction__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIddialog_dialogaction_action: return ".calendarSyncDialogAction(enrollmentMode:pacing:courseId:dialog:action:)" case .m_calendarSyncSnackbar__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIdsnackbar_snackbar: return ".calendarSyncSnackbar(enrollmentMode:pacing:courseId:snackbar:)" case .m_trackCourseEvent__eventbiValue_biValuecourseID_courseID: return ".trackCourseEvent(_:biValue:courseID:)" + case .m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID: return ".trackCourseScreenEvent(_:biValue:courseID:)" case .m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type: return ".plsEvent(_:bivalue:courseID:screenName:type:)" case .m_plsSuccessEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_typesuccess_success: return ".plsSuccessEvent(_:bivalue:courseID:screenName:type:success:)" case .m_bulkDownloadVideosToggle__courseID_courseIDaction_action: return ".bulkDownloadVideosToggle(courseID:action:)" @@ -1854,6 +3893,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public static func finishVerticalBackToOutlineClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_finishVerticalBackToOutlineClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func courseOutlineCourseTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineCourseTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func courseOutlineVideosTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} + public static func courseOutlineOfflineTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineOfflineTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func courseOutlineDatesTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func courseOutlineDiscussionTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineDiscussionTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func courseOutlineHandoutsTabClicked(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseOutlineHandoutsTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} @@ -1862,6 +3902,7 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public static func calendarSyncDialogAction(enrollmentMode: Parameter, pacing: Parameter, courseId: Parameter, dialog: Parameter, action: Parameter) -> Verify { return Verify(method: .m_calendarSyncDialogAction__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIddialog_dialogaction_action(`enrollmentMode`, `pacing`, `courseId`, `dialog`, `action`))} public static func calendarSyncSnackbar(enrollmentMode: Parameter, pacing: Parameter, courseId: Parameter, snackbar: Parameter) -> Verify { return Verify(method: .m_calendarSyncSnackbar__enrollmentMode_enrollmentModepacing_pacingcourseId_courseIdsnackbar_snackbar(`enrollmentMode`, `pacing`, `courseId`, `snackbar`))} public static func trackCourseEvent(_ event: Parameter, biValue: Parameter, courseID: Parameter) -> Verify { return Verify(method: .m_trackCourseEvent__eventbiValue_biValuecourseID_courseID(`event`, `biValue`, `courseID`))} + public static func trackCourseScreenEvent(_ event: Parameter, biValue: Parameter, courseID: Parameter) -> Verify { return Verify(method: .m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(`event`, `biValue`, `courseID`))} public static func plsEvent(_ event: Parameter, bivalue: Parameter, courseID: Parameter, screenName: Parameter, type: Parameter) -> Verify { return Verify(method: .m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(`event`, `bivalue`, `courseID`, `screenName`, `type`))} public static func plsSuccessEvent(_ event: Parameter, bivalue: Parameter, courseID: Parameter, screenName: Parameter, type: Parameter, success: Parameter) -> Verify { return Verify(method: .m_plsSuccessEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_typesuccess_success(`event`, `bivalue`, `courseID`, `screenName`, `type`, `success`))} public static func bulkDownloadVideosToggle(courseID: Parameter, action: Parameter) -> Verify { return Verify(method: .m_bulkDownloadVideosToggle__courseID_courseIDaction_action(`courseID`, `action`))} @@ -1903,6 +3944,9 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public static func courseOutlineVideosTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_courseOutlineVideosTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) } + public static func courseOutlineOfflineTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_courseOutlineOfflineTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) + } public static func courseOutlineDatesTabClicked(courseId: Parameter, courseName: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_courseOutlineDatesTabClicked__courseId_courseIdcourseName_courseName(`courseId`, `courseName`), performs: perform) } @@ -1927,6 +3971,9 @@ open class CourseAnalyticsMock: CourseAnalytics, Mock { public static func trackCourseEvent(_ event: Parameter, biValue: Parameter, courseID: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String) -> Void) -> Perform { return Perform(method: .m_trackCourseEvent__eventbiValue_biValuecourseID_courseID(`event`, `biValue`, `courseID`), performs: perform) } + public static func trackCourseScreenEvent(_ event: Parameter, biValue: Parameter, courseID: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String) -> Void) -> Perform { + return Perform(method: .m_trackCourseScreenEvent__eventbiValue_biValuecourseID_courseID(`event`, `biValue`, `courseID`), performs: perform) + } public static func plsEvent(_ event: Parameter, bivalue: Parameter, courseID: Parameter, screenName: Parameter, type: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String, String) -> Void) -> Perform { return Perform(method: .m_plsEvent__eventbivalue_bivaluecourseID_courseIDscreenName_screenNametype_type(`event`, `bivalue`, `courseID`, `screenName`, `type`), performs: perform) } @@ -2107,6 +4154,22 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { return __value } + open func getSequentialsContainsBlocks(blockIds: [String], courseID: String) throws -> [CourseSequential] { + addInvocation(.m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(Parameter<[String]>.value(`blockIds`), Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(Parameter<[String]>.value(`blockIds`), Parameter.value(`courseID`))) as? ([String], String) -> Void + perform?(`blockIds`, `courseID`) + var __value: [CourseSequential] + do { + __value = try methodReturnValue(.m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(Parameter<[String]>.value(`blockIds`), Parameter.value(`courseID`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getSequentialsContainsBlocks(blockIds: [String], courseID: String). Use given") + Failure("Stub return value not specified for getSequentialsContainsBlocks(blockIds: [String], courseID: String). Use given") + } catch { + throw error + } + return __value + } + open func blockCompletionRequest(courseID: String, blockID: String) throws { addInvocation(.m_blockCompletionRequest__courseID_courseIDblockID_blockID(Parameter.value(`courseID`), Parameter.value(`blockID`))) let perform = methodPerformValue(.m_blockCompletionRequest__courseID_courseIDblockID_blockID(Parameter.value(`courseID`), Parameter.value(`blockID`))) as? (String, String) -> Void @@ -2233,6 +4296,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { case m_getCourseBlocks__courseID_courseID(Parameter) case m_getCourseVideoBlocks__fullStructure_fullStructure(Parameter) case m_getLoadedCourseBlocks__courseID_courseID(Parameter) + case m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(Parameter<[String]>, Parameter) case m_blockCompletionRequest__courseID_courseIDblockID_blockID(Parameter, Parameter) case m_getHandouts__courseID_courseID(Parameter) case m_getUpdates__courseID_courseID(Parameter) @@ -2259,6 +4323,12 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) return Matcher.ComparisonResult(results) + case (.m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(let lhsBlockids, let lhsCourseid), .m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(let rhsBlockids, let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockids, rhs: rhsBlockids, with: matcher), lhsBlockids, rhsBlockids, "blockIds")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + case (.m_blockCompletionRequest__courseID_courseIDblockID_blockID(let lhsCourseid, let lhsBlockid), .m_blockCompletionRequest__courseID_courseIDblockID_blockID(let rhsCourseid, let rhsBlockid)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) @@ -2309,6 +4379,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { case let .m_getCourseBlocks__courseID_courseID(p0): return p0.intValue case let .m_getCourseVideoBlocks__fullStructure_fullStructure(p0): return p0.intValue case let .m_getLoadedCourseBlocks__courseID_courseID(p0): return p0.intValue + case let .m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(p0, p1): return p0.intValue + p1.intValue case let .m_blockCompletionRequest__courseID_courseIDblockID_blockID(p0, p1): return p0.intValue + p1.intValue case let .m_getHandouts__courseID_courseID(p0): return p0.intValue case let .m_getUpdates__courseID_courseID(p0): return p0.intValue @@ -2324,6 +4395,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { case .m_getCourseBlocks__courseID_courseID: return ".getCourseBlocks(courseID:)" case .m_getCourseVideoBlocks__fullStructure_fullStructure: return ".getCourseVideoBlocks(fullStructure:)" case .m_getLoadedCourseBlocks__courseID_courseID: return ".getLoadedCourseBlocks(courseID:)" + case .m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID: return ".getSequentialsContainsBlocks(blockIds:courseID:)" case .m_blockCompletionRequest__courseID_courseIDblockID_blockID: return ".blockCompletionRequest(courseID:blockID:)" case .m_getHandouts__courseID_courseID: return ".getHandouts(courseID:)" case .m_getUpdates__courseID_courseID: return ".getUpdates(courseID:)" @@ -2354,6 +4426,9 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { public static func getLoadedCourseBlocks(courseID: Parameter, willReturn: CourseStructure...) -> MethodStub { return Given(method: .m_getLoadedCourseBlocks__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func getSequentialsContainsBlocks(blockIds: Parameter<[String]>, courseID: Parameter, willReturn: [CourseSequential]...) -> MethodStub { + return Given(method: .m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(`blockIds`, `courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func getHandouts(courseID: Parameter, willReturn: String?...) -> MethodStub { return Given(method: .m_getHandouts__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -2399,6 +4474,16 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { willProduce(stubber) return given } + public static func getSequentialsContainsBlocks(blockIds: Parameter<[String]>, courseID: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(`blockIds`, `courseID`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getSequentialsContainsBlocks(blockIds: Parameter<[String]>, courseID: Parameter, willProduce: (StubberThrows<[CourseSequential]>) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(`blockIds`, `courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: ([CourseSequential]).self) + willProduce(stubber) + return given + } public static func blockCompletionRequest(courseID: Parameter, blockID: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_blockCompletionRequest__courseID_courseIDblockID_blockID(`courseID`, `blockID`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -2487,6 +4572,7 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { public static func getCourseBlocks(courseID: Parameter) -> Verify { return Verify(method: .m_getCourseBlocks__courseID_courseID(`courseID`))} public static func getCourseVideoBlocks(fullStructure: Parameter) -> Verify { return Verify(method: .m_getCourseVideoBlocks__fullStructure_fullStructure(`fullStructure`))} public static func getLoadedCourseBlocks(courseID: Parameter) -> Verify { return Verify(method: .m_getLoadedCourseBlocks__courseID_courseID(`courseID`))} + public static func getSequentialsContainsBlocks(blockIds: Parameter<[String]>, courseID: Parameter) -> Verify { return Verify(method: .m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(`blockIds`, `courseID`))} public static func blockCompletionRequest(courseID: Parameter, blockID: Parameter) -> Verify { return Verify(method: .m_blockCompletionRequest__courseID_courseIDblockID_blockID(`courseID`, `blockID`))} public static func getHandouts(courseID: Parameter) -> Verify { return Verify(method: .m_getHandouts__courseID_courseID(`courseID`))} public static func getUpdates(courseID: Parameter) -> Verify { return Verify(method: .m_getUpdates__courseID_courseID(`courseID`))} @@ -2510,6 +4596,9 @@ open class CourseInteractorProtocolMock: CourseInteractorProtocol, Mock { public static func getLoadedCourseBlocks(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_getLoadedCourseBlocks__courseID_courseID(`courseID`), performs: perform) } + public static func getSequentialsContainsBlocks(blockIds: Parameter<[String]>, courseID: Parameter, perform: @escaping ([String], String) -> Void) -> Perform { + return Perform(method: .m_getSequentialsContainsBlocks__blockIds_blockIdscourseID_courseID(`blockIds`, `courseID`), performs: perform) + } public static func blockCompletionRequest(courseID: Parameter, blockID: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_blockCompletionRequest__courseID_courseIDblockID_blockID(`courseID`, `blockID`), performs: perform) } @@ -2804,6 +4893,20 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func updateUnzippedFileSize(for sequentials: [CourseSequential]) -> [CourseSequential] { + addInvocation(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) + let perform = methodPerformValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) as? ([CourseSequential]) -> Void + perform?(`sequentials`) + var __value: [CourseSequential] + do { + __value = try methodReturnValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))).casted() + } catch { + onFatalFailure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + Failure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + } + return __value + } + open func resumeDownloading() throws { addInvocation(.m_resumeDownloading) let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void @@ -2831,6 +4934,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func removeAppSupportDirectoryUnusedContent() { + addInvocation(.m_removeAppSupportDirectoryUnusedContent) + let perform = methodPerformValue(.m_removeAppSupportDirectoryUnusedContent) as? () -> Void + perform?() + } + fileprivate enum MethodType { case m_publisher @@ -2845,8 +4954,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>) case m_resumeDownloading case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_removeAppSupportDirectoryUnusedContent case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2897,12 +5008,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + case (.m_updateUnzippedFileSize__for_sequentials(let lhsSequentials), .m_updateUnzippedFileSize__for_sequentials(let rhsSequentials)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSequentials, rhs: rhsSequentials, with: matcher), lhsSequentials, rhsSequentials, "for sequentials")) + return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) + + case (.m_removeAppSupportDirectoryUnusedContent, .m_removeAppSupportDirectoryUnusedContent): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -2922,8 +5040,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_updateUnzippedFileSize__for_sequentials(p0): return p0.intValue case .m_resumeDownloading: return 0 case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_removeAppSupportDirectoryUnusedContent: return 0 case .p_currentDownloadTask_get: return 0 } } @@ -2941,8 +5061,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_updateUnzippedFileSize__for_sequentials: return ".updateUnzippedFileSize(for:)" case .m_resumeDownloading: return ".resumeDownloading()" case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_removeAppSupportDirectoryUnusedContent: return ".removeAppSupportDirectoryUnusedContent()" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -2975,6 +5097,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willReturn: [CourseSequential]...) -> MethodStub { + return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -3013,6 +5138,13 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willProduce: (Stubber<[CourseSequential]>) -> Void) -> MethodStub { + let willReturn: [[CourseSequential]] = [] + let given: Given = { return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseSequential]).self) + willProduce(stubber) + return given + } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { let willReturn: [Bool] = [] let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -3097,8 +5229,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>) -> Verify { return Verify(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func removeAppSupportDirectoryUnusedContent() -> Verify { return Verify(method: .m_removeAppSupportDirectoryUnusedContent)} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -3142,12 +5276,217 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, perform: @escaping ([CourseSequential]) -> Void) -> Perform { + return Perform(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), performs: perform) + } public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } + public static func removeAppSupportDirectoryUnusedContent(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeAppSupportDirectoryUnusedContent, performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - OfflineSyncInteractorProtocol + +open class OfflineSyncInteractorProtocolMock: OfflineSyncInteractorProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func submitOfflineProgress(courseID: String, blockID: String, data: String) throws -> Bool { + addInvocation(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) + let perform = methodPerformValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) as? (String, String, String) -> Void + perform?(`courseID`, `blockID`, `data`) + var __value: Bool + do { + __value = try methodReturnValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + Failure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + } catch { + throw error + } + return __value + } + + + fileprivate enum MethodType { + case m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter, Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let lhsCourseid, let lhsBlockid, let lhsData), .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let rhsCourseid, let rhsBlockid, let rhsData)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsData, rhs: rhsData, with: matcher), lhsData, rhsData, "data")) + return Matcher.ComparisonResult(results) + } + } + + func intValue() -> Int { + switch self { + case let .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + } + } + func assertionName() -> String { + switch self { + case .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data: return ".submitOfflineProgress(courseID:blockID:data:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Bool).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter) -> Verify { return Verify(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { + return Perform(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), performs: perform) + } } public func given(_ method: Given) { diff --git a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift index 435539972..c87824041 100644 --- a/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Container/CourseContainerViewModelTests.swift @@ -40,6 +40,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -48,14 +49,16 @@ final class CourseContainerViewModelTests: XCTestCase { id: "", courseId: "123", topicId: "", - graded: true, + graded: true, + due: Date(), completion: 0, type: .problem, displayName: "", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ) let vertical = CourseVertical( blockId: "", @@ -64,7 +67,8 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .vertical, completion: 0, - childs: [block] + childs: [block], + webUrl: "" ) let sequential = CourseSequential( blockId: "", @@ -72,7 +76,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( blockId: "", @@ -98,7 +104,8 @@ final class CourseContainerViewModelTests: XCTestCase { large: "")), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let resumeBlock = ResumeBlock(blockID: "123") @@ -148,6 +155,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -165,7 +173,8 @@ final class CourseContainerViewModelTests: XCTestCase { large: "")), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(interactor, .getLoadedCourseBlocks(courseID: .any, willReturn: courseStructure)) @@ -207,6 +216,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -220,9 +230,8 @@ final class CourseContainerViewModelTests: XCTestCase { Verify(interactor, .getCourseBlocks(courseID: .any)) XCTAssertFalse(viewModel.isShowProgress) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) XCTAssertNil(viewModel.courseStructure) + XCTAssertNil(viewModel.courseVideosStructure) } func testGetCourseBlocksNoCacheError() async throws { @@ -250,6 +259,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -260,9 +270,8 @@ final class CourseContainerViewModelTests: XCTestCase { Verify(interactor, .getCourseBlocks(courseID: .any)) XCTAssertFalse(viewModel.isShowProgress) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) XCTAssertNil(viewModel.courseStructure) + XCTAssertNil(viewModel.courseVideosStructure) } func testGetCourseBlocksUnknownError() async throws { @@ -290,6 +299,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -300,9 +310,8 @@ final class CourseContainerViewModelTests: XCTestCase { Verify(interactor, .getCourseBlocks(courseID: .any)) XCTAssertFalse(viewModel.isShowProgress) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) XCTAssertNil(viewModel.courseStructure) + XCTAssertNil(viewModel.courseVideosStructure) } func testTabSelectedAnalytics() { @@ -330,6 +339,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) @@ -363,6 +373,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -376,8 +387,8 @@ final class CourseContainerViewModelTests: XCTestCase { mobileLow: nil, hls: nil ), - multiDevice: true - + multiDevice: true, + offlineDownload: nil ) let vertical = CourseVertical( @@ -387,7 +398,8 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .vertical, completion: 0, - childs: [block] + childs: [block], + webUrl: "" ) let sequential = CourseSequential( @@ -396,7 +408,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -423,7 +437,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( @@ -438,15 +453,18 @@ final class CourseContainerViewModelTests: XCTestCase { resumeData: nil, state: .inProgress, type: .video, - fileSize: 1000 + fileSize: 1000, + lastModified: "" ) Given(connectivity, .isInternetAvaliable(getter: true)) Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(connectivity, .isMobileData(getter: false)) Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [downloadData])) + Given(downloadManager, .updateUnzippedFileSize(for: .any, willReturn: [sequential])) let viewModel = CourseContainerViewModel( interactor: interactor, @@ -462,16 +480,17 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure - await viewModel.setDownloadsStates() + await viewModel.setDownloadsStates(courseStructure: courseStructure) - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: blockId, - state: .available - ) + await viewModel.download( + state: .available, + blocks: [block], + sequentials: [sequential] + ) let exp = expectation(description: "Task Starting") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -482,6 +501,7 @@ final class CourseContainerViewModelTests: XCTestCase { XCTAssertEqual(viewModel.sequentialsDownloadState[blockId], .downloading) } + func testOnDownloadViewDownloadingTap() async { let interactor = CourseInteractorProtocolMock() @@ -500,6 +520,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -513,7 +534,8 @@ final class CourseContainerViewModelTests: XCTestCase { mobileLow: nil, hls: nil ), - multiDevice: true + multiDevice: true, + offlineDownload: nil ) let vertical = CourseVertical( @@ -523,7 +545,8 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .vertical, completion: 0, - childs: [block] + childs: [block], + webUrl: "" ) let sequential = CourseSequential( @@ -532,7 +555,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -559,15 +584,18 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(connectivity, .isInternetAvaliable(getter: true)) Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(connectivity, .isMobileData(getter: false)) Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [])) + Given(downloadManager, .updateUnzippedFileSize(for: .any, willReturn: [])) let viewModel = CourseContainerViewModel( interactor: interactor, @@ -583,16 +611,17 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure - await viewModel.setDownloadsStates() + await viewModel.setDownloadsStates(courseStructure: courseStructure) - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: blockId, - state: .downloading - ) + await viewModel.download( + state: .available, + blocks: [block], + sequentials: [sequential] + ) let exp = expectation(description: "Task Starting") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -621,6 +650,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -634,7 +664,8 @@ final class CourseContainerViewModelTests: XCTestCase { mobileLow: nil, hls: nil ), - multiDevice: true + multiDevice: true, + offlineDownload: nil ) let vertical = CourseVertical( @@ -644,7 +675,8 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .vertical, completion: 0, - childs: [block] + childs: [block], + webUrl: "" ) let sequential = CourseSequential( @@ -653,7 +685,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -680,15 +714,18 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(connectivity, .isInternetAvaliable(getter: true)) Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(connectivity, .isMobileData(getter: false)) Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [])) + Given(downloadManager, .updateUnzippedFileSize(for: .any, willReturn: [])) let viewModel = CourseContainerViewModel( interactor: interactor, @@ -704,16 +741,17 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure - await viewModel.setDownloadsStates() + await viewModel.setDownloadsStates(courseStructure: courseStructure) - await viewModel.onDownloadViewTap( - chapter: chapter, - blockId: blockId, - state: .finished - ) + await viewModel.download( + state: .available, + blocks: [block], + sequentials: [sequential] + ) let exp = expectation(description: "Task Starting") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -743,6 +781,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -756,7 +795,8 @@ final class CourseContainerViewModelTests: XCTestCase { mobileLow: nil, hls: nil ), - multiDevice: true + multiDevice: true, + offlineDownload: nil ) let vertical = CourseVertical( @@ -766,7 +806,8 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .vertical, completion: 0, - childs: [block] + childs: [block], + webUrl: "" ) let sequential = CourseSequential( @@ -775,7 +816,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -802,15 +845,18 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(connectivity, .isInternetAvaliable(getter: true)) Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(connectivity, .isMobileData(getter: false)) Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [])) + Given(downloadManager, .updateUnzippedFileSize(for: .any, willReturn: [])) let viewModel = CourseContainerViewModel( interactor: interactor, @@ -826,10 +872,11 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure - await viewModel.setDownloadsStates() + await viewModel.setDownloadsStates(courseStructure: courseStructure) let exp = expectation(description: "Task Starting") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -858,6 +905,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -871,7 +919,8 @@ final class CourseContainerViewModelTests: XCTestCase { mobileLow: nil, hls: nil ), - multiDevice: true + multiDevice: true, + offlineDownload: nil ) let vertical = CourseVertical( @@ -881,7 +930,8 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .vertical, completion: 0, - childs: [block] + childs: [block], + webUrl: "" ) let sequential = CourseSequential( @@ -890,7 +940,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -917,7 +969,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( @@ -932,15 +985,18 @@ final class CourseContainerViewModelTests: XCTestCase { resumeData: nil, state: .inProgress, type: .video, - fileSize: 1000 + fileSize: 1000, + lastModified: "" ) Given(connectivity, .isInternetAvaliable(getter: true)) Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(connectivity, .isMobileData(getter: false)) Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [downloadData])) + Given(downloadManager, .updateUnzippedFileSize(for: .any, willReturn: [sequential])) let viewModel = CourseContainerViewModel( interactor: interactor, @@ -956,10 +1012,11 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure - await viewModel.setDownloadsStates() + await viewModel.setDownloadsStates(courseStructure: courseStructure) let exp = expectation(description: "Task Starting") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -988,6 +1045,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -1001,7 +1059,8 @@ final class CourseContainerViewModelTests: XCTestCase { mobileLow: nil, hls: nil ), - multiDevice: true + multiDevice: true, + offlineDownload: nil ) let vertical = CourseVertical( @@ -1011,7 +1070,8 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .vertical, completion: 0, - childs: [block] + childs: [block], + webUrl: "" ) let sequential = CourseSequential( @@ -1020,7 +1080,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -1047,7 +1109,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( @@ -1062,15 +1125,18 @@ final class CourseContainerViewModelTests: XCTestCase { resumeData: nil, state: .finished, type: .video, - fileSize: 1000 + fileSize: 1000, + lastModified: "" ) Given(connectivity, .isInternetAvaliable(getter: true)) Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(connectivity, .isMobileData(getter: false)) Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [downloadData])) + Given(downloadManager, .updateUnzippedFileSize(for: .any, willReturn: [sequential])) let viewModel = CourseContainerViewModel( interactor: interactor, @@ -1086,10 +1152,11 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure - await viewModel.setDownloadsStates() + await viewModel.setDownloadsStates(courseStructure: courseStructure) let exp = expectation(description: "Task Starting") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -1117,6 +1184,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -1130,7 +1198,8 @@ final class CourseContainerViewModelTests: XCTestCase { mobileLow: nil, hls: nil ), - multiDevice: true + multiDevice: true, + offlineDownload: nil ) let block2 = CourseBlock( blockId: "123", @@ -1138,6 +1207,7 @@ final class CourseContainerViewModelTests: XCTestCase { courseId: "123", topicId: "", graded: false, + due: Date(), completion: 0, type: .video, displayName: "", @@ -1151,7 +1221,8 @@ final class CourseContainerViewModelTests: XCTestCase { mobileLow: nil, hls: nil ), - multiDevice: true + multiDevice: true, + offlineDownload: nil ) let vertical = CourseVertical( @@ -1161,7 +1232,8 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .vertical, completion: 0, - childs: [block, block2] + childs: [block, block2], + webUrl: "" ) let sequential = CourseSequential( @@ -1170,7 +1242,9 @@ final class CourseContainerViewModelTests: XCTestCase { displayName: "", type: .chapter, completion: 0, - childs: [vertical] + childs: [vertical], + sequentialProgress: nil, + due: Date() ) let chapter = CourseChapter( @@ -1197,7 +1271,8 @@ final class CourseContainerViewModelTests: XCTestCase { )), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) let downloadData = DownloadDataTask( @@ -1212,15 +1287,18 @@ final class CourseContainerViewModelTests: XCTestCase { resumeData: nil, state: .finished, type: .video, - fileSize: 1000 + fileSize: 1000, + lastModified: "" ) Given(connectivity, .isInternetAvaliable(getter: true)) Given(connectivity, .internetReachableSubject(getter: .init(.reachable))) + Given(connectivity, .isMobileData(getter: false)) Given(downloadManager, .publisher(willReturn: Empty().eraseToAnyPublisher())) Given(downloadManager, .eventPublisher(willReturn: Just(.added).eraseToAnyPublisher())) Given(downloadManager, .getDownloadTasksForCourse(.any, willReturn: [downloadData])) + Given(downloadManager, .updateUnzippedFileSize(for: .any, willReturn: [sequential])) let viewModel = CourseContainerViewModel( interactor: interactor, @@ -1236,10 +1314,11 @@ final class CourseContainerViewModelTests: XCTestCase { courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, + lastVisitedBlockID: nil, coreAnalytics: CoreAnalyticsMock() ) viewModel.courseStructure = courseStructure - await viewModel.setDownloadsStates() + await viewModel.setDownloadsStates(courseStructure: courseStructure) let exp = expectation(description: "Task Starting") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { diff --git a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift index 74d006cbc..34cde8e6e 100644 --- a/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/CourseDateViewModelTests.swift @@ -46,7 +46,8 @@ final class CourseDateViewModelTests: XCTestCase { large: "")), certificate: nil, org: "", - isSelfPaced: true + isSelfPaced: true, + courseProgress: nil ) Given(interactor, .getCourseDates(courseID: .any, willReturn: courseDates)) @@ -60,7 +61,8 @@ final class CourseDateViewModelTests: XCTestCase { config: config, courseID: "1", courseName: "a", - analytics: CourseAnalyticsMock() + analytics: CourseAnalyticsMock(), + calendarManager: CalendarManagerMock() ) await viewModel.getCourseDates(courseID: "1") @@ -90,15 +92,16 @@ final class CourseDateViewModelTests: XCTestCase { config: config, courseID: "1", courseName: "a", - analytics: CourseAnalyticsMock() + analytics: CourseAnalyticsMock(), + calendarManager: CalendarManagerMock() ) await viewModel.getCourseDates(courseID: "1") Verify(interactor, .getCourseDates(courseID: .any)) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError, "Error view should be shown on unknown error.") + XCTAssertNil(viewModel.courseDates) + XCTAssertFalse(viewModel.isShowProgress) } func testNoInternetConnectionError() async throws { @@ -120,15 +123,16 @@ final class CourseDateViewModelTests: XCTestCase { config: config, courseID: "1", courseName: "a", - analytics: CourseAnalyticsMock() + analytics: CourseAnalyticsMock(), + calendarManager: CalendarManagerMock() ) await viewModel.getCourseDates(courseID: "1") Verify(interactor, .getCourseDates(courseID: .any)) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection, "Error message should be set to 'slow or no internet connection'.") + XCTAssertNil(viewModel.courseDates) + XCTAssertFalse(viewModel.isShowProgress) } func testSortedDateTodayToCourseDateBlockDict() { @@ -143,7 +147,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockID1" + firstComponentBlockID: "blockID1", + useRelativeDates: true ) let block2 = CourseDateBlock( @@ -157,7 +162,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockID1" + firstComponentBlockID: "blockID1", + useRelativeDates: true ) let courseDates = CourseDates( @@ -191,7 +197,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockID1" + firstComponentBlockID: "blockID1", + useRelativeDates: true ) let block2 = CourseDateBlock( @@ -205,7 +212,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockID1" + firstComponentBlockID: "blockID1", + useRelativeDates: true ) let courseDates = CourseDates( @@ -238,7 +246,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestAssignment", extraInfo: nil, - firstComponentBlockID: "blockID3" + firstComponentBlockID: "blockID3", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .dueNext) @@ -256,7 +265,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: CourseLocalization.CourseDates.today, extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.title, "Today", "Block title for 'today' should be 'Today'") @@ -274,7 +284,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .completed, "Block status for a completed assignment should be 'completed'") } @@ -291,7 +302,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .verifiedOnly, "Block status for a block without learner access should be 'verifiedOnly'") @@ -309,7 +321,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .pastDue, "Block status for a past due assignment should be 'pastDue'") @@ -327,7 +340,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertTrue(availableAssignment.canShowLink, "Available assignments should be hyperlinked.") } @@ -344,7 +358,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertTrue(block.isAssignment) @@ -362,7 +377,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, BlockStatus.courseStartDate) @@ -380,7 +396,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, BlockStatus.courseEndDate) @@ -398,7 +415,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertTrue(block.isVerifiedOnly, "Block should be identified as 'verified only' when the learner has no access.") @@ -416,7 +434,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertTrue(block.isComplete, "Block should be marked as completed.") @@ -434,7 +453,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .unreleased, "Block status should be set to 'unreleased' for unreleased assignments.") @@ -452,7 +472,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .verifiedOnly) @@ -471,7 +492,8 @@ final class CourseDateViewModelTests: XCTestCase { linkText: nil, title: "TestBlock", extraInfo: nil, - firstComponentBlockID: "blockIDTest" + firstComponentBlockID: "blockIDTest", + useRelativeDates: true ) XCTAssertEqual(block.blockStatus, .unreleased) diff --git a/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift b/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift index 1e206e81e..8a43f206e 100644 --- a/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/CourseUnitViewModelTests.swift @@ -20,53 +20,61 @@ final class CourseUnitViewModelTests: XCTestCase { id: "1", courseId: "123", topicId: "1", - graded: false, + graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 1", studentUrl: "", webUrl: "", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ), CourseBlock(blockId: "2", id: "2", courseId: "123", topicId: "2", graded: false, + due: Date(), completion: 0, type: .video, displayName: "Lesson 2", studentUrl: "2", webUrl: "2", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ), CourseBlock(blockId: "3", id: "3", courseId: "123", topicId: "3", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "Lesson 3", studentUrl: "3", webUrl: "3", encodedVideo: nil, - multiDevice: true + multiDevice: true, + offlineDownload: nil ), CourseBlock(blockId: "4", id: "4", courseId: "123", topicId: "4", graded: false, + due: Date(), completion: 0, type: .unknown, displayName: "4", studentUrl: "4", webUrl: "4", encodedVideo: nil, - multiDevice: false + multiDevice: false, + offlineDownload: nil ), ] @@ -77,20 +85,27 @@ final class CourseUnitViewModelTests: XCTestCase { displayName: "0", type: .chapter, childs: [ - CourseSequential(blockId: "5", - id: "5", - displayName: "5", - type: .sequential, - completion: 0, - childs: [ - CourseVertical(blockId: "6", - id: "6", - courseId: "123", - displayName: "6", - type: .vertical, - completion: 0, - childs: blocks) - ]) + CourseSequential( + blockId: "5", + id: "5", + displayName: "5", + type: .sequential, + completion: 0, + childs: [ + CourseVertical( + blockId: "6", + id: "6", + courseId: "123", + displayName: "6", + type: .vertical, + completion: 0, + childs: blocks, + webUrl: "" + ) + ], + sequentialProgress: nil, + due: Date() + ) ]), CourseChapter( @@ -99,20 +114,27 @@ final class CourseUnitViewModelTests: XCTestCase { displayName: "2", type: .chapter, childs: [ - CourseSequential(blockId: "3", - id: "3", - displayName: "3", - type: .sequential, - completion: 0, - childs: [ - CourseVertical(blockId: "4", - id: "4", - courseId: "123", - displayName: "4", - type: .vertical, - completion: 0, - childs: blocks) - ]) + CourseSequential( + blockId: "3", + id: "3", + displayName: "3", + type: .sequential, + completion: 0, + childs: [ + CourseVertical( + blockId: "4", + id: "4", + courseId: "123", + displayName: "4", + type: .vertical, + completion: 0, + childs: blocks, + webUrl: "" + ) + ], + sequentialProgress: nil, + due: Date() + ) ]) ] diff --git a/Course/CourseTests/Presentation/Unit/HandoutsViewModelTests.swift b/Course/CourseTests/Presentation/Unit/HandoutsViewModelTests.swift index c5874f90c..bad7ec2f6 100644 --- a/Course/CourseTests/Presentation/Unit/HandoutsViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/HandoutsViewModelTests.swift @@ -66,8 +66,6 @@ final class HandoutsViewModelTests: XCTestCase { XCTAssert(viewModel.handouts == nil) XCTAssertFalse(viewModel.isShowProgress) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) } func testGetHandoutsUnknownError() async throws { @@ -92,8 +90,6 @@ final class HandoutsViewModelTests: XCTestCase { XCTAssert(viewModel.handouts == nil) XCTAssertFalse(viewModel.isShowProgress) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) } func testGetUpdatesSuccess() async throws { @@ -146,8 +142,6 @@ final class HandoutsViewModelTests: XCTestCase { XCTAssertTrue(viewModel.updates.isEmpty) XCTAssertFalse(viewModel.isShowProgress) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) } func testGetUpdatesUnknownError() async throws { @@ -172,8 +166,6 @@ final class HandoutsViewModelTests: XCTestCase { XCTAssertTrue(viewModel.updates.isEmpty) XCTAssertFalse(viewModel.isShowProgress) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) } } diff --git a/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift b/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift index 2a6b2f722..a083fa577 100644 --- a/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift +++ b/Course/CourseTests/Presentation/Unit/VideoPlayerViewModelTests.swift @@ -33,13 +33,10 @@ final class VideoPlayerViewModelTests: XCTestCase { Given(interactor, .getSubtitles(url: .any, selectedLanguage: .any, willReturn: subtitles)) - let viewModel = VideoPlayerViewModel(blockID: "", - courseID: "", - languages: [], - interactor: interactor, - router: router, - appStorage: CoreStorageMock(), - connectivity: connectivity) + let tracker = PlayerTrackerProtocolMock(url: nil) + let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) + let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service) + let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder) await viewModel.getSubtitles(subtitlesUrl: "url") @@ -60,13 +57,10 @@ final class VideoPlayerViewModelTests: XCTestCase { Given(connectivity, .isInternetAvaliable(getter: false)) Given(interactor, .getSubtitles(url: .any, selectedLanguage: .any, willReturn: subtitles)) - let viewModel = VideoPlayerViewModel(blockID: "", - courseID: "", - languages: [], - interactor: interactor, - router: router, - appStorage: CoreStorageMock(), - connectivity: connectivity) + let tracker = PlayerTrackerProtocolMock(url: nil) + let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) + let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service) + let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder) await viewModel.getSubtitles(subtitlesUrl: "url") @@ -82,14 +76,11 @@ final class VideoPlayerViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = VideoPlayerViewModel(blockID: "", - courseID: "", - languages: [], - interactor: interactor, - router: router, - appStorage: CoreStorageMock(), - connectivity: connectivity) - + let tracker = PlayerTrackerProtocolMock(url: nil) + let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) + let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service) + let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder) + viewModel.languages = [ SubtitleUrl(language: "en", url: "url"), SubtitleUrl(language: "uk", url: "url2") @@ -110,17 +101,13 @@ final class VideoPlayerViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = VideoPlayerViewModel(blockID: "", - courseID: "", - languages: [], - interactor: interactor, - router: router, - appStorage: CoreStorageMock(), - connectivity: connectivity) + let tracker = PlayerTrackerProtocolMock(url: nil) + let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) + let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willProduce: {_ in})) - await viewModel.blockCompletionRequest() + await playerHolder.sendCompletion() Verify(interactor, .blockCompletionRequest(courseID: .any, blockID: .any)) } @@ -130,20 +117,24 @@ final class VideoPlayerViewModelTests: XCTestCase { let router = CourseRouterMock() let connectivity = ConnectivityProtocolMock() - let viewModel = VideoPlayerViewModel(blockID: "", - courseID: "", - languages: [], - interactor: interactor, - router: router, - appStorage: CoreStorageMock(), - connectivity: connectivity) + let tracker = PlayerTrackerProtocolMock(url: nil) + let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) + let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service) + let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: NSError())) - await viewModel.blockCompletionRequest() + await playerHolder.sendCompletion() Verify(interactor, .blockCompletionRequest(courseID: .any, blockID: .any)) + let expectation = XCTestExpectation(description: "Wait for combine") + + DispatchQueue.main.asyncAfter(deadline: .now()+0.1) { + expectation.fulfill() + } + await fulfillment(of: [expectation], timeout: 1) + XCTAssertTrue(viewModel.showError) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) } @@ -155,17 +146,21 @@ final class VideoPlayerViewModelTests: XCTestCase { let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) - let viewModel = VideoPlayerViewModel(blockID: "", - courseID: "", - languages: [], - interactor: interactor, - router: router, - appStorage: CoreStorageMock(), - connectivity: connectivity) + let tracker = PlayerTrackerProtocolMock(url: nil) + let service = PlayerService(courseID: "", blockID: "", interactor: interactor, router: router) + let playerHolder = PlayerViewControllerHolder(url: nil, blockID: "", courseID: "", selectedCourseTab: 0, videoResolution: .zero, pipManager: PipManagerProtocolMock(), playerTracker: tracker, playerDelegate: nil, playerService: service) + let viewModel = VideoPlayerViewModel(languages: [], connectivity: connectivity, playerHolder: playerHolder) Given(interactor, .blockCompletionRequest(courseID: .any, blockID: .any, willThrow: noInternetError)) - await viewModel.blockCompletionRequest() + await playerHolder.sendCompletion() + + let expectation = XCTestExpectation(description: "Wait for combine") + + DispatchQueue.main.asyncAfter(deadline: .now()+0.1) { + expectation.fulfill() + } + await fulfillment(of: [expectation], timeout: 1) Verify(interactor, .blockCompletionRequest(courseID: .any, blockID: .any)) diff --git a/Course/Mockfile b/Course/Mockfile index 58cd4b263..42d9b4b0e 100644 --- a/Course/Mockfile +++ b/Course/Mockfile @@ -14,4 +14,5 @@ unit.tests.mock: - Course - Foundation - SwiftUI - - Combine \ No newline at end of file + - Combine + - OEXFoundation \ No newline at end of file diff --git a/Dashboard/Dashboard.xcodeproj/project.pbxproj b/Dashboard/Dashboard.xcodeproj/project.pbxproj index 2a7b812fe..5688cafb4 100644 --- a/Dashboard/Dashboard.xcodeproj/project.pbxproj +++ b/Dashboard/Dashboard.xcodeproj/project.pbxproj @@ -7,14 +7,24 @@ objects = { /* Begin PBXBuildFile section */ - 027DB33328D8BDBA002B6862 /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33228D8BDBA002B6862 /* DashboardView.swift */; }; + 0277241E2BCE9E1500C2908D /* PrimaryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0277241D2BCE9E1500C2908D /* PrimaryCardView.swift */; }; + 027724202BCEA16C00C2908D /* ProgressLineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0277241F2BCEA16C00C2908D /* ProgressLineView.swift */; }; + 027DB33328D8BDBA002B6862 /* ListDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33228D8BDBA002B6862 /* ListDashboardView.swift */; }; 027DB33928D8D9F6002B6862 /* DashboardEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33828D8D9F6002B6862 /* DashboardEndpoint.swift */; }; 027DB33D28D8DB5E002B6862 /* DashboardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB33C28D8DB5E002B6862 /* DashboardRepository.swift */; }; 027DB33F28D8E605002B6862 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 027DB33E28D8E605002B6862 /* Core.framework */; }; 027DB34328D8E89B002B6862 /* DashboardInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB34228D8E89B002B6862 /* DashboardInteractor.swift */; }; - 027DB34528D8E9D2002B6862 /* DashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */; }; + 027DB34528D8E9D2002B6862 /* ListDashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027DB34428D8E9D2002B6862 /* ListDashboardViewModel.swift */; }; + 028895672BE3B34F00102D8C /* NoCoursesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028895662BE3B34E00102D8C /* NoCoursesView.swift */; }; + 02935B6F2BCEC91100B22F66 /* PrimaryCourseDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B6E2BCEC91100B22F66 /* PrimaryCourseDashboardView.swift */; }; + 02935B712BCEC91F00B22F66 /* PrimaryCourseDashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B702BCEC91F00B22F66 /* PrimaryCourseDashboardViewModel.swift */; }; + 02935B772BCFB2C100B22F66 /* CourseCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02935B762BCFB2C100B22F66 /* CourseCardView.swift */; }; 02A48B18295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 02A48B16295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld */; }; 02A48B1A295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A48B19295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift */; }; + 02A98F112BD96814009B46BD /* DropDownMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A98F102BD96814009B46BD /* DropDownMenu.swift */; }; + 02A98F132BD96B9D009B46BD /* AllCoursesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A98F122BD96B9D009B46BD /* AllCoursesView.swift */; }; + 02A98F152BD96BC2009B46BD /* AllCoursesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A98F142BD96BC2009B46BD /* AllCoursesViewModel.swift */; }; + 02A98F172BD97886009B46BD /* CategoryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A98F162BD97885009B46BD /* CategoryFilterView.swift */; }; 02A9A90B2978194100B55797 /* DashboardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A90A2978194100B55797 /* DashboardViewModelTests.swift */; }; 02A9A90C2978194100B55797 /* Dashboard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02EF39E728D89F560058F6BD /* Dashboard.framework */; platformFilter = ios; }; 02A9A92929781A4D00B55797 /* DashboardMock.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A92829781A4D00B55797 /* DashboardMock.generated.swift */; }; @@ -26,6 +36,9 @@ 214DA1AADABC7BF4FB8EA1D7 /* Pods_App_Dashboard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B008B2F0762EF35CADE3DD4 /* Pods_App_Dashboard.framework */; }; 97E7DF0B2B7A3EAF00A2A09B /* CourseEnrollmentsMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E7DF0A2B7A3EAF00A2A09B /* CourseEnrollmentsMock.swift */; }; 9AD4A6A1AAF97092CF457FE2 /* Pods_App_Dashboard_DashboardTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22905947A936093AD23D4CF8 /* Pods_App_Dashboard_DashboardTests.framework */; }; + CE1735062CD2552A00F9606A /* PrimaryCourseDashboardViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1735052CD2552A00F9606A /* PrimaryCourseDashboardViewModelTests.swift */; }; + CE17350A2CD26CB500F9606A /* AllCoursesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1735092CD26CB500F9606A /* AllCoursesViewModelTests.swift */; }; + CEB1E26D2CC14E9B00921517 /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CEB1E26C2CC14E9B00921517 /* OEXFoundation */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -39,18 +52,27 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 027DB33228D8BDBA002B6862 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; }; + 0277241D2BCE9E1500C2908D /* PrimaryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryCardView.swift; sourceTree = ""; }; + 0277241F2BCEA16C00C2908D /* ProgressLineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressLineView.swift; sourceTree = ""; }; + 027DB33228D8BDBA002B6862 /* ListDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDashboardView.swift; sourceTree = ""; }; 027DB33828D8D9F6002B6862 /* DashboardEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardEndpoint.swift; sourceTree = ""; }; 027DB33C28D8DB5E002B6862 /* DashboardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardRepository.swift; sourceTree = ""; }; 027DB33E28D8E605002B6862 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 027DB34228D8E89B002B6862 /* DashboardInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardInteractor.swift; sourceTree = ""; }; - 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewModel.swift; sourceTree = ""; }; + 027DB34428D8E9D2002B6862 /* ListDashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDashboardViewModel.swift; sourceTree = ""; }; + 028895662BE3B34E00102D8C /* NoCoursesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCoursesView.swift; sourceTree = ""; }; + 02935B6E2BCEC91100B22F66 /* PrimaryCourseDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryCourseDashboardView.swift; sourceTree = ""; }; + 02935B702BCEC91F00B22F66 /* PrimaryCourseDashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryCourseDashboardViewModel.swift; sourceTree = ""; }; + 02935B762BCFB2C100B22F66 /* CourseCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseCardView.swift; sourceTree = ""; }; 02A48B17295ACE200033D5E0 /* DashboardCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DashboardCoreModel.xcdatamodel; sourceTree = ""; }; 02A48B19295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPersistenceProtocol.swift; sourceTree = ""; }; + 02A98F102BD96814009B46BD /* DropDownMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropDownMenu.swift; sourceTree = ""; }; + 02A98F122BD96B9D009B46BD /* AllCoursesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllCoursesView.swift; sourceTree = ""; }; + 02A98F142BD96BC2009B46BD /* AllCoursesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllCoursesViewModel.swift; sourceTree = ""; }; + 02A98F162BD97885009B46BD /* CategoryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryFilterView.swift; sourceTree = ""; }; 02A9A9082978194100B55797 /* DashboardTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DashboardTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02A9A90A2978194100B55797 /* DashboardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewModelTests.swift; sourceTree = ""; }; 02A9A92829781A4D00B55797 /* DashboardMock.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardMock.generated.swift; sourceTree = ""; }; - 02ED50CD29A64B9B008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02EF39E728D89F560058F6BD /* Dashboard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Dashboard.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 02F175322A4DABBF0019CD70 /* DashboardAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardAnalytics.swift; sourceTree = ""; }; 02F3BFE029252FCB0051930C /* DashboardRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardRouter.swift; sourceTree = ""; }; @@ -72,6 +94,8 @@ 97E7DF0A2B7A3EAF00A2A09B /* CourseEnrollmentsMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseEnrollmentsMock.swift; sourceTree = ""; }; BBABB135366FFB1DAEFA0D16 /* Pods-App-Dashboard-DashboardTests.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Dashboard-DashboardTests.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Dashboard-DashboardTests/Pods-App-Dashboard-DashboardTests.debugprod.xcconfig"; sourceTree = ""; }; CCF4C665AD91B6B96F6A11DF /* Pods-App-Dashboard-DashboardTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Dashboard-DashboardTests.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Dashboard-DashboardTests/Pods-App-Dashboard-DashboardTests.debugdev.xcconfig"; sourceTree = ""; }; + CE1735052CD2552A00F9606A /* PrimaryCourseDashboardViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryCourseDashboardViewModelTests.swift; sourceTree = ""; }; + CE1735092CD26CB500F9606A /* AllCoursesViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllCoursesViewModelTests.swift; sourceTree = ""; }; DE6CF4F983BBF52606807F9A /* Pods-App-Dashboard-DashboardTests.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Dashboard-DashboardTests.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Dashboard-DashboardTests/Pods-App-Dashboard-DashboardTests.debugstage.xcconfig"; sourceTree = ""; }; E36D702D7E3F9A8B3303AD0A /* Pods-App-Dashboard-DashboardTests.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Dashboard-DashboardTests.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Dashboard-DashboardTests/Pods-App-Dashboard-DashboardTests.releasedev.xcconfig"; sourceTree = ""; }; E5B672C28C8F9279BB4E5C9B /* Pods-App-Dashboard.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Dashboard.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Dashboard/Pods-App-Dashboard.releasestage.xcconfig"; sourceTree = ""; }; @@ -93,6 +117,7 @@ buildActionMask = 2147483647; files = ( 027DB33F28D8E605002B6862 /* Core.framework in Frameworks */, + CEB1E26D2CC14E9B00921517 /* OEXFoundation in Frameworks */, 214DA1AADABC7BF4FB8EA1D7 /* Pods_App_Dashboard.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -109,6 +134,19 @@ path = Persistence; sourceTree = ""; }; + 0277241C2BCE9DF300C2908D /* Elements */ = { + isa = PBXGroup; + children = ( + 0277241D2BCE9E1500C2908D /* PrimaryCardView.swift */, + 02A98F102BD96814009B46BD /* DropDownMenu.swift */, + 02A98F162BD97885009B46BD /* CategoryFilterView.swift */, + 02935B762BCFB2C100B22F66 /* CourseCardView.swift */, + 0277241F2BCEA16C00C2908D /* ProgressLineView.swift */, + 028895662BE3B34E00102D8C /* NoCoursesView.swift */, + ); + path = Elements; + sourceTree = ""; + }; 027DB33628D8D851002B6862 /* Domain */ = { isa = PBXGroup; children = ( @@ -181,8 +219,13 @@ 02F6EF3F28D9ECA200835477 /* Presentation */ = { isa = PBXGroup; children = ( - 027DB33228D8BDBA002B6862 /* DashboardView.swift */, - 027DB34428D8E9D2002B6862 /* DashboardViewModel.swift */, + 0277241C2BCE9DF300C2908D /* Elements */, + 02935B6E2BCEC91100B22F66 /* PrimaryCourseDashboardView.swift */, + 02935B702BCEC91F00B22F66 /* PrimaryCourseDashboardViewModel.swift */, + 02A98F122BD96B9D009B46BD /* AllCoursesView.swift */, + 02A98F142BD96BC2009B46BD /* AllCoursesViewModel.swift */, + 027DB33228D8BDBA002B6862 /* ListDashboardView.swift */, + 027DB34428D8E9D2002B6862 /* ListDashboardViewModel.swift */, 02F3BFE029252FCB0051930C /* DashboardRouter.swift */, 02F175322A4DABBF0019CD70 /* DashboardAnalytics.swift */, ); @@ -200,6 +243,8 @@ 0766DFD2299AD99B00EBEF6A /* Presentation */ = { isa = PBXGroup; children = ( + CE1735092CD26CB500F9606A /* AllCoursesViewModelTests.swift */, + CE1735052CD2552A00F9606A /* PrimaryCourseDashboardViewModelTests.swift */, 02A9A90A2978194100B55797 /* DashboardViewModelTests.swift */, ); path = Presentation; @@ -329,6 +374,9 @@ uk, ); mainGroup = 02EF39DD28D89F560058F6BD; + packageReferences = ( + CEB1E26B2CC14E9B00921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */, + ); productRefGroup = 02EF39E828D89F560058F6BD /* Products */; projectDirPath = ""; projectRoot = ""; @@ -446,7 +494,9 @@ buildActionMask = 2147483647; files = ( 02A9A90B2978194100B55797 /* DashboardViewModelTests.swift in Sources */, + CE17350A2CD26CB500F9606A /* AllCoursesViewModelTests.swift in Sources */, 02A9A92929781A4D00B55797 /* DashboardMock.generated.swift in Sources */, + CE1735062CD2552A00F9606A /* PrimaryCourseDashboardViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -455,14 +505,24 @@ buildActionMask = 2147483647; files = ( 02A48B1A295ACE3D0033D5E0 /* DashboardPersistenceProtocol.swift in Sources */, + 028895672BE3B34F00102D8C /* NoCoursesView.swift in Sources */, + 02935B6F2BCEC91100B22F66 /* PrimaryCourseDashboardView.swift in Sources */, + 02A98F172BD97886009B46BD /* CategoryFilterView.swift in Sources */, 027DB33D28D8DB5E002B6862 /* DashboardRepository.swift in Sources */, + 02935B712BCEC91F00B22F66 /* PrimaryCourseDashboardViewModel.swift in Sources */, + 02A98F112BD96814009B46BD /* DropDownMenu.swift in Sources */, 02A48B18295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld in Sources */, + 02935B772BCFB2C100B22F66 /* CourseCardView.swift in Sources */, 02F175332A4DABBF0019CD70 /* DashboardAnalytics.swift in Sources */, - 027DB34528D8E9D2002B6862 /* DashboardViewModel.swift in Sources */, - 027DB33328D8BDBA002B6862 /* DashboardView.swift in Sources */, + 027DB34528D8E9D2002B6862 /* ListDashboardViewModel.swift in Sources */, + 0277241E2BCE9E1500C2908D /* PrimaryCardView.swift in Sources */, + 027DB33328D8BDBA002B6862 /* ListDashboardView.swift in Sources */, + 02A98F152BD96BC2009B46BD /* AllCoursesViewModel.swift in Sources */, + 02A98F132BD96B9D009B46BD /* AllCoursesView.swift in Sources */, 02F3BFE129252FCB0051930C /* DashboardRouter.swift in Sources */, 027DB33928D8D9F6002B6862 /* DashboardEndpoint.swift in Sources */, 02F6EF4828D9ED8300835477 /* Strings.swift in Sources */, + 027724202BCEA16C00C2908D /* ProgressLineView.swift in Sources */, 97E7DF0B2B7A3EAF00A2A09B /* CourseEnrollmentsMock.swift in Sources */, 027DB34328D8E89B002B6862 /* DashboardInteractor.swift in Sources */, ); @@ -484,7 +544,6 @@ isa = PBXVariantGroup; children = ( 02F6EF4428D9ECC500835477 /* en */, - 02ED50CD29A64B9B008341CD /* uk */, ); name = Localizable.strings; sourceTree = ""; @@ -500,7 +559,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -521,7 +580,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -542,7 +601,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -563,7 +622,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -584,7 +643,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -605,7 +664,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -690,14 +749,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -725,7 +784,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -804,14 +863,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -838,7 +897,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DashboardTests; @@ -981,14 +1040,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1016,14 +1075,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1114,14 +1173,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1207,14 +1266,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1305,14 +1364,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1398,14 +1457,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1474,6 +1533,25 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + CEB1E26B2CC14E9B00921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; + requirement = { + kind = exactVersion; + version = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + CEB1E26C2CC14E9B00921517 /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E26B2CC14E9B00921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; +/* End XCSwiftPackageProductDependency section */ + /* Begin XCVersionGroup section */ 02A48B16295ACE200033D5E0 /* DashboardCoreModel.xcdatamodeld */ = { isa = XCVersionGroup; diff --git a/Dashboard/Dashboard/Data/DashboardRepository.swift b/Dashboard/Dashboard/Data/DashboardRepository.swift index 65816872f..9487caab4 100644 --- a/Dashboard/Dashboard/Data/DashboardRepository.swift +++ b/Dashboard/Dashboard/Data/DashboardRepository.swift @@ -7,10 +7,14 @@ import Foundation import Core +import OEXFoundation public protocol DashboardRepositoryProtocol { - func getMyCourses(page: Int) async throws -> [CourseItem] - func getMyCoursesOffline() throws -> [CourseItem] + func getEnrollments(page: Int) async throws -> [CourseItem] + func getEnrollmentsOffline() async throws -> [CourseItem] + func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment + func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment + func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment } public class DashboardRepository: DashboardRepositoryProtocol { @@ -27,39 +31,58 @@ public class DashboardRepository: DashboardRepositoryProtocol { self.persistence = persistence } - public func getMyCourses(page: Int) async throws -> [CourseItem] { + public func getEnrollments(page: Int) async throws -> [CourseItem] { let result = try await api.requestData( - DashboardEndpoint.getMyCourses(username: storage.user?.username ?? "", page: page) + DashboardEndpoint.getEnrollments(username: storage.user?.username ?? "", page: page) ) .mapResponse(DataLayer.CourseEnrollments.self) .domain(baseURL: config.baseURL.absoluteString) - persistence.saveMyCourses(items: result) + persistence.saveEnrollments(items: result) return result } - public func getMyCoursesOffline() throws -> [CourseItem] { - return try persistence.loadMyCourses() + public func getEnrollmentsOffline() async throws -> [CourseItem] { + return try await persistence.loadEnrollments() } + public func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment { + let result = try await api.requestData( + DashboardEndpoint.getPrimaryEnrollment( + username: storage.user?.username ?? "", + pageSize: pageSize + ) + ) + .mapResponse(DataLayer.PrimaryEnrollment.self) + .domain(baseURL: config.baseURL.absoluteString) + persistence.savePrimaryEnrollment(enrollments: result) + return result + } + + public func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment { + return try await persistence.loadPrimaryEnrollment() + } + + public func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment { + let result = try await api.requestData( + DashboardEndpoint.getAllCourses( + username: storage.user?.username ?? "", + filteredBy: filteredBy, + page: page + ) + ) + .mapResponse(DataLayer.PrimaryEnrollment.self) + .domain(baseURL: config.baseURL.absoluteString) + return result + } } +// swiftlint:disable all // Mark - For testing and SwiftUI preview #if DEBUG class DashboardRepositoryMock: DashboardRepositoryProtocol { - func getCourseEnrollments(baseURL: String) async throws -> [CourseItem] { - do { - let courseEnrollments = try - DashboardRepository.CourseEnrollmentsJSON.data(using: .utf8)! - .mapResponse(DataLayer.CourseEnrollments.self) - .domain(baseURL: baseURL) - return courseEnrollments - } catch { - throw error - } - } - func getMyCourses(page: Int) async throws -> [CourseItem] { + func getEnrollments(page: Int) async throws -> [CourseItem] { var models: [CourseItem] = [] for i in 0...10 { models.append( @@ -68,20 +91,107 @@ class DashboardRepositoryMock: DashboardRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: true, + hasAccess: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, courseID: "course_id_\(i)", numPages: 1, - coursesCount: 0 + coursesCount: 0, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0 ) ) } return models } - func getMyCoursesOffline() throws -> [CourseItem] { return [] } + func getEnrollmentsOffline() throws -> [CourseItem] { return [] } + + public func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment { + + var courses: [CourseItem] = [] + for i in 0...10 { + courses.append( + CourseItem( + name: "Course name \(i)", + org: "Organization", + shortDescription: "shortDescription", + imageURL: "", + hasAccess: true, + courseStart: nil, + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil, + courseID: "course_id_\(i)", + numPages: 1, + coursesCount: 0, + courseRawImage: nil, + progressEarned: 4, + progressPossible: 10 + ) + ) + } + + let futureAssignment = Assignment( + type: "Final Exam", + title: "Subsection 3", + description: "", + date: Date(), + complete: false, + firstComponentBlockId: nil + ) + + let primaryCourse = PrimaryCourse( + name: "Primary Course", + org: "Organization", + courseID: "123", + hasAccess: true, + courseStart: Date(), + courseEnd: Date(), + courseBanner: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", + futureAssignments: [futureAssignment], + pastAssignments: [futureAssignment], + progressEarned: 2, + progressPossible: 5, + lastVisitedBlockID: nil, + resumeTitle: nil + ) + return PrimaryEnrollment(primaryCourse: primaryCourse, courses: courses, totalPages: 1, count: 1) + } + + func getPrimaryEnrollmentOffline() async throws -> Core.PrimaryEnrollment { + Core.PrimaryEnrollment(primaryCourse: nil, courses: [], totalPages: 1, count: 1) + } + + func getAllCourses(filteredBy: String, page: Int) async throws -> Core.PrimaryEnrollment { + var courses: [CourseItem] = [] + for i in 0...10 { + courses.append( + CourseItem( + name: "Course name \(i)", + org: "Organization", + shortDescription: "shortDescription", + imageURL: "", + hasAccess: true, + courseStart: nil, + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil, + courseID: "course_id_\(i)", + numPages: 1, + coursesCount: 0, + courseRawImage: nil, + progressEarned: 4, + progressPossible: 10 + ) + ) + } + + return PrimaryEnrollment(primaryCourse: nil, courses: courses, totalPages: 1, count: 1) + } } #endif +// swiftlint:enable all diff --git a/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift b/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift index 1d6845214..cfc946d95 100644 --- a/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift +++ b/Dashboard/Dashboard/Data/Network/DashboardEndpoint.swift @@ -8,20 +8,28 @@ import Foundation import Core import Alamofire +import OEXFoundation +import UIKit enum DashboardEndpoint: EndPointType { - case getMyCourses(username: String, page: Int) + case getEnrollments(username: String, page: Int) + case getPrimaryEnrollment(username: String, pageSize: Int) + case getAllCourses(username: String, filteredBy: String, page: Int) var path: String { switch self { - case let .getMyCourses(username, _): + case let .getEnrollments(username, _): return "/api/mobile/v3/users/\(username)/course_enrollments" + case let .getPrimaryEnrollment(username, _): + return "/api/mobile/v4/users/\(username)/course_enrollments" + case let .getAllCourses(username, _, _): + return "/api/mobile/v4/users/\(username)/course_enrollments" } } var httpMethod: HTTPMethod { switch self { - case .getMyCourses: + case .getEnrollments, .getPrimaryEnrollment, .getAllCourses: return .get } } @@ -32,11 +40,27 @@ enum DashboardEndpoint: EndPointType { var task: HTTPTask { switch self { - case let .getMyCourses(_, page): + case let .getEnrollments(_, page): let params: Parameters = [ "page": page ] return .requestParameters(parameters: params, encoding: URLEncoding.queryString) + + case let .getPrimaryEnrollment(_, pageSize): + let params: Parameters = [ + "page_size": pageSize + ] + return .requestParameters(parameters: params, encoding: URLEncoding.queryString) + + case let .getAllCourses(_, filteredBy, page): + var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + let params: Parameters = [ + "page_size": idiom == .pad ? 24 : 12, + "status": filteredBy, + "requested_fields": "course_progress", + "page": page + ] + return .requestParameters(parameters: params, encoding: URLEncoding.queryString) } } } diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents b/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents index eeee515fe..48a5a25c6 100644 --- a/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents +++ b/Dashboard/Dashboard/Data/Persistence/DashboardCoreModel.xcdatamodeld/DashboardCoreModel.xcdatamodel/contents @@ -1,18 +1,37 @@ - - + + + + + + + + + + + + + + + + + + + + + @@ -20,4 +39,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift index 14bad2aaa..2257c8238 100644 --- a/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift +++ b/Dashboard/Dashboard/Data/Persistence/DashboardPersistenceProtocol.swift @@ -9,8 +9,10 @@ import CoreData import Core public protocol DashboardPersistenceProtocol { - func loadMyCourses() throws -> [CourseItem] - func saveMyCourses(items: [CourseItem]) + func loadEnrollments() async throws -> [CourseItem] + func saveEnrollments(items: [CourseItem]) + func loadPrimaryEnrollment() async throws -> PrimaryEnrollment + func savePrimaryEnrollment(enrollments: PrimaryEnrollment) } public final class DashboardBundle { diff --git a/Dashboard/Dashboard/Domain/DashboardInteractor.swift b/Dashboard/Dashboard/Domain/DashboardInteractor.swift index 8e84d847b..60a920eaf 100644 --- a/Dashboard/Dashboard/Domain/DashboardInteractor.swift +++ b/Dashboard/Dashboard/Domain/DashboardInteractor.swift @@ -10,8 +10,11 @@ import Core //sourcery: AutoMockable public protocol DashboardInteractorProtocol { - func getMyCourses(page: Int) async throws -> [CourseItem] - func discoveryOffline() throws -> [CourseItem] + func getEnrollments(page: Int) async throws -> [CourseItem] + func getEnrollmentsOffline() async throws -> [CourseItem] + func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment + func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment + func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment } public class DashboardInteractor: DashboardInteractorProtocol { @@ -23,12 +26,24 @@ public class DashboardInteractor: DashboardInteractorProtocol { } @discardableResult - public func getMyCourses(page: Int) async throws -> [CourseItem] { - return try await repository.getMyCourses(page: page) + public func getEnrollments(page: Int) async throws -> [CourseItem] { + return try await repository.getEnrollments(page: page) } - public func discoveryOffline() throws -> [CourseItem] { - return try repository.getMyCoursesOffline() + public func getEnrollmentsOffline() async throws -> [CourseItem] { + return try await repository.getEnrollmentsOffline() + } + + public func getPrimaryEnrollment(pageSize: Int) async throws -> PrimaryEnrollment { + return try await repository.getPrimaryEnrollment(pageSize: pageSize) + } + + public func getPrimaryEnrollmentOffline() async throws -> PrimaryEnrollment { + return try await repository.getPrimaryEnrollmentOffline() + } + + public func getAllCourses(filteredBy: String, page: Int) async throws -> PrimaryEnrollment { + return try await repository.getAllCourses(filteredBy: filteredBy, page: page) } } diff --git a/Dashboard/Dashboard/Presentation/AllCoursesView.swift b/Dashboard/Dashboard/Presentation/AllCoursesView.swift new file mode 100644 index 000000000..f35299372 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/AllCoursesView.swift @@ -0,0 +1,221 @@ +// +// AllCoursesView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 24.04.2024. +// + +import SwiftUI +import Core +import OEXFoundation +import Theme + +public struct AllCoursesView: View { + + @ObservedObject + private var viewModel: AllCoursesViewModel + private let router: DashboardRouter + @Environment(\.isHorizontal) private var isHorizontal + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + public init(viewModel: AllCoursesViewModel, router: DashboardRouter) { + self.viewModel = viewModel + self.router = router + } + + public var body: some View { + GeometryReader { proxy in + ZStack(alignment: .top) { + VStack { + BackNavigationButton( + color: Theme.Colors.textPrimary, + action: { + router.back() + } + ) + .backViewStyle() + .padding(.top, isHorizontal ? 32 : 16) + .padding(.leading, 7) + + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + .zIndex(1) + + if let myEnrollments = viewModel.myEnrollments, + myEnrollments.courses.isEmpty, + !viewModel.fetchInProgress, + !viewModel.refresh { + NoCoursesView(selectedMenu: viewModel.selectedMenu) + } + // MARK: - Page body + VStack(alignment: .center) { + learnTitleAndSearch() + .frameLimit(width: proxy.size.width) + ScrollView { + VStack(spacing: 0) { + CategoryFilterView(selectedOption: $viewModel.selectedMenu) + .disabled(viewModel.fetchInProgress) + .frameLimit(width: proxy.size.width) + if let myEnrollments = viewModel.myEnrollments { + let useRelativeDates = viewModel.storage.useRelativeDates + LazyVGrid(columns: columns(), spacing: 15) { + ForEach( + Array(myEnrollments.courses.enumerated()), + id: \.offset + ) { index, course in + Button(action: { + viewModel.trackDashboardCourseClicked( + courseID: course.courseID, + courseName: course.name + ) + router.showCourseScreens( + courseID: course.courseID, + hasAccess: course.hasAccess, + courseStart: course.courseStart, + courseEnd: course.courseEnd, + enrollmentStart: course.enrollmentStart, + enrollmentEnd: course.enrollmentEnd, + title: course.name, + courseRawImage: course.imageURL, + showDates: false, + lastVisitedBlockID: nil + ) + }, label: { + CourseCardView( + courseName: course.name, + courseImage: course.imageURL, + progressEarned: course.progressEarned, + progressPossible: course.progressPossible, + courseStartDate: course.courseStart, + courseEndDate: course.courseEnd, + hasAccess: course.hasAccess, + showProgress: true, + useRelativeDates: useRelativeDates + ).padding(8) + }) + .accessibilityIdentifier("course_item") + .onAppear { + Task { + await viewModel.getMyCoursesPagination(index: index) + } + } + } + .padding(10) + .frameLimit(width: proxy.size.width) + } + } + // MARK: - ProgressBar + if viewModel.nextPage <= viewModel.totalPages, !viewModel.refresh { + VStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 20) + }.frame(maxWidth: .infinity, + maxHeight: .infinity) + } + VStack {}.frame(height: 40) + } + } + .refreshable { + Task { + await viewModel.getCourses(page: 1, refresh: true) + } + } + .accessibilityAction {} + } + .padding(.top, 8) + + // MARK: - Offline mode SnackBar + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.getCourses(page: 1, refresh: true) + } + ) + + // MARK: - Error Alert + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, viewModel.connectivity.isInternetAvaliable + ? 0 : OfflineSnackBarView.height) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } + } + .onFirstAppear { + Task { + await viewModel.getCourses(page: 1) + } + } + .onChange(of: viewModel.selectedMenu) { _ in + Task { + viewModel.myEnrollments?.courses = [] + await viewModel.getCourses(page: 1, refresh: false) + } + } + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + .navigationBarBackButtonHidden(true) + .navigationBarHidden(true) + .navigationTitle(DashboardLocalization.Learn.allCourses) + } + } + + private func columns() -> [GridItem] { + isHorizontal || idiom == .pad + ? [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ] + : [ + GridItem(.flexible()), + GridItem(.flexible()) + ] + } + + private func learnTitleAndSearch() -> some View { + HStack(alignment: .center) { + Text(DashboardLocalization.Learn.allCourses) + .font(Theme.Fonts.displaySmall) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("all_courses_header_text") + Spacer() + } + .padding(.horizontal, 20) + .padding(.top, 20) + .padding(.bottom, 10) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DashboardLocalization.Learn.allCourses) + } +} + +#if DEBUG +struct AllCoursesView_Previews: PreviewProvider { + static var previews: some View { + let vm = AllCoursesViewModel( + interactor: DashboardInteractor.mock, + connectivity: Connectivity(), + analytics: DashboardAnalyticsMock(), + storage: CoreStorageMock() + ) + + AllCoursesView(viewModel: vm, router: DashboardRouterMock()) + .preferredColorScheme(.light) + .previewDisplayName("AllCoursesView Light") + + AllCoursesView(viewModel: vm, router: DashboardRouterMock()) + .preferredColorScheme(.dark) + .previewDisplayName("AllCoursesView Dark") + } +} +#endif diff --git a/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift new file mode 100644 index 000000000..439f329f7 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/AllCoursesViewModel.swift @@ -0,0 +1,108 @@ +// +// AllCoursesViewModel.swift +// Dashboard +// +// Created by  Stepanok Ivan on 24.04.2024. +// + +import Foundation +import Core +import SwiftUI +import Combine + +public class AllCoursesViewModel: ObservableObject { + + var nextPage = 1 + var totalPages = 1 + @Published private(set) var fetchInProgress = false + @Published private(set) var refresh = false + @Published var selectedMenu: CategoryOption = .all + + @Published var myEnrollments: PrimaryEnrollment? + @Published var showError: Bool = false + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + + let connectivity: ConnectivityProtocol + let storage: CoreStorage + private let interactor: DashboardInteractorProtocol + private let analytics: DashboardAnalytics + private var onCourseEnrolledCancellable: AnyCancellable? + + public init( + interactor: DashboardInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DashboardAnalytics, + storage: CoreStorage + ) { + self.interactor = interactor + self.connectivity = connectivity + self.analytics = analytics + self.storage = storage + + onCourseEnrolledCancellable = NotificationCenter.default + .publisher(for: .onCourseEnrolled) + .sink { [weak self] _ in + guard let self = self else { return } + Task { + await self.getCourses(page: 1, refresh: true) + } + } + } + + @MainActor + public func getCourses(page: Int, refresh: Bool = false) async { + self.refresh = refresh + do { + if refresh || page == 1 { + fetchInProgress = true + myEnrollments?.courses = [] + nextPage = 1 + myEnrollments = try await interactor.getAllCourses(filteredBy: selectedMenu.status, page: page) + self.totalPages = myEnrollments?.totalPages ?? 1 + } else { + fetchInProgress = true + myEnrollments?.courses += try await interactor.getAllCourses( + filteredBy: selectedMenu.status, page: page + ).courses + } + self.nextPage += 1 + totalPages = myEnrollments?.totalPages ?? 1 + fetchInProgress = false + self.refresh = false + } catch let error { + fetchInProgress = false + self.refresh = false + if error is NoCachedDataError { + errorMessage = CoreLocalization.Error.noCachedData + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + @MainActor + public func getMyCoursesPagination(index: Int) async { + guard let courses = myEnrollments?.courses else { return } + if !fetchInProgress { + if totalPages > 1 { + if index == courses.count - 3 { + if totalPages != 1 { + if nextPage <= totalPages { + await getCourses(page: self.nextPage) + } + } + } + } + } + } + + func trackDashboardCourseClicked(courseID: String, courseName: String) { + analytics.dashboardCourseClicked(courseID: courseID, courseName: courseName) + } +} diff --git a/Dashboard/Dashboard/Presentation/DashboardRouter.swift b/Dashboard/Dashboard/Presentation/DashboardRouter.swift index 40bc86c41..0d38f3199 100644 --- a/Dashboard/Dashboard/Presentation/DashboardRouter.swift +++ b/Dashboard/Dashboard/Presentation/DashboardRouter.swift @@ -11,12 +11,21 @@ import Core public protocol DashboardRouter: BaseRouter { func showCourseScreens(courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String) + title: String, + courseRawImage: String?, + showDates: Bool, + lastVisitedBlockID: String?) + + func showAllCourses(courses: [CourseItem]) + + func showDiscoverySearch(searchQuery: String?) + + func showSettings() } @@ -27,12 +36,20 @@ public class DashboardRouterMock: BaseRouterMock, DashboardRouter { public override init() {} public func showCourseScreens(courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String) {} + title: String, + courseRawImage: String?, + showDates: Bool, + lastVisitedBlockID: String?) {} + + public func showAllCourses(courses: [CourseItem]) {} + + public func showDiscoverySearch(searchQuery: String?) {} + public func showSettings() {} } #endif diff --git a/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift b/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift new file mode 100644 index 000000000..a46a41d46 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/CategoryFilterView.swift @@ -0,0 +1,91 @@ +// +// CategoryFilterView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 24.04.2024. +// + +import Theme +import Core +import SwiftUI + +enum CategoryOption: String, CaseIterable { + case all + case inProgress + case completed + case expired + + var status: String { + switch self { + case .all: + "all" + case .inProgress: + "in_progress" + case .completed: + "completed" + case .expired: + "expired" + } + } + + var text: String { + switch self { + case .all: + DashboardLocalization.Learn.Category.all + case .inProgress: + DashboardLocalization.Learn.Category.inProgress + case .completed: + DashboardLocalization.Learn.Category.completed + case .expired: + DashboardLocalization.Learn.Category.expired + } + } +} + +struct CategoryFilterView: View { + @Binding var selectedOption: CategoryOption + @Environment(\.colorScheme) var colorScheme + + var body: some View { + ScrollView(.horizontal) { + HStack(spacing: 8) { + ForEach(Array(CategoryOption.allCases.enumerated()), id: \.offset) { index, option in + Button(action: { + selectedOption = option + }, + label: { + HStack { + Text(option.text) + .font(Theme.Fonts.titleSmall) + .foregroundColor( + option == selectedOption ? Theme.Colors.slidingSelectedTextColor : ( + colorScheme == .light ? Theme.Colors.accentColor : .white + ) + ) + } + .padding(.horizontal, 17) + .padding(.vertical, 8) + .background { + ZStack { + RoundedRectangle(cornerRadius: 20) + .foregroundStyle( + option == selectedOption + ? Theme.Colors.accentColor + : Theme.Colors.cardViewBackground + ) + RoundedRectangle(cornerRadius: 20) + .stroke( + colorScheme == .light ? Theme.Colors.accentColor : .clear, + style: .init(lineWidth: 1) + ) + } + .padding(.vertical, 1) + } + }) + .padding(.leading, index == 0 ? 16 : 0) + } + } + .fixedSize() + } + } +} diff --git a/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift new file mode 100644 index 000000000..e493c00d5 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/CourseCardView.swift @@ -0,0 +1,129 @@ +// +// CourseCardView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 17.04.2024. +// + +import SwiftUI +import Theme +import Kingfisher +import Core + +struct CourseCardView: View { + + private let courseName: String + private let courseImage: String + private let progressEarned: Int + private let progressPossible: Int + private let courseStartDate: Date? + private let courseEndDate: Date? + private let hasAccess: Bool + private let showProgress: Bool + private let useRelativeDates: Bool + + init( + courseName: String, + courseImage: String, + progressEarned: Int, + progressPossible: Int, + courseStartDate: Date?, + courseEndDate: Date?, + hasAccess: Bool, + showProgress: Bool, + useRelativeDates: Bool + ) { + self.courseName = courseName + self.courseImage = courseImage + self.progressEarned = progressEarned + self.progressPossible = progressPossible + self.courseStartDate = courseStartDate + self.courseEndDate = courseEndDate + self.hasAccess = hasAccess + self.showProgress = showProgress + self.useRelativeDates = useRelativeDates + } + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack(alignment: .leading, spacing: 0) { + courseBanner + if showProgress { + ProgressLineView( + progressEarned: progressEarned, + progressPossible: progressPossible, + height: 4 + ) + } + courseTitle + } + if !hasAccess { + ZStack(alignment: .center) { + Circle() + .foregroundStyle(Theme.Colors.primaryHeaderColor) + .opacity(0.7) + .frame(width: 24, height: 24) + CoreAssets.lockIcon.swiftUIImage + .foregroundStyle(Theme.Colors.textPrimary) + } + .padding(8) + } + } + .background(Theme.Colors.courseCardBackground) + .cornerRadius(8) + .shadow(color: Theme.Colors.courseCardShadow, radius: 6, x: 2, y: 2) + } + + private var courseBanner: some View { + return KFImage(URL(string: courseImage)) + .onFailureImage(CoreAssets.noCourseImage.image) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(minWidth: 120, minHeight: 90, maxHeight: 100) + .clipped() + .accessibilityElement(children: .ignore) + .accessibilityIdentifier("course_image") + } + + private var courseTitle: some View { + VStack(alignment: .leading, spacing: 3) { + if let courseEndDate { + Text(courseEndDate.dateToString(style: .courseEndsMonthDDYear, useRelativeDates: useRelativeDates)) + .font(Theme.Fonts.labelSmall) + .foregroundStyle(Theme.Colors.textSecondaryLight) + .multilineTextAlignment(.leading) + } else if let courseStartDate { + Text(courseStartDate.dateToString(style: .courseStartsMonthDDYear, useRelativeDates: useRelativeDates)) + .font(Theme.Fonts.labelSmall) + .foregroundStyle(Theme.Colors.textSecondaryLight) + .multilineTextAlignment(.leading) + } + Text(courseName) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textPrimary) + .lineLimit(2) + .multilineTextAlignment(.leading) + } + .frame(height: showProgress ? 51 : 40, alignment: .topLeading) + .fixedSize(horizontal: false, vertical: true) + .padding(.top, 10) + .padding(.horizontal, 12) + .padding(.bottom, 16) + } +} + +#if DEBUG +#Preview { + CourseCardView( + courseName: "Six Sigma Part 2: Analyze, Improve, Control", + courseImage: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", + progressEarned: 4, + progressPossible: 8, + courseStartDate: nil, + courseEndDate: Date(), + hasAccess: true, + showProgress: true, + useRelativeDates: true + ).frame(width: 170) +} +#endif diff --git a/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift new file mode 100644 index 000000000..beaf3bec5 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/DropDownMenu.swift @@ -0,0 +1,92 @@ +// +// DropDownMenu.swift +// Dashboard +// +// Created by  Stepanok Ivan on 24.04.2024. +// + +import Theme +import Core +import SwiftUI + +enum MenuOption: String, CaseIterable { + case courses + case programs + + var text: String { + switch self { + case .courses: + DashboardLocalization.Learn.DropdownMenu.courses + case .programs: + DashboardLocalization.Learn.DropdownMenu.programs + } + } +} + +struct DropDownMenu: View { + @Binding var selectedOption: MenuOption + @State private var expanded: Bool = false + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + HStack { + Text(selectedOption.text) + .font(Theme.Fonts.titleSmall) + .accessibilityIdentifier("dropdown_menu_text") + Image(systemName: "chevron.down") + .rotation3DEffect( + .degrees(expanded ? 180 : 0), + axis: (x: 1.0, y: 0.0, z: 0.0) + ) + } + .foregroundColor(Theme.Colors.textPrimary) + .onTapGesture { + withAnimation(.snappy(duration: 0.2)) { + expanded.toggle() + } + } + + if expanded { + VStack(spacing: 0) { + ForEach(Array(MenuOption.allCases.enumerated()), id: \.offset) { index, option in + Button( + action: { + selectedOption = option + expanded = false + }, label: { + HStack { + Text(option.text) + .font(Theme.Fonts.titleSmall) + .foregroundColor( + option == selectedOption ? Theme.Colors.primaryButtonTextColor : + Theme.Colors.textPrimary + ) + Spacer() + } + .padding(10) + .background { + ZStack { + RoundedCorners(bl: index == MenuOption.allCases.count-1 ? 8 : 0, + br: index == MenuOption.allCases.count-1 ? 8 : 0) + .foregroundStyle(option == selectedOption + ? Theme.Colors.accentColor + : Theme.Colors.cardViewBackground) + RoundedCorners(bl: index == MenuOption.allCases.count-1 ? 8 : 0, + br: index == MenuOption.allCases.count-1 ? 8 : 0) + .stroke(Theme.Colors.cardViewStroke, style: .init(lineWidth: 1)) + } + } + } + ) + } + } + .frame(minWidth: 182) + .fixedSize() + } + } + .onTapBackground(enabled: expanded, { expanded = false }) + .onDisappear { + expanded = false + } + } +} diff --git a/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift b/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift new file mode 100644 index 000000000..82a471509 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/NoCoursesView.swift @@ -0,0 +1,89 @@ +// +// NoCoursesView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 02.05.2024. +// + +import SwiftUI +import Core +import Theme + +struct NoCoursesView: View { + + enum NoCoursesType { + case primary + case inProgress + case completed + case expired + + var title: String { + switch self { + case .primary: + DashboardLocalization.Learn.NoCoursesView.noCourses + case .inProgress: + DashboardLocalization.Learn.NoCoursesView.noCoursesInProgress + case .completed: + DashboardLocalization.Learn.NoCoursesView.noCompletedCourses + case .expired: + DashboardLocalization.Learn.NoCoursesView.noExpiredCourses + } + } + + var description: String? { + switch self { + case .primary: + DashboardLocalization.Learn.NoCoursesView.noCoursesDescription + case .inProgress, .completed, .expired: + nil + } + } + } + + private let type: NoCoursesType + private var openDiscovery: (() -> Void) + + init(openDiscovery: @escaping (() -> Void)) { + self.type = .primary + self.openDiscovery = openDiscovery + } + + init(selectedMenu: CategoryOption) { + switch selectedMenu { + case .all: + type = .inProgress + case .inProgress: + type = .inProgress + case .completed: + type = .completed + case .expired: + type = .expired + } + openDiscovery = {} + } + + var body: some View { + VStack(spacing: 8) { + Spacer() + CoreAssets.learnEmpty.swiftUIImage + .resizable() + .frame(width: 96, height: 96) + .foregroundStyle(Theme.Colors.textSecondaryLight) + Text(type.title) + .foregroundStyle(Theme.Colors.textPrimary) + .font(Theme.Fonts.titleMedium) + if let description = type.description { + Text(description) + .multilineTextAlignment(.center) + .foregroundStyle(Theme.Colors.textPrimary) + .font(Theme.Fonts.labelMedium) + .frame(width: 245) + } + Spacer() + if type == .primary { + StyledButton(DashboardLocalization.Learn.NoCoursesView.findACourse, action: { openDiscovery() }) + .padding(24) + } + } + } +} diff --git a/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift new file mode 100644 index 000000000..8e96c9d77 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/PrimaryCardView.swift @@ -0,0 +1,334 @@ +// +// PrimaryCardView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import SwiftUI +import Kingfisher +import Theme +import Core + +public struct PrimaryCardView: View { + + private let courseName: String + private let org: String + private let courseImage: String + private let courseStartDate: Date? + private let courseEndDate: Date? + private var futureAssignments: [Assignment] + private let pastAssignments: [Assignment] + private let progressEarned: Int + private let progressPossible: Int + private let canResume: Bool + private let resumeTitle: String? + private let useRelativeDates: Bool + private var assignmentAction: (String?) -> Void + private var openCourseAction: () -> Void + private var resumeAction: () -> Void + @Environment(\.isHorizontal) var isHorizontal + + public init( + courseName: String, + org: String, + courseImage: String, + courseStartDate: Date?, + courseEndDate: Date?, + futureAssignments: [Assignment], + pastAssignments: [Assignment], + progressEarned: Int, + progressPossible: Int, + canResume: Bool, + resumeTitle: String?, + useRelativeDates: Bool, + assignmentAction: @escaping (String?) -> Void, + openCourseAction: @escaping () -> Void, + resumeAction: @escaping () -> Void + ) { + self.courseName = courseName + self.org = org + self.courseImage = courseImage + self.courseStartDate = courseStartDate + self.courseEndDate = courseEndDate + self.futureAssignments = futureAssignments + self.pastAssignments = pastAssignments + self.progressEarned = progressEarned + self.progressPossible = progressPossible + self.canResume = canResume + self.resumeTitle = resumeTitle + self.useRelativeDates = useRelativeDates + self.assignmentAction = assignmentAction + self.openCourseAction = openCourseAction + self.resumeAction = resumeAction + } + + public var body: some View { + ZStack { + if isHorizontal { + horizontalLayout + } else { + verticalLayout + } + } + .background(Theme.Colors.courseCardBackground) + .cornerRadius(8) + .shadow(color: Theme.Colors.courseCardShadow, radius: 4, x: 0, y: 3) + .padding(20) + } + + @ViewBuilder + var verticalLayout: some View { + VStack(alignment: .leading, spacing: 0) { + Group { + courseBanner + .frame(height: 140) + .clipped() + ProgressLineView(progressEarned: progressEarned, progressPossible: progressPossible) + courseTitle + } + .onTapGesture { + openCourseAction() + } + assignments + } + } + + @ViewBuilder + var horizontalLayout: some View { + HStack(alignment: .top, spacing: 0) { + VStack(alignment: .leading, spacing: 0) { + GeometryReader { proxy in + courseBanner + .frame(width: proxy.size.width) + .clipped() + } + ProgressLineView(progressEarned: progressEarned, progressPossible: progressPossible) + } + .onTapGesture { + openCourseAction() + } + VStack(alignment: .leading, spacing: 0) { + ZStack(alignment: .leading) { + courseTitle + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .background( + Theme.Colors.background // need for tap area + ) + + .onTapGesture { + openCourseAction() + } + assignments + } + } + .frame(minHeight: 240) + } + + private var assignments: some View { + VStack(alignment: .leading, spacing: 8) { + // pastAssignments + if pastAssignments.count == 1, let pastAssignment = pastAssignments.first { + courseButton( + title: pastAssignment.title, + description: DashboardLocalization.Learn.PrimaryCard.onePastAssignment, + icon: CoreAssets.warning.swiftUIImage, + selected: false, + action: { assignmentAction(pastAssignments.first?.firstComponentBlockId) } + ) + } else if pastAssignments.count > 1 { + courseButton( + title: DashboardLocalization.Learn.PrimaryCard.viewAssignments, + description: DashboardLocalization.Learn.PrimaryCard.pastAssignments(pastAssignments.count), + icon: CoreAssets.warning.swiftUIImage, + selected: false, + action: { assignmentAction(nil) } + ) + } + + // futureAssignment + if !futureAssignments.isEmpty { + if futureAssignments.count == 1, let futureAssignment = futureAssignments.first { + let daysRemaining = Calendar.current.dateComponents( + [.day], + from: Date(), + to: futureAssignment.date + ).day ?? 0 + courseButton( + title: futureAssignment.title, + description: futureAssignment.date.dateToString( + style: .shortWeekdayMonthDayYear, + useRelativeDates: useRelativeDates, + dueIn: true + ), + icon: CoreAssets.chapter.swiftUIImage, + selected: false, + action: { + assignmentAction(futureAssignments.first?.firstComponentBlockId) + } + ) + } else if futureAssignments.count > 1 { + if let firtsData = futureAssignments.sorted(by: { $0.date < $1.date }).first { + courseButton( + title: DashboardLocalization.Learn.PrimaryCard.futureAssignments( + futureAssignments.count, + firtsData.date.dateToString(style: .lastPost, useRelativeDates: useRelativeDates) + ), + description: nil, + icon: CoreAssets.chapter.swiftUIImage, + selected: false, + action: { + assignmentAction(nil) + } + ) + } + } + } + + // ResumeButton + if canResume { + courseButton( + title: resumeTitle ?? "", + description: DashboardLocalization.Learn.PrimaryCard.resume, + icon: CoreAssets.resumeCourse.swiftUIImage, + selected: true, + action: { resumeAction() } + ) + } else { + courseButton( + title: DashboardLocalization.Learn.PrimaryCard.startCourse, + description: nil, + icon: CoreAssets.resumeCourse.swiftUIImage, + selected: true, + action: { resumeAction() } + ) + } + } + } + + private func courseButton( + title: String, + description: String?, + icon: Image, + selected: Bool, + action: @escaping () -> Void + ) -> some View { + Button(action: { + action() + }, label: { + ZStack(alignment: .top) { + Rectangle().frame(height: selected ? 0 : 1) + .foregroundStyle(Theme.Colors.cardViewStroke) + HStack(alignment: .center) { + VStack(alignment: .leading) { + HStack(spacing: 0) { + icon + .renderingMode(.template) + .resizable() + .frame(width: 24, height: 24) + .foregroundStyle(foregroundColor(selected)) + .padding(12) + + VStack(alignment: .leading, spacing: 6) { + if let description { + Text(description) + .font(Theme.Fonts.labelSmall) + .multilineTextAlignment(.leading) + .lineLimit(1) + .foregroundStyle(foregroundColor(selected)) + } + Text(title) + .font(Theme.Fonts.titleSmall) + .multilineTextAlignment(.leading) + .lineLimit(1) + .foregroundStyle(foregroundColor(selected)) + } + .padding(.top, 2) + } + } + Spacer() + CoreAssets.chevronRight.swiftUIImage + .foregroundStyle(foregroundColor(selected)) + .padding(8) + } + .padding(.top, 8) + .padding(.bottom, selected ? 10 : 0) + }.background(selected ? Theme.Colors.accentButtonColor : .clear) + }) + } + + private func foregroundColor(_ selected: Bool) -> SwiftUI.Color { + return selected ? Theme.Colors.white : Theme.Colors.textPrimary + } + + private var courseBanner: some View { + return KFImage(URL(string: courseImage)) + .onFailureImage(CoreAssets.noCourseImage.image) + .resizable() + .aspectRatio(contentMode: .fill) + .accessibilityElement(children: .ignore) + .accessibilityIdentifier("course_image") + } + + private var courseTitle: some View { + VStack(alignment: .leading, spacing: 3) { + Text(org) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textSecondaryLight) + Text(courseName) + .font(Theme.Fonts.titleLarge) + .foregroundStyle(Theme.Colors.textPrimary) + .lineLimit(3) + if let courseEndDate { + Text(courseEndDate.dateToString(style: .courseEndsMonthDDYear, useRelativeDates: useRelativeDates)) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textSecondaryLight) + } else if let courseStartDate { + Text(courseStartDate.dateToString(style: .courseStartsMonthDDYear, useRelativeDates: useRelativeDates)) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textSecondaryLight) + } + } + .padding(.top, 10) + .padding(.horizontal, 12) + .padding(.bottom, 16) + } +} + +#if DEBUG +struct PrimaryCardView_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Theme.Colors.background + PrimaryCardView( + courseName: "Course Title", + org: "Organization", + courseImage: "https://thumbs.dreamstime.com/b/logo-edx-samsung-tablet-edx-massive-open-online-course-mooc-provider-hosts-online-university-level-courses-wide-117763805.jpg", + courseStartDate: nil, + courseEndDate: Date(), + futureAssignments: [ + Assignment( + type: "Lesson", + title: "HomeWork", + description: "Some description", + date: Date().addingTimeInterval(64000 * 3), + complete: false, + firstComponentBlockId: "123" + ) + ], + pastAssignments: [], + progressEarned: 10, + progressPossible: 45, + canResume: true, + resumeTitle: "Course Chapter 1", + useRelativeDates: false, + assignmentAction: { _ in }, + openCourseAction: {}, + resumeAction: {} + ) + .loadFonts() + } + } +} +#endif diff --git a/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift new file mode 100644 index 000000000..80ef325a1 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/Elements/ProgressLineView.swift @@ -0,0 +1,48 @@ +// +// ProgressLineView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import SwiftUI +import Theme + +struct ProgressLineView: View { + private let progressEarned: Int + private let progressPossible: Int + private let height: CGFloat + + var progressValue: CGFloat { + guard progressPossible != 0 else { return 0 } + return CGFloat(progressEarned) / CGFloat(progressPossible) + } + + init(progressEarned: Int, progressPossible: Int, height: CGFloat = 8) { + self.progressEarned = progressEarned + self.progressPossible = progressPossible + self.height = height + } + + var body: some View { + ZStack(alignment: .leading) { + GeometryReader { geometry in + Rectangle() + .foregroundStyle(Theme.Colors.cardViewStroke) + Rectangle() + .foregroundStyle(Theme.Colors.accentButtonColor) + .frame(width: geometry.size.width * progressValue) + }.frame(height: height) + } + } +} + +#if DEBUG +struct ProgressLineView_Previews: PreviewProvider { + static var previews: some View { + ProgressLineView(progressEarned: 4, progressPossible: 6) + .frame(height: 8) + .padding() + } +} +#endif diff --git a/Dashboard/Dashboard/Presentation/DashboardView.swift b/Dashboard/Dashboard/Presentation/ListDashboardView.swift similarity index 77% rename from Dashboard/Dashboard/Presentation/DashboardView.swift rename to Dashboard/Dashboard/Presentation/ListDashboardView.swift index 44c6c3fc8..d5d925bd0 100644 --- a/Dashboard/Dashboard/Presentation/DashboardView.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardView.swift @@ -1,5 +1,5 @@ // -// DashboardView.swift +// ListDashboardView.swift // Dashboard // // Created by  Stepanok Ivan on 19.09.2022. @@ -7,9 +7,10 @@ import SwiftUI import Core +import OEXFoundation import Theme -public struct DashboardView: View { +public struct ListDashboardView: View { private let dashboardCourses: some View = VStack(alignment: .leading) { Text(DashboardLocalization.Header.courses) .font(Theme.Fonts.displaySmall) @@ -25,10 +26,11 @@ public struct DashboardView: View { .accessibilityLabel(DashboardLocalization.Header.courses + DashboardLocalization.Header.welcomeBack) @StateObject - private var viewModel: DashboardViewModel + private var viewModel: ListDashboardViewModel private let router: DashboardRouter + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } - public init(viewModel: DashboardViewModel, router: DashboardRouter) { + public init(viewModel: ListDashboardViewModel, router: DashboardRouter) { self._viewModel = StateObject(wrappedValue: { viewModel }()) self.router = router } @@ -39,9 +41,7 @@ public struct DashboardView: View { // MARK: - Page body VStack(alignment: .center) { - RefreshableScrollViewCompat(action: { - await viewModel.getMyCourses(page: 1, refresh: true) - }) { + ScrollView { Group { LazyVStack(spacing: 0) { HStack { @@ -53,14 +53,15 @@ public struct DashboardView: View { if viewModel.courses.isEmpty && !viewModel.fetchInProgress { EmptyPageIcon() } else { + let useRelativeDates = viewModel.storage.useRelativeDates ForEach(Array(viewModel.courses.enumerated()), id: \.offset) { index, course in - CourseCellView( model: course, type: .dashboard, index: index, - cellsCount: viewModel.courses.count + cellsCount: viewModel.courses.count, + useRelativeDates: useRelativeDates ) .padding(.horizontal, 20) .listRowBackground(Color.clear) @@ -76,12 +77,15 @@ public struct DashboardView: View { ) router.showCourseScreens( courseID: course.courseID, - isActive: course.isActive, + hasAccess: course.hasAccess, courseStart: course.courseStart, courseEnd: course.courseEnd, enrollmentStart: course.enrollmentStart, enrollmentEnd: course.enrollmentEnd, - title: course.name + title: course.name, + courseRawImage: course.courseRawImage, + showDates: false, + lastVisitedBlockID: nil ) } .accessibilityIdentifier("course_item") @@ -99,8 +103,25 @@ public struct DashboardView: View { } } .frameLimit(width: proxy.size.width) - }.accessibilityAction {} + } + .refreshable { + Task { + await viewModel.getMyCourses(page: 1, refresh: true) + } + } + .accessibilityAction {} }.padding(.top, 8) + HStack { + Spacer() + Button(action: { + router.showSettings() + }, label: { + CoreAssets.settings.swiftUIImage.renderingMode(.template) + .foregroundColor(Theme.Colors.accentColor) + }) + } + .padding(.top, idiom == .pad ? 13 : 5) + .padding(.trailing, idiom == .pad ? 20 : 16) // MARK: - Offline mode SnackBar OfflineSnackBarView(connectivity: viewModel.connectivity, @@ -138,22 +159,23 @@ public struct DashboardView: View { } #if DEBUG -struct DashboardView_Previews: PreviewProvider { +struct ListDashboardView_Previews: PreviewProvider { static var previews: some View { - let vm = DashboardViewModel( + let vm = ListDashboardViewModel( interactor: DashboardInteractor.mock, connectivity: Connectivity(), - analytics: DashboardAnalyticsMock() + analytics: DashboardAnalyticsMock(), + storage: CoreStorageMock() ) let router = DashboardRouterMock() - DashboardView(viewModel: vm, router: router) + ListDashboardView(viewModel: vm, router: router) .preferredColorScheme(.light) - .previewDisplayName("DashboardView Light") + .previewDisplayName("ListDashboardView Light") - DashboardView(viewModel: vm, router: router) + ListDashboardView(viewModel: vm, router: router) .preferredColorScheme(.dark) - .previewDisplayName("DashboardView Dark") + .previewDisplayName("ListDashboardView Dark") } } #endif diff --git a/Dashboard/Dashboard/Presentation/DashboardViewModel.swift b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift similarity index 76% rename from Dashboard/Dashboard/Presentation/DashboardViewModel.swift rename to Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift index 6e4d9974a..112865e86 100644 --- a/Dashboard/Dashboard/Presentation/DashboardViewModel.swift +++ b/Dashboard/Dashboard/Presentation/ListDashboardViewModel.swift @@ -1,5 +1,5 @@ // -// DashboardViewModel.swift +// ListDashboardViewModel.swift // Dashboard // // Created by  Stepanok Ivan on 19.09.2022. @@ -10,7 +10,7 @@ import Core import SwiftUI import Combine -public class DashboardViewModel: ObservableObject { +public class ListDashboardViewModel: ObservableObject { public var nextPage = 1 public var totalPages = 1 @@ -29,14 +29,18 @@ public class DashboardViewModel: ObservableObject { let connectivity: ConnectivityProtocol private let interactor: DashboardInteractorProtocol private let analytics: DashboardAnalytics + let storage: CoreStorage private var onCourseEnrolledCancellable: AnyCancellable? + private var refreshEnrollmentsCancellable: AnyCancellable? public init(interactor: DashboardInteractorProtocol, connectivity: ConnectivityProtocol, - analytics: DashboardAnalytics) { + analytics: DashboardAnalytics, + storage: CoreStorage) { self.interactor = interactor self.connectivity = connectivity self.analytics = analytics + self.storage = storage onCourseEnrolledCancellable = NotificationCenter.default .publisher(for: .onCourseEnrolled) @@ -46,6 +50,14 @@ public class DashboardViewModel: ObservableObject { await self.getMyCourses(page: 1, refresh: true) } } + refreshEnrollmentsCancellable = NotificationCenter.default + .publisher(for: .refreshEnrollments) + .sink { [weak self] _ in + guard let self = self else { return } + Task { + await self.getMyCourses(page: 1, refresh: true) + } + } } @MainActor @@ -54,11 +66,11 @@ public class DashboardViewModel: ObservableObject { fetchInProgress = true if connectivity.isInternetAvaliable { if refresh { - courses = try await interactor.getMyCourses(page: page) + courses = try await interactor.getEnrollments(page: page) self.totalPages = 1 self.nextPage = 2 } else { - courses += try await interactor.getMyCourses(page: page) + courses += try await interactor.getEnrollments(page: page) self.nextPage += 1 } if !courses.isEmpty { @@ -66,7 +78,7 @@ public class DashboardViewModel: ObservableObject { } fetchInProgress = false } else { - courses = try interactor.discoveryOffline() + courses = try await interactor.getEnrollmentsOffline() self.nextPage += 1 fetchInProgress = false } diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift new file mode 100644 index 000000000..e7af096b1 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardView.swift @@ -0,0 +1,370 @@ +// +// PrimaryCourseDashboardView.swift +// Dashboard +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import SwiftUI +import Core +import OEXFoundation +import Theme +import Swinject + +public struct PrimaryCourseDashboardView: View { + + @StateObject private var viewModel: PrimaryCourseDashboardViewModel + private let router: DashboardRouter + @ViewBuilder let programView: ProgramView + private var openDiscoveryPage: () -> Void + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @State private var selectedMenu: MenuOption = .courses + + public init( + viewModel: PrimaryCourseDashboardViewModel, + router: DashboardRouter, + programView: ProgramView, + openDiscoveryPage: @escaping () -> Void + ) { + self._viewModel = StateObject(wrappedValue: { viewModel }()) + self.router = router + self.programView = programView + self.openDiscoveryPage = openDiscoveryPage + } + + public var body: some View { + GeometryReader { proxy in + ZStack(alignment: .top) { + if viewModel.enrollments?.primaryCourse == nil + && !viewModel.fetchInProgress + && selectedMenu == .courses { + NoCoursesView(openDiscovery: { + openDiscoveryPage() + }).zIndex(1) + } + learnTitleAndSearch(proxy: proxy) + .zIndex(1) + // MARK: - Page body + VStack(alignment: .leading) { + Spacer(minLength: 50) + switch selectedMenu { + case .courses: + ScrollView { + ZStack(alignment: .topLeading) { + if viewModel.fetchInProgress { + VStack(alignment: .center) { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + }.frame(maxWidth: .infinity, + maxHeight: .infinity) + } else { + LazyVStack(spacing: 0) { + if let enrollments = viewModel.enrollments { + if let primary = enrollments.primaryCourse { + PrimaryCardView( + courseName: primary.name, + org: primary.org, + courseImage: primary.courseBanner, + courseStartDate: primary.courseStart, + courseEndDate: primary.courseEnd, + futureAssignments: primary.futureAssignments, + pastAssignments: primary.pastAssignments, + progressEarned: primary.progressEarned, + progressPossible: primary.progressPossible, + canResume: primary.lastVisitedBlockID != nil, + resumeTitle: primary.resumeTitle, + useRelativeDates: viewModel.storage.useRelativeDates, + assignmentAction: { lastVisitedBlockID in + router.showCourseScreens( + courseID: primary.courseID, + hasAccess: primary.hasAccess, + courseStart: primary.courseStart, + courseEnd: primary.courseEnd, + enrollmentStart: nil, + enrollmentEnd: nil, + title: primary.name, + courseRawImage: primary.courseBanner, + showDates: lastVisitedBlockID == nil, + lastVisitedBlockID: lastVisitedBlockID + ) + }, + openCourseAction: { + router.showCourseScreens( + courseID: primary.courseID, + hasAccess: primary.hasAccess, + courseStart: primary.courseStart, + courseEnd: primary.courseEnd, + enrollmentStart: nil, + enrollmentEnd: nil, + title: primary.name, + courseRawImage: primary.courseBanner, + showDates: false, + lastVisitedBlockID: nil + ) + }, + resumeAction: { + router.showCourseScreens( + courseID: primary.courseID, + hasAccess: primary.hasAccess, + courseStart: primary.courseStart, + courseEnd: primary.courseEnd, + enrollmentStart: nil, + enrollmentEnd: nil, + title: primary.name, + courseRawImage: primary.courseBanner, + showDates: false, + lastVisitedBlockID: primary.lastVisitedBlockID + ) + } + ) + } + if !enrollments.courses.isEmpty { + viewAll(enrollments) + } + if idiom == .pad { + LazyVGrid( + columns: [ + GridItem(.flexible(), spacing: 16), + GridItem(.flexible(), spacing: 16), + GridItem(.flexible(), spacing: 16), + GridItem(.flexible(), spacing: 16) + ], + alignment: .leading, + spacing: 15 + ) { + courses(enrollments) + } + .padding(20) + } else { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + courses(enrollments) + } + .padding(20) + } + } + Spacer(minLength: 100) + } + } + } + } + .frameLimit(width: proxy.size.width) + } + .refreshable { + Task { + await viewModel.getEnrollments(showProgress: false) + } + } + .accessibilityAction {} + case .programs: + programView + } + }.padding(.top, 8) + // MARK: - Offline mode SnackBar + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.getEnrollments(showProgress: false) + } + ).zIndex(2) + + // MARK: - Error Alert + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) + } + .padding( + .bottom, + viewModel.connectivity.isInternetAvaliable ? 0 : OfflineSnackBarView.height + ) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + .zIndex(2) + } + } + .onFirstAppear { + Task { + await viewModel.getEnrollments() + } + } + .onAppear { + viewModel.updateNeeded = true + } + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + .navigationBarBackButtonHidden(true) + .navigationBarHidden(true) + .navigationTitle(DashboardLocalization.title) + } + } + + @ViewBuilder + private func courses(_ enrollments: PrimaryEnrollment) -> some View { + let useRelativeDates = viewModel.storage.useRelativeDates + ForEach( + Array(enrollments.courses.enumerated()), + id: \.offset + ) { _, course in + Button(action: { + viewModel.trackDashboardCourseClicked( + courseID: course.courseID, + courseName: course.name + ) + router.showCourseScreens( + courseID: course.courseID, + hasAccess: course.hasAccess, + courseStart: course.courseStart, + courseEnd: course.courseEnd, + enrollmentStart: course.enrollmentStart, + enrollmentEnd: course.enrollmentEnd, + title: course.name, + courseRawImage: course.imageURL, + showDates: false, + lastVisitedBlockID: nil + ) + }, label: { + CourseCardView( + courseName: course.name, + courseImage: course.imageURL, + progressEarned: 0, + progressPossible: 0, + courseStartDate: nil, + courseEndDate: nil, + hasAccess: course.hasAccess, + showProgress: false, + useRelativeDates: useRelativeDates + ).frame(width: idiom == .pad ? nil : 120) + } + ) + .accessibilityIdentifier("course_item") + } + if enrollments.courses.count < enrollments.count { + viewAllButton(enrollments) + } + } + + private func viewAllButton(_ enrollments: PrimaryEnrollment) -> some View { + Button(action: { + router.showAllCourses(courses: enrollments.courses) + }, label: { + ZStack(alignment: .topTrailing) { + HStack { + Spacer() + VStack(alignment: .leading, spacing: 0) { + Spacer() + CoreAssets.viewAll.swiftUIImage + Text(DashboardLocalization.Learn.viewAll) + .font(Theme.Fonts.labelMedium) + .foregroundStyle(Theme.Colors.textPrimary) + Spacer() + } + Spacer() + } + .frame(width: idiom == .pad ? nil : 120) + } + .background(Theme.Colors.cardViewBackground) + .cornerRadius(8) + .shadow(color: Theme.Colors.courseCardShadow, radius: 6, x: 2, y: 2) + }) + } + + private func viewAll(_ enrollments: PrimaryEnrollment) -> some View { + Button(action: { + router.showAllCourses(courses: enrollments.courses) + }, label: { + HStack { + Text(DashboardLocalization.Learn.viewAllCourses(enrollments.count + 1)) + .font(Theme.Fonts.titleSmall) + .accessibilityIdentifier("courses_welcomeback_text") + Image(systemName: "chevron.right") + } + .padding(.horizontal, 16) + .foregroundColor(Theme.Colors.textPrimary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + }) + } + + private func learnTitleAndSearch(proxy: GeometryProxy) -> some View { + let showDropdown = viewModel.config.program.enabled && viewModel.config.program.isWebViewConfigured + return ZStack(alignment: .top) { + Theme.Colors.background + .frame(height: showDropdown ? 70 : 50) + ZStack(alignment: .topTrailing) { + VStack { + HStack(alignment: .center) { + Text(DashboardLocalization.Learn.title) + .font(Theme.Fonts.displaySmall) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("courses_header_text") + Spacer() + } + if showDropdown { + HStack(alignment: .center) { + DropDownMenu(selectedOption: $selectedMenu) + Spacer() + } + } + } + .frameLimit(width: proxy.size.width) + HStack { + Spacer() + Button(action: { + router.showSettings() + }, label: { + CoreAssets.settings.swiftUIImage.renderingMode(.template) + .foregroundColor(Theme.Colors.accentColor) + }) + } + .padding(.top, 8) + .offset(x: idiom == .pad ? 1 : 5, y: idiom == .pad ? 4 : -5) + } + + .listRowBackground(Color.clear) + .padding(.horizontal, 20) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DashboardLocalization.Header.courses + DashboardLocalization.Header.welcomeBack) + } + } +} + +#if DEBUG +struct PrimaryCourseDashboardView_Previews: PreviewProvider { + static var previews: some View { + let vm = PrimaryCourseDashboardViewModel( + interactor: DashboardInteractor.mock, + connectivity: Connectivity(), + analytics: DashboardAnalyticsMock(), + config: ConfigMock(), + storage: CoreStorageMock() + ) + + PrimaryCourseDashboardView( + viewModel: vm, + router: DashboardRouterMock(), + programView: EmptyView(), + openDiscoveryPage: { + } + ) + .preferredColorScheme(.light) + .previewDisplayName("DashboardView Light") + + PrimaryCourseDashboardView( + viewModel: vm, + router: DashboardRouterMock(), + programView: EmptyView(), + openDiscoveryPage: { + } + ) + .preferredColorScheme(.dark) + .previewDisplayName("DashboardView Dark") + } +} +#endif diff --git a/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift new file mode 100644 index 000000000..f1a74f773 --- /dev/null +++ b/Dashboard/Dashboard/Presentation/PrimaryCourseDashboardViewModel.swift @@ -0,0 +1,115 @@ +// +// PrimaryCourseDashboardViewModel.swift +// Dashboard +// +// Created by  Stepanok Ivan on 16.04.2024. +// + +import Foundation +import Core +import SwiftUI +import Combine + +public class PrimaryCourseDashboardViewModel: ObservableObject { + + var nextPage = 1 + var totalPages = 1 + @Published public private(set) var fetchInProgress = true + @Published var enrollments: PrimaryEnrollment? + @Published var showError: Bool = false + @Published var updateNeeded: Bool = false + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + + let connectivity: ConnectivityProtocol + private let interactor: DashboardInteractorProtocol + private let analytics: DashboardAnalytics + let config: ConfigProtocol + let storage: CoreStorage + private var cancellables = Set() + + private let ipadPageSize = 7 + private let iphonePageSize = 5 + + public init( + interactor: DashboardInteractorProtocol, + connectivity: ConnectivityProtocol, + analytics: DashboardAnalytics, + config: ConfigProtocol, + storage: CoreStorage + ) { + self.interactor = interactor + self.connectivity = connectivity + self.analytics = analytics + self.config = config + self.storage = storage + + let enrollmentPublisher = NotificationCenter.default.publisher(for: .onCourseEnrolled) + let completionPublisher = NotificationCenter.default.publisher(for: .onblockCompletionRequested) + let refreshEnrollmentsPublisher = NotificationCenter.default.publisher(for: .refreshEnrollments) + + enrollmentPublisher + .sink { [weak self] _ in + guard let self = self else { return } + Task { + await self.getEnrollments() + } + } + .store(in: &cancellables) + + completionPublisher + .sink { [weak self] _ in + guard let self = self else { return } + updateEnrollmentsIfNeeded() + } + .store(in: &cancellables) + + refreshEnrollmentsPublisher + .sink { [weak self] _ in + guard let self = self else { return } + Task { + await self.getEnrollments() + } + } + .store(in: &cancellables) + } + + private func updateEnrollmentsIfNeeded() { + guard updateNeeded else { return } + Task { + await getEnrollments() + updateNeeded = false + } + } + + @MainActor + public func getEnrollments(showProgress: Bool = true) async { + let pageSize = UIDevice.current.userInterfaceIdiom == .pad ? ipadPageSize : iphonePageSize + fetchInProgress = showProgress + do { + if connectivity.isInternetAvaliable { + enrollments = try await interactor.getPrimaryEnrollment(pageSize: pageSize) + fetchInProgress = false + } else { + enrollments = try await interactor.getPrimaryEnrollmentOffline() + fetchInProgress = false + } + } catch let error { + fetchInProgress = false + if error is NoCachedDataError { + errorMessage = CoreLocalization.Error.noCachedData + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + func trackDashboardCourseClicked(courseID: String, courseName: String) { + analytics.dashboardCourseClicked(courseID: courseID, courseName: courseName) + } +} diff --git a/Dashboard/Dashboard/SwiftGen/Strings.swift b/Dashboard/Dashboard/SwiftGen/Strings.swift index aac74931c..7b5924613 100644 --- a/Dashboard/Dashboard/SwiftGen/Strings.swift +++ b/Dashboard/Dashboard/SwiftGen/Strings.swift @@ -10,6 +10,8 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum DashboardLocalization { + /// Search + public static let search = DashboardLocalization.tr("Localizable", "SEARCH", fallback: "Search") /// Localizable.strings /// Dashboard /// @@ -25,6 +27,70 @@ public enum DashboardLocalization { /// Welcome back. Let's keep learning. public static let welcomeBack = DashboardLocalization.tr("Localizable", "HEADER.WELCOME_BACK", fallback: "Welcome back. Let's keep learning.") } + public enum Learn { + /// All Courses + public static let allCourses = DashboardLocalization.tr("Localizable", "LEARN.ALL_COURSES", fallback: "All Courses") + /// Learn + public static let title = DashboardLocalization.tr("Localizable", "LEARN.TITLE", fallback: "Learn") + /// View All + public static let viewAll = DashboardLocalization.tr("Localizable", "LEARN.VIEW_ALL", fallback: "View All") + /// View All Courses (%@) + public static func viewAllCourses(_ p1: Any) -> String { + return DashboardLocalization.tr("Localizable", "LEARN.VIEW_ALL_COURSES", String(describing: p1), fallback: "View All Courses (%@)") + } + public enum Category { + /// All + public static let all = DashboardLocalization.tr("Localizable", "LEARN.CATEGORY.ALL", fallback: "All") + /// Completed + public static let completed = DashboardLocalization.tr("Localizable", "LEARN.CATEGORY.COMPLETED", fallback: "Completed") + /// Expired + public static let expired = DashboardLocalization.tr("Localizable", "LEARN.CATEGORY.EXPIRED", fallback: "Expired") + /// In Progress + public static let inProgress = DashboardLocalization.tr("Localizable", "LEARN.CATEGORY.IN_PROGRESS", fallback: "In Progress") + } + public enum DropdownMenu { + /// Courses + public static let courses = DashboardLocalization.tr("Localizable", "LEARN.DROPDOWN_MENU.COURSES", fallback: "Courses") + /// Programs + public static let programs = DashboardLocalization.tr("Localizable", "LEARN.DROPDOWN_MENU.PROGRAMS", fallback: "Programs") + } + public enum NoCoursesView { + /// Find a Course + public static let findACourse = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.FIND_A_COURSE", fallback: "Find a Course") + /// No Completed Courses + public static let noCompletedCourses = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COMPLETED_COURSES", fallback: "No Completed Courses") + /// No Courses + public static let noCourses = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COURSES", fallback: "No Courses") + /// You are not currently enrolled in any courses, would you like to explore the course catalog? + public static let noCoursesDescription = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION", fallback: "You are not currently enrolled in any courses, would you like to explore the course catalog?") + /// No Courses in Progress + public static let noCoursesInProgress = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_COURSES_IN_PROGRESS", fallback: "No Courses in Progress") + /// No Expired Courses + public static let noExpiredCourses = DashboardLocalization.tr("Localizable", "LEARN.NO_COURSES_VIEW.NO_EXPIRED_COURSES", fallback: "No Expired Courses") + } + public enum PrimaryCard { + /// %@ Due in %@ Days + public static func dueDays(_ p1: Any, _ p2: Any) -> String { + return DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.DUE_DAYS", String(describing: p1), String(describing: p2), fallback: "%@ Due in %@ Days") + } + /// %@ Assignments Due %@ + public static func futureAssignments(_ p1: Any, _ p2: Any) -> String { + return DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.FUTURE_ASSIGNMENTS", String(describing: p1), String(describing: p2), fallback: "%@ Assignments Due %@ ") + } + /// 1 Past Due Assignment + public static let onePastAssignment = DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.ONE_PAST_ASSIGNMENT", fallback: "1 Past Due Assignment") + /// %@ Past Due Assignments + public static func pastAssignments(_ p1: Any) -> String { + return DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.PAST_ASSIGNMENTS", String(describing: p1), fallback: "%@ Past Due Assignments") + } + /// Resume Course + public static let resume = DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.RESUME", fallback: "Resume Course") + /// Start Course + public static let startCourse = DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.START_COURSE", fallback: "Start Course") + /// View Assignments + public static let viewAssignments = DashboardLocalization.tr("Localizable", "LEARN.PRIMARY_CARD.VIEW_ASSIGNMENTS", fallback: "View Assignments") + } + } } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces diff --git a/Dashboard/Dashboard/en.lproj/Localizable.strings b/Dashboard/Dashboard/en.lproj/Localizable.strings index 88fc5d371..406b6c34e 100644 --- a/Dashboard/Dashboard/en.lproj/Localizable.strings +++ b/Dashboard/Dashboard/en.lproj/Localizable.strings @@ -10,4 +10,36 @@ "HEADER.COURSES" = "Courses"; "HEADER.WELCOME_BACK" = "Welcome back. Let's keep learning."; +"SEARCH" = "Search"; + "EMPTY.SUBTITLE" = "You are not enrolled in any courses yet."; + +"LEARN.TITLE" = "Learn"; +"LEARN.VIEW_ALL" = "View All"; +"LEARN.VIEW_ALL_COURSES" = "View All Courses (%@)"; +"LEARN.ALL_COURSES" = "All Courses"; + +"LEARN.PRIMARY_CARD.ONE_PAST_ASSIGNMENT" = "1 Past Due Assignment"; +"LEARN.PRIMARY_CARD.VIEW_ASSIGNMENTS" = "View Assignments"; +"LEARN.PRIMARY_CARD.PAST_ASSIGNMENTS" = "%@ Past Due Assignments"; +"LEARN.PRIMARY_CARD.FUTURE_ASSIGNMENTS" = "%@ Assignments Due %@ "; +"LEARN.PRIMARY_CARD.DUE_DAYS" = "%@ Due in %@ Days"; +"LEARN.PRIMARY_CARD.RESUME" = "Resume Course"; +"LEARN.PRIMARY_CARD.START_COURSE" = "Start Course"; + +"LEARN.DROPDOWN_MENU.COURSES" = "Courses"; +"LEARN.DROPDOWN_MENU.PROGRAMS" = "Programs"; + +"LEARN.CATEGORY.ALL" = "All"; +"LEARN.CATEGORY.IN_PROGRESS" = "In Progress"; +"LEARN.CATEGORY.COMPLETED" = "Completed"; +"LEARN.CATEGORY.EXPIRED" = "Expired"; + +"LEARN.NO_COURSES_VIEW.NO_COURSES" = "No Courses"; +"LEARN.NO_COURSES_VIEW.NO_COURSES_IN_PROGRESS" = "No Courses in Progress"; +"LEARN.NO_COURSES_VIEW.NO_COMPLETED_COURSES" = "No Completed Courses"; +"LEARN.NO_COURSES_VIEW.NO_EXPIRED_COURSES" = "No Expired Courses"; + +"LEARN.NO_COURSES_VIEW.NO_COURSES_DESCRIPTION" = "You are not currently enrolled in any courses, would you like to explore the course catalog?"; + +"LEARN.NO_COURSES_VIEW.FIND_A_COURSE" = "Find a Course"; diff --git a/Dashboard/Dashboard/uk.lproj/Localizable.strings b/Dashboard/Dashboard/uk.lproj/Localizable.strings deleted file mode 100644 index 748f2c021..000000000 --- a/Dashboard/Dashboard/uk.lproj/Localizable.strings +++ /dev/null @@ -1,14 +0,0 @@ -/* - Localizable.strings - Dashboard - - Created by  Stepanok Ivan on 20.09.2022. - -*/ - -"TITLE" = "Мої курси"; -"HEADER.COURSES" = "Курси"; -"HEADER.WELCOME_BACK" = "З поверненням. Давайте продовжимо вчитись."; - -"EMPTY.TITLE" = "Нічого немає"; -"EMPTY.SUBTITLE" = "Ви не підписані на жодний курс."; diff --git a/Dashboard/DashboardTests/DashboardMock.generated.swift b/Dashboard/DashboardTests/DashboardMock.generated.swift index fb6a1334e..975fac8b3 100644 --- a/Dashboard/DashboardTests/DashboardMock.generated.swift +++ b/Dashboard/DashboardTests/DashboardMock.generated.swift @@ -13,6 +13,7 @@ import Dashboard import Foundation import SwiftUI import Combine +import OEXFoundation // MARK: - AuthInteractorProtocol @@ -93,6 +94,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + open func login(ssoToken: String) throws -> User { + addInvocation(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) + let perform = methodPerformValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) as? (String) -> Void + perform?(`ssoToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(ssoToken: String). Use given") + Failure("Stub return value not specified for login(ssoToken: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -174,6 +191,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) + case m_login__ssoToken_ssoToken(Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields @@ -194,6 +212,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) return Matcher.ComparisonResult(results) + case (.m_login__ssoToken_ssoToken(let lhsSsotoken), .m_login__ssoToken_ssoToken(let rhsSsotoken)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSsotoken, rhs: rhsSsotoken, with: matcher), lhsSsotoken, rhsSsotoken, "ssoToken")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -224,6 +247,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue + case let .m_login__ssoToken_ssoToken(p0): return p0.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 @@ -235,6 +259,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" + case .m_login__ssoToken_ssoToken: return ".login(ssoToken:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" @@ -261,6 +286,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func login(ssoToken: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -297,6 +325,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + public static func login(ssoToken: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func login(ssoToken: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -356,6 +394,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} @discardableResult public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} + public static func login(ssoToken: Parameter) -> Verify { return Verify(method: .m_login__ssoToken_ssoToken(`ssoToken`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} @@ -375,6 +414,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) } + public static func login(ssoToken: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_login__ssoToken_ssoToken(`ssoToken`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -581,6 +623,12 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -619,6 +667,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showSSOWebBrowser__title_title(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -679,6 +728,11 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) return Matcher.ComparisonResult(results) + case (.m_showSSOWebBrowser__title_title(let lhsTitle), .m_showSSOWebBrowser__title_title(let rhsTitle)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -732,6 +786,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showSSOWebBrowser__title_title(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -752,6 +807,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showSSOWebBrowser__title_title: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -786,6 +842,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} + public static func showSSOWebBrowser(title: Parameter) -> Verify { return Verify(method: .m_showSSOWebBrowser__title_title(`title`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`))} @@ -832,6 +889,9 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showSSOWebBrowser__title_title(`title`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -919,9 +979,9 @@ open class BaseRouterMock: BaseRouter, Mock { } } -// MARK: - ConnectivityProtocol +// MARK: - CalendarManagerProtocol -open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { +open class CalendarManagerProtocolMock: CalendarManagerProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -959,51 +1019,176 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } - public var isInternetAvaliable: Bool { - get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } - } - private var __p_isInternetAvaliable: (Bool)? - public var isMobileData: Bool { - get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } - } - private var __p_isMobileData: (Bool)? - public var internetReachableSubject: CurrentValueSubject { - get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } - } - private var __p_internetReachableSubject: (CurrentValueSubject)? + open func createCalendarIfNeeded() { + addInvocation(.m_createCalendarIfNeeded) + let perform = methodPerformValue(.m_createCalendarIfNeeded) as? () -> Void + perform?() + } + + open func filterCoursesBySelected(fetchedCourses: [CourseForSync]) -> [CourseForSync] { + addInvocation(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) + let perform = methodPerformValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) as? ([CourseForSync]) -> Void + perform?(`fetchedCourses`) + var __value: [CourseForSync] + do { + __value = try methodReturnValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))).casted() + } catch { + onFatalFailure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + Failure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + } + return __value + } + + open func removeOldCalendar() { + addInvocation(.m_removeOldCalendar) + let perform = methodPerformValue(.m_removeOldCalendar) as? () -> Void + perform?() + } + + open func removeOutdatedEvents(courseID: String) { + addInvocation(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + } + + open func syncCourse(courseID: String, courseName: String, dates: CourseDates) { + addInvocation(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) + let perform = methodPerformValue(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) as? (String, String, CourseDates) -> Void + perform?(`courseID`, `courseName`, `dates`) + } + + open func requestAccess() -> Bool { + addInvocation(.m_requestAccess) + let perform = methodPerformValue(.m_requestAccess) as? () -> Void + perform?() + var __value: Bool + do { + __value = try methodReturnValue(.m_requestAccess).casted() + } catch { + onFatalFailure("Stub return value not specified for requestAccess(). Use given") + Failure("Stub return value not specified for requestAccess(). Use given") + } + return __value + } + + open func courseStatus(courseID: String) -> SyncStatus { + addInvocation(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: SyncStatus + do { + __value = try methodReturnValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch { + onFatalFailure("Stub return value not specified for courseStatus(courseID: String). Use given") + Failure("Stub return value not specified for courseStatus(courseID: String). Use given") + } + return __value + } + open func clearAllData(removeCalendar: Bool) { + addInvocation(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) + let perform = methodPerformValue(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) as? (Bool) -> Void + perform?(`removeCalendar`) + } + open func isDatesChanged(courseID: String, checksum: String) -> Bool { + addInvocation(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) + let perform = methodPerformValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) as? (String, String) -> Void + perform?(`courseID`, `checksum`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + Failure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + } + return __value + } fileprivate enum MethodType { - case p_isInternetAvaliable_get - case p_isMobileData_get - case p_internetReachableSubject_get + case m_createCalendarIfNeeded + case m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>) + case m_removeOldCalendar + case m_removeOutdatedEvents__courseID_courseID(Parameter) + case m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter, Parameter, Parameter) + case m_requestAccess + case m_courseStatus__courseID_courseID(Parameter) + case m_clearAllData__removeCalendar_removeCalendar(Parameter) + case m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match - case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match - case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + switch (lhs, rhs) { + case (.m_createCalendarIfNeeded, .m_createCalendarIfNeeded): return .match + + case (.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let lhsFetchedcourses), .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let rhsFetchedcourses)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFetchedcourses, rhs: rhsFetchedcourses, with: matcher), lhsFetchedcourses, rhsFetchedcourses, "fetchedCourses")) + return Matcher.ComparisonResult(results) + + case (.m_removeOldCalendar, .m_removeOldCalendar): return .match + + case (.m_removeOutdatedEvents__courseID_courseID(let lhsCourseid), .m_removeOutdatedEvents__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let lhsCourseid, let lhsCoursename, let lhsDates), .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let rhsCourseid, let rhsCoursename, let rhsDates)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDates, rhs: rhsDates, with: matcher), lhsDates, rhsDates, "dates")) + return Matcher.ComparisonResult(results) + + case (.m_requestAccess, .m_requestAccess): return .match + + case (.m_courseStatus__courseID_courseID(let lhsCourseid), .m_courseStatus__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_clearAllData__removeCalendar_removeCalendar(let lhsRemovecalendar), .m_clearAllData__removeCalendar_removeCalendar(let rhsRemovecalendar)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRemovecalendar, rhs: rhsRemovecalendar, with: matcher), lhsRemovecalendar, rhsRemovecalendar, "removeCalendar")) + return Matcher.ComparisonResult(results) + + case (.m_isDatesChanged__courseID_courseIDchecksum_checksum(let lhsCourseid, let lhsChecksum), .m_isDatesChanged__courseID_courseIDchecksum_checksum(let rhsCourseid, let rhsChecksum)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsChecksum, rhs: rhsChecksum, with: matcher), lhsChecksum, rhsChecksum, "checksum")) + return Matcher.ComparisonResult(results) default: return .none } } func intValue() -> Int { switch self { - case .p_isInternetAvaliable_get: return 0 - case .p_isMobileData_get: return 0 - case .p_internetReachableSubject_get: return 0 + case .m_createCalendarIfNeeded: return 0 + case let .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(p0): return p0.intValue + case .m_removeOldCalendar: return 0 + case let .m_removeOutdatedEvents__courseID_courseID(p0): return p0.intValue + case let .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case .m_requestAccess: return 0 + case let .m_courseStatus__courseID_courseID(p0): return p0.intValue + case let .m_clearAllData__removeCalendar_removeCalendar(p0): return p0.intValue + case let .m_isDatesChanged__courseID_courseIDchecksum_checksum(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { - case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" - case .p_isMobileData_get: return "[get] .isMobileData" - case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + case .m_createCalendarIfNeeded: return ".createCalendarIfNeeded()" + case .m_filterCoursesBySelected__fetchedCourses_fetchedCourses: return ".filterCoursesBySelected(fetchedCourses:)" + case .m_removeOldCalendar: return ".removeOldCalendar()" + case .m_removeOutdatedEvents__courseID_courseID: return ".removeOutdatedEvents(courseID:)" + case .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates: return ".syncCourse(courseID:courseName:dates:)" + case .m_requestAccess: return ".requestAccess()" + case .m_courseStatus__courseID_courseID: return ".courseStatus(courseID:)" + case .m_clearAllData__removeCalendar_removeCalendar: return ".clearAllData(removeCalendar:)" + case .m_isDatesChanged__courseID_courseIDchecksum_checksum: return ".isDatesChanged(courseID:checksum:)" } } } @@ -1016,30 +1201,94 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { super.init(products) } - public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willReturn: [CourseForSync]...) -> MethodStub { + return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func requestAccess(willReturn: Bool...) -> MethodStub { + return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { - return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func courseStatus(courseID: Parameter, willReturn: SyncStatus...) -> MethodStub { + return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willProduce: (Stubber<[CourseForSync]>) -> Void) -> MethodStub { + let willReturn: [[CourseForSync]] = [] + let given: Given = { return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseForSync]).self) + willProduce(stubber) + return given + } + public static func requestAccess(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given + } + public static func courseStatus(courseID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [SyncStatus] = [] + let given: Given = { return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (SyncStatus).self) + willProduce(stubber) + return given + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given } - } public struct Verify { fileprivate var method: MethodType - public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } - public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } - public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + public static func createCalendarIfNeeded() -> Verify { return Verify(method: .m_createCalendarIfNeeded)} + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>) -> Verify { return Verify(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`))} + public static func removeOldCalendar() -> Verify { return Verify(method: .m_removeOldCalendar)} + public static func removeOutdatedEvents(courseID: Parameter) -> Verify { return Verify(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`))} + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter) -> Verify { return Verify(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`))} + public static func requestAccess() -> Verify { return Verify(method: .m_requestAccess)} + public static func courseStatus(courseID: Parameter) -> Verify { return Verify(method: .m_courseStatus__courseID_courseID(`courseID`))} + public static func clearAllData(removeCalendar: Parameter) -> Verify { return Verify(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`))} + public static func isDatesChanged(courseID: Parameter, checksum: Parameter) -> Verify { return Verify(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`))} } public struct Perform { fileprivate var method: MethodType var performs: Any + public static func createCalendarIfNeeded(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_createCalendarIfNeeded, performs: perform) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, perform: @escaping ([CourseForSync]) -> Void) -> Perform { + return Perform(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), performs: perform) + } + public static func removeOldCalendar(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeOldCalendar, performs: perform) + } + public static func removeOutdatedEvents(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`), performs: perform) + } + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter, perform: @escaping (String, String, CourseDates) -> Void) -> Perform { + return Perform(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`), performs: perform) + } + public static func requestAccess(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_requestAccess, performs: perform) + } + public static func courseStatus(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_courseStatus__courseID_courseID(`courseID`), performs: perform) + } + public static func clearAllData(removeCalendar: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`), performs: perform) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), performs: perform) + } } public func given(_ method: Given) { @@ -1115,9 +1364,9 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } -// MARK: - CoreAnalytics +// MARK: - ConfigProtocol -open class CoreAnalyticsMock: CoreAnalytics, Mock { +open class ConfigProtocolMock: ConfigProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -1155,118 +1404,249 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var baseURL: URL { + get { invocations.append(.p_baseURL_get); return __p_baseURL ?? givenGetterValue(.p_baseURL_get, "ConfigProtocolMock - stub value for baseURL was not defined") } + } + private var __p_baseURL: (URL)? + public var baseSSOURL: URL { + get { invocations.append(.p_baseSSOURL_get); return __p_baseSSOURL ?? givenGetterValue(.p_baseSSOURL_get, "ConfigProtocolMock - stub value for baseSSOURL was not defined") } + } + private var __p_baseSSOURL: (URL)? + public var ssoFinishedURL: URL { + get { invocations.append(.p_ssoFinishedURL_get); return __p_ssoFinishedURL ?? givenGetterValue(.p_ssoFinishedURL_get, "ConfigProtocolMock - stub value for ssoFinishedURL was not defined") } + } + private var __p_ssoFinishedURL: (URL)? + public var ssoButtonTitle: [String: Any] { + get { invocations.append(.p_ssoButtonTitle_get); return __p_ssoButtonTitle ?? givenGetterValue(.p_ssoButtonTitle_get, "ConfigProtocolMock - stub value for ssoButtonTitle was not defined") } + } + private var __p_ssoButtonTitle: ([String: Any])? - open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void - perform?(`event`, `parameters`) - } + public var oAuthClientId: String { + get { invocations.append(.p_oAuthClientId_get); return __p_oAuthClientId ?? givenGetterValue(.p_oAuthClientId_get, "ConfigProtocolMock - stub value for oAuthClientId was not defined") } + } + private var __p_oAuthClientId: (String)? - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void - perform?(`event`, `biValue`, `parameters`) - } + public var tokenType: TokenType { + get { invocations.append(.p_tokenType_get); return __p_tokenType ?? givenGetterValue(.p_tokenType_get, "ConfigProtocolMock - stub value for tokenType was not defined") } + } + private var __p_tokenType: (TokenType)? - open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { - addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) - let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void - perform?(`event`, `biValue`, `action`, `rating`) - } + public var feedbackEmail: String { + get { invocations.append(.p_feedbackEmail_get); return __p_feedbackEmail ?? givenGetterValue(.p_feedbackEmail_get, "ConfigProtocolMock - stub value for feedbackEmail was not defined") } + } + private var __p_feedbackEmail: (String)? - open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { - addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) - let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void - perform?(`event`, `bivalue`, `value`, `oldValue`) - } + public var appStoreLink: String { + get { invocations.append(.p_appStoreLink_get); return __p_appStoreLink ?? givenGetterValue(.p_appStoreLink_get, "ConfigProtocolMock - stub value for appStoreLink was not defined") } + } + private var __p_appStoreLink: (String)? - open func trackEvent(_ event: AnalyticsEvent) { - addInvocation(.m_trackEvent__event(Parameter.value(`event`))) - let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void - perform?(`event`) - } + public var faq: URL? { + get { invocations.append(.p_faq_get); return __p_faq ?? optionalGivenGetterValue(.p_faq_get, "ConfigProtocolMock - stub value for faq was not defined") } + } + private var __p_faq: (URL)? - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void - perform?(`event`, `biValue`) - } + public var platformName: String { + get { invocations.append(.p_platformName_get); return __p_platformName ?? givenGetterValue(.p_platformName_get, "ConfigProtocolMock - stub value for platformName was not defined") } + } + private var __p_platformName: (String)? + public var agreement: AgreementConfig { + get { invocations.append(.p_agreement_get); return __p_agreement ?? givenGetterValue(.p_agreement_get, "ConfigProtocolMock - stub value for agreement was not defined") } + } + private var __p_agreement: (AgreementConfig)? - fileprivate enum MethodType { - case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) - case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) - case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) - case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) - case m_trackEvent__event(Parameter) - case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + public var firebase: FirebaseConfig { + get { invocations.append(.p_firebase_get); return __p_firebase ?? givenGetterValue(.p_firebase_get, "ConfigProtocolMock - stub value for firebase was not defined") } + } + private var __p_firebase: (FirebaseConfig)? - static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { - case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + public var facebook: FacebookConfig { + get { invocations.append(.p_facebook_get); return __p_facebook ?? givenGetterValue(.p_facebook_get, "ConfigProtocolMock - stub value for facebook was not defined") } + } + private var __p_facebook: (FacebookConfig)? - case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + public var microsoft: MicrosoftConfig { + get { invocations.append(.p_microsoft_get); return __p_microsoft ?? givenGetterValue(.p_microsoft_get, "ConfigProtocolMock - stub value for microsoft was not defined") } + } + private var __p_microsoft: (MicrosoftConfig)? - case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) - return Matcher.ComparisonResult(results) + public var google: GoogleConfig { + get { invocations.append(.p_google_get); return __p_google ?? givenGetterValue(.p_google_get, "ConfigProtocolMock - stub value for google was not defined") } + } + private var __p_google: (GoogleConfig)? - case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) - return Matcher.ComparisonResult(results) + public var appleSignIn: AppleSignInConfig { + get { invocations.append(.p_appleSignIn_get); return __p_appleSignIn ?? givenGetterValue(.p_appleSignIn_get, "ConfigProtocolMock - stub value for appleSignIn was not defined") } + } + private var __p_appleSignIn: (AppleSignInConfig)? - case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - return Matcher.ComparisonResult(results) + public var features: FeaturesConfig { + get { invocations.append(.p_features_get); return __p_features ?? givenGetterValue(.p_features_get, "ConfigProtocolMock - stub value for features was not defined") } + } + private var __p_features: (FeaturesConfig)? - case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - return Matcher.ComparisonResult(results) - default: return .none - } - } + public var theme: ThemeConfig { + get { invocations.append(.p_theme_get); return __p_theme ?? givenGetterValue(.p_theme_get, "ConfigProtocolMock - stub value for theme was not defined") } + } + private var __p_theme: (ThemeConfig)? - func intValue() -> Int { - switch self { - case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue - case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue - case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue - case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue - case let .m_trackEvent__event(p0): return p0.intValue - case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + public var uiComponents: UIComponentsConfig { + get { invocations.append(.p_uiComponents_get); return __p_uiComponents ?? givenGetterValue(.p_uiComponents_get, "ConfigProtocolMock - stub value for uiComponents was not defined") } + } + private var __p_uiComponents: (UIComponentsConfig)? + + public var discovery: DiscoveryConfig { + get { invocations.append(.p_discovery_get); return __p_discovery ?? givenGetterValue(.p_discovery_get, "ConfigProtocolMock - stub value for discovery was not defined") } + } + private var __p_discovery: (DiscoveryConfig)? + + public var dashboard: DashboardConfig { + get { invocations.append(.p_dashboard_get); return __p_dashboard ?? givenGetterValue(.p_dashboard_get, "ConfigProtocolMock - stub value for dashboard was not defined") } + } + private var __p_dashboard: (DashboardConfig)? + + public var braze: BrazeConfig { + get { invocations.append(.p_braze_get); return __p_braze ?? givenGetterValue(.p_braze_get, "ConfigProtocolMock - stub value for braze was not defined") } + } + private var __p_braze: (BrazeConfig)? + + public var branch: BranchConfig { + get { invocations.append(.p_branch_get); return __p_branch ?? givenGetterValue(.p_branch_get, "ConfigProtocolMock - stub value for branch was not defined") } + } + private var __p_branch: (BranchConfig)? + + public var program: DiscoveryConfig { + get { invocations.append(.p_program_get); return __p_program ?? givenGetterValue(.p_program_get, "ConfigProtocolMock - stub value for program was not defined") } + } + private var __p_program: (DiscoveryConfig)? + + public var URIScheme: String { + get { invocations.append(.p_URIScheme_get); return __p_URIScheme ?? givenGetterValue(.p_URIScheme_get, "ConfigProtocolMock - stub value for URIScheme was not defined") } + } + private var __p_URIScheme: (String)? + + + + + + + fileprivate enum MethodType { + case p_baseURL_get + case p_baseSSOURL_get + case p_ssoFinishedURL_get + case p_ssoButtonTitle_get + case p_oAuthClientId_get + case p_tokenType_get + case p_feedbackEmail_get + case p_appStoreLink_get + case p_faq_get + case p_platformName_get + case p_agreement_get + case p_firebase_get + case p_facebook_get + case p_microsoft_get + case p_google_get + case p_appleSignIn_get + case p_features_get + case p_theme_get + case p_uiComponents_get + case p_discovery_get + case p_dashboard_get + case p_braze_get + case p_branch_get + case p_program_get + case p_URIScheme_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_baseURL_get,.p_baseURL_get): return Matcher.ComparisonResult.match + case (.p_baseSSOURL_get,.p_baseSSOURL_get): return Matcher.ComparisonResult.match + case (.p_ssoFinishedURL_get,.p_ssoFinishedURL_get): return Matcher.ComparisonResult.match + case (.p_ssoButtonTitle_get,.p_ssoButtonTitle_get): return Matcher.ComparisonResult.match + case (.p_oAuthClientId_get,.p_oAuthClientId_get): return Matcher.ComparisonResult.match + case (.p_tokenType_get,.p_tokenType_get): return Matcher.ComparisonResult.match + case (.p_feedbackEmail_get,.p_feedbackEmail_get): return Matcher.ComparisonResult.match + case (.p_appStoreLink_get,.p_appStoreLink_get): return Matcher.ComparisonResult.match + case (.p_faq_get,.p_faq_get): return Matcher.ComparisonResult.match + case (.p_platformName_get,.p_platformName_get): return Matcher.ComparisonResult.match + case (.p_agreement_get,.p_agreement_get): return Matcher.ComparisonResult.match + case (.p_firebase_get,.p_firebase_get): return Matcher.ComparisonResult.match + case (.p_facebook_get,.p_facebook_get): return Matcher.ComparisonResult.match + case (.p_microsoft_get,.p_microsoft_get): return Matcher.ComparisonResult.match + case (.p_google_get,.p_google_get): return Matcher.ComparisonResult.match + case (.p_appleSignIn_get,.p_appleSignIn_get): return Matcher.ComparisonResult.match + case (.p_features_get,.p_features_get): return Matcher.ComparisonResult.match + case (.p_theme_get,.p_theme_get): return Matcher.ComparisonResult.match + case (.p_uiComponents_get,.p_uiComponents_get): return Matcher.ComparisonResult.match + case (.p_discovery_get,.p_discovery_get): return Matcher.ComparisonResult.match + case (.p_dashboard_get,.p_dashboard_get): return Matcher.ComparisonResult.match + case (.p_braze_get,.p_braze_get): return Matcher.ComparisonResult.match + case (.p_branch_get,.p_branch_get): return Matcher.ComparisonResult.match + case (.p_program_get,.p_program_get): return Matcher.ComparisonResult.match + case (.p_URIScheme_get,.p_URIScheme_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_baseURL_get: return 0 + case .p_baseSSOURL_get: return 0 + case .p_ssoFinishedURL_get: return 0 + case .p_ssoButtonTitle_get: return 0 + case .p_oAuthClientId_get: return 0 + case .p_tokenType_get: return 0 + case .p_feedbackEmail_get: return 0 + case .p_appStoreLink_get: return 0 + case .p_faq_get: return 0 + case .p_platformName_get: return 0 + case .p_agreement_get: return 0 + case .p_firebase_get: return 0 + case .p_facebook_get: return 0 + case .p_microsoft_get: return 0 + case .p_google_get: return 0 + case .p_appleSignIn_get: return 0 + case .p_features_get: return 0 + case .p_theme_get: return 0 + case .p_uiComponents_get: return 0 + case .p_discovery_get: return 0 + case .p_dashboard_get: return 0 + case .p_braze_get: return 0 + case .p_branch_get: return 0 + case .p_program_get: return 0 + case .p_URIScheme_get: return 0 } } func assertionName() -> String { switch self { - case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" - case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" - case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" - case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" - case .m_trackEvent__event: return ".trackEvent(_:)" - case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .p_baseURL_get: return "[get] .baseURL" + case .p_baseSSOURL_get: return "[get] .baseSSOURL" + case .p_ssoFinishedURL_get: return "[get] .ssoFinishedURL" + case .p_ssoButtonTitle_get: return "[get] .ssoButtonTitle" + case .p_oAuthClientId_get: return "[get] .oAuthClientId" + case .p_tokenType_get: return "[get] .tokenType" + case .p_feedbackEmail_get: return "[get] .feedbackEmail" + case .p_appStoreLink_get: return "[get] .appStoreLink" + case .p_faq_get: return "[get] .faq" + case .p_platformName_get: return "[get] .platformName" + case .p_agreement_get: return "[get] .agreement" + case .p_firebase_get: return "[get] .firebase" + case .p_facebook_get: return "[get] .facebook" + case .p_microsoft_get: return "[get] .microsoft" + case .p_google_get: return "[get] .google" + case .p_appleSignIn_get: return "[get] .appleSignIn" + case .p_features_get: return "[get] .features" + case .p_theme_get: return "[get] .theme" + case .p_uiComponents_get: return "[get] .uiComponents" + case .p_discovery_get: return "[get] .discovery" + case .p_dashboard_get: return "[get] .dashboard" + case .p_braze_get: return "[get] .braze" + case .p_branch_get: return "[get] .branch" + case .p_program_get: return "[get] .program" + case .p_URIScheme_get: return "[get] .URIScheme" } } } @@ -1279,41 +1659,1669 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { super.init(products) } + public static func baseURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func baseSSOURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseSSOURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoFinishedURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_ssoFinishedURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoButtonTitle(getter defaultValue: [String: Any]...) -> PropertyStub { + return Given(method: .p_ssoButtonTitle_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func oAuthClientId(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_oAuthClientId_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func tokenType(getter defaultValue: TokenType...) -> PropertyStub { + return Given(method: .p_tokenType_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func feedbackEmail(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_feedbackEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appStoreLink(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_appStoreLink_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func faq(getter defaultValue: URL?...) -> PropertyStub { + return Given(method: .p_faq_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func platformName(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_platformName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func agreement(getter defaultValue: AgreementConfig...) -> PropertyStub { + return Given(method: .p_agreement_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func firebase(getter defaultValue: FirebaseConfig...) -> PropertyStub { + return Given(method: .p_firebase_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func facebook(getter defaultValue: FacebookConfig...) -> PropertyStub { + return Given(method: .p_facebook_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func microsoft(getter defaultValue: MicrosoftConfig...) -> PropertyStub { + return Given(method: .p_microsoft_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func google(getter defaultValue: GoogleConfig...) -> PropertyStub { + return Given(method: .p_google_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignIn(getter defaultValue: AppleSignInConfig...) -> PropertyStub { + return Given(method: .p_appleSignIn_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func features(getter defaultValue: FeaturesConfig...) -> PropertyStub { + return Given(method: .p_features_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func theme(getter defaultValue: ThemeConfig...) -> PropertyStub { + return Given(method: .p_theme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func uiComponents(getter defaultValue: UIComponentsConfig...) -> PropertyStub { + return Given(method: .p_uiComponents_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func discovery(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_discovery_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func dashboard(getter defaultValue: DashboardConfig...) -> PropertyStub { + return Given(method: .p_dashboard_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func braze(getter defaultValue: BrazeConfig...) -> PropertyStub { + return Given(method: .p_braze_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func branch(getter defaultValue: BranchConfig...) -> PropertyStub { + return Given(method: .p_branch_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func program(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_program_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func URIScheme(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_URIScheme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } } public struct Verify { fileprivate var method: MethodType - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} - public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static var baseURL: Verify { return Verify(method: .p_baseURL_get) } + public static var baseSSOURL: Verify { return Verify(method: .p_baseSSOURL_get) } + public static var ssoFinishedURL: Verify { return Verify(method: .p_ssoFinishedURL_get) } + public static var ssoButtonTitle: Verify { return Verify(method: .p_ssoButtonTitle_get) } + public static var oAuthClientId: Verify { return Verify(method: .p_oAuthClientId_get) } + public static var tokenType: Verify { return Verify(method: .p_tokenType_get) } + public static var feedbackEmail: Verify { return Verify(method: .p_feedbackEmail_get) } + public static var appStoreLink: Verify { return Verify(method: .p_appStoreLink_get) } + public static var faq: Verify { return Verify(method: .p_faq_get) } + public static var platformName: Verify { return Verify(method: .p_platformName_get) } + public static var agreement: Verify { return Verify(method: .p_agreement_get) } + public static var firebase: Verify { return Verify(method: .p_firebase_get) } + public static var facebook: Verify { return Verify(method: .p_facebook_get) } + public static var microsoft: Verify { return Verify(method: .p_microsoft_get) } + public static var google: Verify { return Verify(method: .p_google_get) } + public static var appleSignIn: Verify { return Verify(method: .p_appleSignIn_get) } + public static var features: Verify { return Verify(method: .p_features_get) } + public static var theme: Verify { return Verify(method: .p_theme_get) } + public static var uiComponents: Verify { return Verify(method: .p_uiComponents_get) } + public static var discovery: Verify { return Verify(method: .p_discovery_get) } + public static var dashboard: Verify { return Verify(method: .p_dashboard_get) } + public static var braze: Verify { return Verify(method: .p_braze_get) } + public static var branch: Verify { return Verify(method: .p_branch_get) } + public static var program: Verify { return Verify(method: .p_program_get) } + public static var URIScheme: Verify { return Verify(method: .p_URIScheme_get) } } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) } - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil } - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { - return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - ConnectivityProtocol + +open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var isInternetAvaliable: Bool { + get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } + } + private var __p_isInternetAvaliable: (Bool)? + + public var isMobileData: Bool { + get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } + } + private var __p_isMobileData: (Bool)? + + public var internetReachableSubject: CurrentValueSubject { + get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } + } + private var __p_internetReachableSubject: (CurrentValueSubject)? + + + + + + + fileprivate enum MethodType { + case p_isInternetAvaliable_get + case p_isMobileData_get + case p_internetReachableSubject_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match + case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match + case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + default: return .none + } } - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { - return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) + + func intValue() -> Int { + switch self { + case .p_isInternetAvaliable_get: return 0 + case .p_isMobileData_get: return 0 + case .p_internetReachableSubject_get: return 0 + } } - public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { - return Perform(method: .m_trackEvent__event(`event`), performs: perform) + func assertionName() -> String { + switch self { + case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" + case .p_isMobileData_get: return "[get] .isMobileData" + case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + } } - public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { + return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } + public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } + public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreAnalytics + +open class CoreAnalyticsMock: CoreAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { + addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) + let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void + perform?(`event`, `biValue`, `action`, `rating`) + } + + open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { + addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) + let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void + perform?(`event`, `bivalue`, `value`, `oldValue`) + } + + open func trackEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + + fileprivate enum MethodType { + case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) + case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) + case m_trackEvent__event(Parameter) + case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) + return Matcher.ComparisonResult(results) + + case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_trackEvent__event(p0): return p0.intValue + case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" + case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" + case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" + case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" + case .m_trackEvent__event: return ".trackEvent(_:)" + case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} + public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { + return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) + } + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { + return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) + } + public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackEvent__event(`event`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CorePersistenceProtocol + +open class CorePersistenceProtocolMock: CorePersistenceProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func set(userId: Int) { + addInvocation(.m_set__userId_userId(Parameter.value(`userId`))) + let perform = methodPerformValue(.m_set__userId_userId(Parameter.value(`userId`))) as? (Int) -> Void + perform?(`userId`) + } + + open func getUserID() -> Int? { + addInvocation(.m_getUserID) + let perform = methodPerformValue(.m_getUserID) as? () -> Void + perform?() + var __value: Int? = nil + do { + __value = try methodReturnValue(.m_getUserID).casted() + } catch { + // do nothing + } + return __value + } + + open func publisher() -> AnyPublisher { + addInvocation(.m_publisher) + let perform = methodPerformValue(.m_publisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_publisher).casted() + } catch { + onFatalFailure("Stub return value not specified for publisher(). Use given") + Failure("Stub return value not specified for publisher(). Use given") + } + return __value + } + + open func addToDownloadQueue(tasks: [DownloadDataTask]) { + addInvocation(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) as? ([DownloadDataTask]) -> Void + perform?(`tasks`) + } + + open func saveOfflineProgress(progress: OfflineProgress) { + addInvocation(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) + let perform = methodPerformValue(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) as? (OfflineProgress) -> Void + perform?(`progress`) + } + + open func loadProgress(for blockID: String) -> OfflineProgress? { + addInvocation(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + var __value: OfflineProgress? = nil + do { + __value = try methodReturnValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))).casted() + } catch { + // do nothing + } + return __value + } + + open func loadAllOfflineProgress() -> [OfflineProgress] { + addInvocation(.m_loadAllOfflineProgress) + let perform = methodPerformValue(.m_loadAllOfflineProgress) as? () -> Void + perform?() + var __value: [OfflineProgress] + do { + __value = try methodReturnValue(.m_loadAllOfflineProgress).casted() + } catch { + onFatalFailure("Stub return value not specified for loadAllOfflineProgress(). Use given") + Failure("Stub return value not specified for loadAllOfflineProgress(). Use given") + } + return __value + } + + open func deleteProgress(for blockID: String) { + addInvocation(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + } + + open func deleteAllProgress() { + addInvocation(.m_deleteAllProgress) + let perform = methodPerformValue(.m_deleteAllProgress) as? () -> Void + perform?() + } + + open func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) { + addInvocation(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) as? ([CourseBlock], DownloadQuality) -> Void + perform?(`blocks`, `downloadQuality`) + } + + open func nextBlockForDownloading() -> DownloadDataTask? { + addInvocation(.m_nextBlockForDownloading) + let perform = methodPerformValue(.m_nextBlockForDownloading) as? () -> Void + perform?() + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_nextBlockForDownloading).casted() + } catch { + // do nothing + } + return __value + } + + open func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) { + addInvocation(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) + let perform = methodPerformValue(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) as? (String, DownloadState, Data?) -> Void + perform?(`id`, `state`, `resumeData`) + } + + open func deleteDownloadDataTask(id: String) throws { + addInvocation(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) + let perform = methodPerformValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) as? (String) -> Void + perform?(`id`) + do { + _ = try methodReturnValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func saveDownloadDataTask(_ task: DownloadDataTask) { + addInvocation(.m_saveDownloadDataTask__task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_saveDownloadDataTask__task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) + } + + open func downloadDataTask(for blockId: String) -> DownloadDataTask? { + addInvocation(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) + let perform = methodPerformValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void + perform?(`blockId`) + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))).casted() + } catch { + // do nothing + } + return __value + } + + open func getDownloadDataTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasks) + let perform = methodPerformValue(.m_getDownloadDataTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasks(). Use given") + Failure("Stub return value not specified for getDownloadDataTasks(). Use given") + } + return __value + } + + open func getDownloadDataTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + } + return __value + } + + + fileprivate enum MethodType { + case m_set__userId_userId(Parameter) + case m_getUserID + case m_publisher + case m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>) + case m_saveOfflineProgress__progress_progress(Parameter) + case m_loadProgress__for_blockID(Parameter) + case m_loadAllOfflineProgress + case m_deleteProgress__for_blockID(Parameter) + case m_deleteAllProgress + case m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>, Parameter) + case m_nextBlockForDownloading + case m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter, Parameter, Parameter) + case m_deleteDownloadDataTask__id_id(Parameter) + case m_saveDownloadDataTask__task(Parameter) + case m_downloadDataTask__for_blockId(Parameter) + case m_getDownloadDataTasks + case m_getDownloadDataTasksForCourse__courseId(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_set__userId_userId(let lhsUserid), .m_set__userId_userId(let rhsUserid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUserid, rhs: rhsUserid, with: matcher), lhsUserid, rhsUserid, "userId")) + return Matcher.ComparisonResult(results) + + case (.m_getUserID, .m_getUserID): return .match + + case (.m_publisher, .m_publisher): return .match + + case (.m_addToDownloadQueue__tasks_tasks(let lhsTasks), .m_addToDownloadQueue__tasks_tasks(let rhsTasks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTasks, rhs: rhsTasks, with: matcher), lhsTasks, rhsTasks, "tasks")) + return Matcher.ComparisonResult(results) + + case (.m_saveOfflineProgress__progress_progress(let lhsProgress), .m_saveOfflineProgress__progress_progress(let rhsProgress)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsProgress, rhs: rhsProgress, with: matcher), lhsProgress, rhsProgress, "progress")) + return Matcher.ComparisonResult(results) + + case (.m_loadProgress__for_blockID(let lhsBlockid), .m_loadProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_loadAllOfflineProgress, .m_loadAllOfflineProgress): return .match + + case (.m_deleteProgress__for_blockID(let lhsBlockid), .m_deleteProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_deleteAllProgress, .m_deleteAllProgress): return .match + + case (.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let lhsBlocks, let lhsDownloadquality), .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let rhsBlocks, let rhsDownloadquality)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDownloadquality, rhs: rhsDownloadquality, with: matcher), lhsDownloadquality, rhsDownloadquality, "downloadQuality")) + return Matcher.ComparisonResult(results) + + case (.m_nextBlockForDownloading, .m_nextBlockForDownloading): return .match + + case (.m_updateDownloadState__id_idstate_stateresumeData_resumeData(let lhsId, let lhsState, let lhsResumedata), .m_updateDownloadState__id_idstate_stateresumeData_resumeData(let rhsId, let rhsState, let rhsResumedata)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsState, rhs: rhsState, with: matcher), lhsState, rhsState, "state")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResumedata, rhs: rhsResumedata, with: matcher), lhsResumedata, rhsResumedata, "resumeData")) + return Matcher.ComparisonResult(results) + + case (.m_deleteDownloadDataTask__id_id(let lhsId), .m_deleteDownloadDataTask__id_id(let rhsId)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + return Matcher.ComparisonResult(results) + + case (.m_saveDownloadDataTask__task(let lhsTask), .m_saveDownloadDataTask__task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "_ task")) + return Matcher.ComparisonResult(results) + + case (.m_downloadDataTask__for_blockId(let lhsBlockid), .m_downloadDataTask__for_blockId(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) + return Matcher.ComparisonResult(results) + + case (.m_getDownloadDataTasks, .m_getDownloadDataTasks): return .match + + case (.m_getDownloadDataTasksForCourse__courseId(let lhsCourseid), .m_getDownloadDataTasksForCourse__courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_set__userId_userId(p0): return p0.intValue + case .m_getUserID: return 0 + case .m_publisher: return 0 + case let .m_addToDownloadQueue__tasks_tasks(p0): return p0.intValue + case let .m_saveOfflineProgress__progress_progress(p0): return p0.intValue + case let .m_loadProgress__for_blockID(p0): return p0.intValue + case .m_loadAllOfflineProgress: return 0 + case let .m_deleteProgress__for_blockID(p0): return p0.intValue + case .m_deleteAllProgress: return 0 + case let .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(p0, p1): return p0.intValue + p1.intValue + case .m_nextBlockForDownloading: return 0 + case let .m_updateDownloadState__id_idstate_stateresumeData_resumeData(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_deleteDownloadDataTask__id_id(p0): return p0.intValue + case let .m_saveDownloadDataTask__task(p0): return p0.intValue + case let .m_downloadDataTask__for_blockId(p0): return p0.intValue + case .m_getDownloadDataTasks: return 0 + case let .m_getDownloadDataTasksForCourse__courseId(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_set__userId_userId: return ".set(userId:)" + case .m_getUserID: return ".getUserID()" + case .m_publisher: return ".publisher()" + case .m_addToDownloadQueue__tasks_tasks: return ".addToDownloadQueue(tasks:)" + case .m_saveOfflineProgress__progress_progress: return ".saveOfflineProgress(progress:)" + case .m_loadProgress__for_blockID: return ".loadProgress(for:)" + case .m_loadAllOfflineProgress: return ".loadAllOfflineProgress()" + case .m_deleteProgress__for_blockID: return ".deleteProgress(for:)" + case .m_deleteAllProgress: return ".deleteAllProgress()" + case .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality: return ".addToDownloadQueue(blocks:downloadQuality:)" + case .m_nextBlockForDownloading: return ".nextBlockForDownloading()" + case .m_updateDownloadState__id_idstate_stateresumeData_resumeData: return ".updateDownloadState(id:state:resumeData:)" + case .m_deleteDownloadDataTask__id_id: return ".deleteDownloadDataTask(id:)" + case .m_saveDownloadDataTask__task: return ".saveDownloadDataTask(_:)" + case .m_downloadDataTask__for_blockId: return ".downloadDataTask(for:)" + case .m_getDownloadDataTasks: return ".getDownloadDataTasks()" + case .m_getDownloadDataTasksForCourse__courseId: return ".getDownloadDataTasksForCourse(_:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func getUserID(willReturn: Int?...) -> MethodStub { + return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func publisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadProgress(for blockID: Parameter, willReturn: OfflineProgress?...) -> MethodStub { + return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadAllOfflineProgress(willReturn: [OfflineProgress]...) -> MethodStub { + return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func nextBlockForDownloading(willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func downloadDataTask(for blockId: Parameter, willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getUserID(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Int?] = [] + let given: Given = { return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Int?).self) + willProduce(stubber) + return given + } + public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func loadProgress(for blockID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [OfflineProgress?] = [] + let given: Given = { return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (OfflineProgress?).self) + willProduce(stubber) + return given + } + public static func loadAllOfflineProgress(willProduce: (Stubber<[OfflineProgress]>) -> Void) -> MethodStub { + let willReturn: [[OfflineProgress]] = [] + let given: Given = { return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([OfflineProgress]).self) + willProduce(stubber) + return given + } + public static func nextBlockForDownloading(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func downloadDataTask(for blockId: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func deleteDownloadDataTask(id: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func deleteDownloadDataTask(id: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func set(userId: Parameter) -> Verify { return Verify(method: .m_set__userId_userId(`userId`))} + public static func getUserID() -> Verify { return Verify(method: .m_getUserID)} + public static func publisher() -> Verify { return Verify(method: .m_publisher)} + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>) -> Verify { return Verify(method: .m_addToDownloadQueue__tasks_tasks(`tasks`))} + public static func saveOfflineProgress(progress: Parameter) -> Verify { return Verify(method: .m_saveOfflineProgress__progress_progress(`progress`))} + public static func loadProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_loadProgress__for_blockID(`blockID`))} + public static func loadAllOfflineProgress() -> Verify { return Verify(method: .m_loadAllOfflineProgress)} + public static func deleteProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_deleteProgress__for_blockID(`blockID`))} + public static func deleteAllProgress() -> Verify { return Verify(method: .m_deleteAllProgress)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`))} + public static func nextBlockForDownloading() -> Verify { return Verify(method: .m_nextBlockForDownloading)} + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter) -> Verify { return Verify(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`))} + public static func deleteDownloadDataTask(id: Parameter) -> Verify { return Verify(method: .m_deleteDownloadDataTask__id_id(`id`))} + public static func saveDownloadDataTask(_ task: Parameter) -> Verify { return Verify(method: .m_saveDownloadDataTask__task(`task`))} + public static func downloadDataTask(for blockId: Parameter) -> Verify { return Verify(method: .m_downloadDataTask__for_blockId(`blockId`))} + public static func getDownloadDataTasks() -> Verify { return Verify(method: .m_getDownloadDataTasks)} + public static func getDownloadDataTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func set(userId: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_set__userId_userId(`userId`), performs: perform) + } + public static func getUserID(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getUserID, performs: perform) + } + public static func publisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_publisher, performs: perform) + } + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>, perform: @escaping ([DownloadDataTask]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__tasks_tasks(`tasks`), performs: perform) + } + public static func saveOfflineProgress(progress: Parameter, perform: @escaping (OfflineProgress) -> Void) -> Perform { + return Perform(method: .m_saveOfflineProgress__progress_progress(`progress`), performs: perform) + } + public static func loadProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_loadProgress__for_blockID(`blockID`), performs: perform) + } + public static func loadAllOfflineProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_loadAllOfflineProgress, performs: perform) + } + public static func deleteProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteProgress__for_blockID(`blockID`), performs: perform) + } + public static func deleteAllProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteAllProgress, performs: perform) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter, perform: @escaping ([CourseBlock], DownloadQuality) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`), performs: perform) + } + public static func nextBlockForDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_nextBlockForDownloading, performs: perform) + } + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter, perform: @escaping (String, DownloadState, Data?) -> Void) -> Perform { + return Perform(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`), performs: perform) + } + public static func deleteDownloadDataTask(id: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteDownloadDataTask__id_id(`id`), performs: perform) + } + public static func saveDownloadDataTask(_ task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_saveDownloadDataTask__task(`task`), performs: perform) + } + public static func downloadDataTask(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_downloadDataTask__for_blockId(`blockId`), performs: perform) + } + public static func getDownloadDataTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasks, performs: perform) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreStorage + +open class CoreStorageMock: CoreStorage, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var accessToken: String? { + get { invocations.append(.p_accessToken_get); return __p_accessToken ?? optionalGivenGetterValue(.p_accessToken_get, "CoreStorageMock - stub value for accessToken was not defined") } + set { invocations.append(.p_accessToken_set(.value(newValue))); __p_accessToken = newValue } + } + private var __p_accessToken: (String)? + + public var refreshToken: String? { + get { invocations.append(.p_refreshToken_get); return __p_refreshToken ?? optionalGivenGetterValue(.p_refreshToken_get, "CoreStorageMock - stub value for refreshToken was not defined") } + set { invocations.append(.p_refreshToken_set(.value(newValue))); __p_refreshToken = newValue } + } + private var __p_refreshToken: (String)? + + public var pushToken: String? { + get { invocations.append(.p_pushToken_get); return __p_pushToken ?? optionalGivenGetterValue(.p_pushToken_get, "CoreStorageMock - stub value for pushToken was not defined") } + set { invocations.append(.p_pushToken_set(.value(newValue))); __p_pushToken = newValue } + } + private var __p_pushToken: (String)? + + public var appleSignFullName: String? { + get { invocations.append(.p_appleSignFullName_get); return __p_appleSignFullName ?? optionalGivenGetterValue(.p_appleSignFullName_get, "CoreStorageMock - stub value for appleSignFullName was not defined") } + set { invocations.append(.p_appleSignFullName_set(.value(newValue))); __p_appleSignFullName = newValue } + } + private var __p_appleSignFullName: (String)? + + public var appleSignEmail: String? { + get { invocations.append(.p_appleSignEmail_get); return __p_appleSignEmail ?? optionalGivenGetterValue(.p_appleSignEmail_get, "CoreStorageMock - stub value for appleSignEmail was not defined") } + set { invocations.append(.p_appleSignEmail_set(.value(newValue))); __p_appleSignEmail = newValue } + } + private var __p_appleSignEmail: (String)? + + public var cookiesDate: Date? { + get { invocations.append(.p_cookiesDate_get); return __p_cookiesDate ?? optionalGivenGetterValue(.p_cookiesDate_get, "CoreStorageMock - stub value for cookiesDate was not defined") } + set { invocations.append(.p_cookiesDate_set(.value(newValue))); __p_cookiesDate = newValue } + } + private var __p_cookiesDate: (Date)? + + public var reviewLastShownVersion: String? { + get { invocations.append(.p_reviewLastShownVersion_get); return __p_reviewLastShownVersion ?? optionalGivenGetterValue(.p_reviewLastShownVersion_get, "CoreStorageMock - stub value for reviewLastShownVersion was not defined") } + set { invocations.append(.p_reviewLastShownVersion_set(.value(newValue))); __p_reviewLastShownVersion = newValue } + } + private var __p_reviewLastShownVersion: (String)? + + public var lastReviewDate: Date? { + get { invocations.append(.p_lastReviewDate_get); return __p_lastReviewDate ?? optionalGivenGetterValue(.p_lastReviewDate_get, "CoreStorageMock - stub value for lastReviewDate was not defined") } + set { invocations.append(.p_lastReviewDate_set(.value(newValue))); __p_lastReviewDate = newValue } + } + private var __p_lastReviewDate: (Date)? + + public var user: DataLayer.User? { + get { invocations.append(.p_user_get); return __p_user ?? optionalGivenGetterValue(.p_user_get, "CoreStorageMock - stub value for user was not defined") } + set { invocations.append(.p_user_set(.value(newValue))); __p_user = newValue } + } + private var __p_user: (DataLayer.User)? + + public var userSettings: UserSettings? { + get { invocations.append(.p_userSettings_get); return __p_userSettings ?? optionalGivenGetterValue(.p_userSettings_get, "CoreStorageMock - stub value for userSettings was not defined") } + set { invocations.append(.p_userSettings_set(.value(newValue))); __p_userSettings = newValue } + } + private var __p_userSettings: (UserSettings)? + + public var resetAppSupportDirectoryUserData: Bool? { + get { invocations.append(.p_resetAppSupportDirectoryUserData_get); return __p_resetAppSupportDirectoryUserData ?? optionalGivenGetterValue(.p_resetAppSupportDirectoryUserData_get, "CoreStorageMock - stub value for resetAppSupportDirectoryUserData was not defined") } + set { invocations.append(.p_resetAppSupportDirectoryUserData_set(.value(newValue))); __p_resetAppSupportDirectoryUserData = newValue } + } + private var __p_resetAppSupportDirectoryUserData: (Bool)? + + public var useRelativeDates: Bool { + get { invocations.append(.p_useRelativeDates_get); return __p_useRelativeDates ?? givenGetterValue(.p_useRelativeDates_get, "CoreStorageMock - stub value for useRelativeDates was not defined") } + set { invocations.append(.p_useRelativeDates_set(.value(newValue))); __p_useRelativeDates = newValue } + } + private var __p_useRelativeDates: (Bool)? + + + + + + open func clear() { + addInvocation(.m_clear) + let perform = methodPerformValue(.m_clear) as? () -> Void + perform?() + } + + + fileprivate enum MethodType { + case m_clear + case p_accessToken_get + case p_accessToken_set(Parameter) + case p_refreshToken_get + case p_refreshToken_set(Parameter) + case p_pushToken_get + case p_pushToken_set(Parameter) + case p_appleSignFullName_get + case p_appleSignFullName_set(Parameter) + case p_appleSignEmail_get + case p_appleSignEmail_set(Parameter) + case p_cookiesDate_get + case p_cookiesDate_set(Parameter) + case p_reviewLastShownVersion_get + case p_reviewLastShownVersion_set(Parameter) + case p_lastReviewDate_get + case p_lastReviewDate_set(Parameter) + case p_user_get + case p_user_set(Parameter) + case p_userSettings_get + case p_userSettings_set(Parameter) + case p_resetAppSupportDirectoryUserData_get + case p_resetAppSupportDirectoryUserData_set(Parameter) + case p_useRelativeDates_get + case p_useRelativeDates_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_clear, .m_clear): return .match + case (.p_accessToken_get,.p_accessToken_get): return Matcher.ComparisonResult.match + case (.p_accessToken_set(let left),.p_accessToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_refreshToken_get,.p_refreshToken_get): return Matcher.ComparisonResult.match + case (.p_refreshToken_set(let left),.p_refreshToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_pushToken_get,.p_pushToken_get): return Matcher.ComparisonResult.match + case (.p_pushToken_set(let left),.p_pushToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignFullName_get,.p_appleSignFullName_get): return Matcher.ComparisonResult.match + case (.p_appleSignFullName_set(let left),.p_appleSignFullName_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignEmail_get,.p_appleSignEmail_get): return Matcher.ComparisonResult.match + case (.p_appleSignEmail_set(let left),.p_appleSignEmail_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_cookiesDate_get,.p_cookiesDate_get): return Matcher.ComparisonResult.match + case (.p_cookiesDate_set(let left),.p_cookiesDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_reviewLastShownVersion_get,.p_reviewLastShownVersion_get): return Matcher.ComparisonResult.match + case (.p_reviewLastShownVersion_set(let left),.p_reviewLastShownVersion_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_lastReviewDate_get,.p_lastReviewDate_get): return Matcher.ComparisonResult.match + case (.p_lastReviewDate_set(let left),.p_lastReviewDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_user_get,.p_user_get): return Matcher.ComparisonResult.match + case (.p_user_set(let left),.p_user_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_userSettings_get,.p_userSettings_get): return Matcher.ComparisonResult.match + case (.p_userSettings_set(let left),.p_userSettings_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_resetAppSupportDirectoryUserData_get,.p_resetAppSupportDirectoryUserData_get): return Matcher.ComparisonResult.match + case (.p_resetAppSupportDirectoryUserData_set(let left),.p_resetAppSupportDirectoryUserData_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_useRelativeDates_get,.p_useRelativeDates_get): return Matcher.ComparisonResult.match + case (.p_useRelativeDates_set(let left),.p_useRelativeDates_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .m_clear: return 0 + case .p_accessToken_get: return 0 + case .p_accessToken_set(let newValue): return newValue.intValue + case .p_refreshToken_get: return 0 + case .p_refreshToken_set(let newValue): return newValue.intValue + case .p_pushToken_get: return 0 + case .p_pushToken_set(let newValue): return newValue.intValue + case .p_appleSignFullName_get: return 0 + case .p_appleSignFullName_set(let newValue): return newValue.intValue + case .p_appleSignEmail_get: return 0 + case .p_appleSignEmail_set(let newValue): return newValue.intValue + case .p_cookiesDate_get: return 0 + case .p_cookiesDate_set(let newValue): return newValue.intValue + case .p_reviewLastShownVersion_get: return 0 + case .p_reviewLastShownVersion_set(let newValue): return newValue.intValue + case .p_lastReviewDate_get: return 0 + case .p_lastReviewDate_set(let newValue): return newValue.intValue + case .p_user_get: return 0 + case .p_user_set(let newValue): return newValue.intValue + case .p_userSettings_get: return 0 + case .p_userSettings_set(let newValue): return newValue.intValue + case .p_resetAppSupportDirectoryUserData_get: return 0 + case .p_resetAppSupportDirectoryUserData_set(let newValue): return newValue.intValue + case .p_useRelativeDates_get: return 0 + case .p_useRelativeDates_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .m_clear: return ".clear()" + case .p_accessToken_get: return "[get] .accessToken" + case .p_accessToken_set: return "[set] .accessToken" + case .p_refreshToken_get: return "[get] .refreshToken" + case .p_refreshToken_set: return "[set] .refreshToken" + case .p_pushToken_get: return "[get] .pushToken" + case .p_pushToken_set: return "[set] .pushToken" + case .p_appleSignFullName_get: return "[get] .appleSignFullName" + case .p_appleSignFullName_set: return "[set] .appleSignFullName" + case .p_appleSignEmail_get: return "[get] .appleSignEmail" + case .p_appleSignEmail_set: return "[set] .appleSignEmail" + case .p_cookiesDate_get: return "[get] .cookiesDate" + case .p_cookiesDate_set: return "[set] .cookiesDate" + case .p_reviewLastShownVersion_get: return "[get] .reviewLastShownVersion" + case .p_reviewLastShownVersion_set: return "[set] .reviewLastShownVersion" + case .p_lastReviewDate_get: return "[get] .lastReviewDate" + case .p_lastReviewDate_set: return "[set] .lastReviewDate" + case .p_user_get: return "[get] .user" + case .p_user_set: return "[set] .user" + case .p_userSettings_get: return "[get] .userSettings" + case .p_userSettings_set: return "[set] .userSettings" + case .p_resetAppSupportDirectoryUserData_get: return "[get] .resetAppSupportDirectoryUserData" + case .p_resetAppSupportDirectoryUserData_set: return "[set] .resetAppSupportDirectoryUserData" + case .p_useRelativeDates_get: return "[get] .useRelativeDates" + case .p_useRelativeDates_set: return "[set] .useRelativeDates" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func accessToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_accessToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func refreshToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_refreshToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func pushToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_pushToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignFullName(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignFullName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignEmail(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_cookiesDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func reviewLastShownVersion(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_reviewLastShownVersion_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func lastReviewDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_lastReviewDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func user(getter defaultValue: DataLayer.User?...) -> PropertyStub { + return Given(method: .p_user_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func userSettings(getter defaultValue: UserSettings?...) -> PropertyStub { + return Given(method: .p_userSettings_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func resetAppSupportDirectoryUserData(getter defaultValue: Bool?...) -> PropertyStub { + return Given(method: .p_resetAppSupportDirectoryUserData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func useRelativeDates(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_useRelativeDates_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func clear() -> Verify { return Verify(method: .m_clear)} + public static var accessToken: Verify { return Verify(method: .p_accessToken_get) } + public static func accessToken(set newValue: Parameter) -> Verify { return Verify(method: .p_accessToken_set(newValue)) } + public static var refreshToken: Verify { return Verify(method: .p_refreshToken_get) } + public static func refreshToken(set newValue: Parameter) -> Verify { return Verify(method: .p_refreshToken_set(newValue)) } + public static var pushToken: Verify { return Verify(method: .p_pushToken_get) } + public static func pushToken(set newValue: Parameter) -> Verify { return Verify(method: .p_pushToken_set(newValue)) } + public static var appleSignFullName: Verify { return Verify(method: .p_appleSignFullName_get) } + public static func appleSignFullName(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignFullName_set(newValue)) } + public static var appleSignEmail: Verify { return Verify(method: .p_appleSignEmail_get) } + public static func appleSignEmail(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignEmail_set(newValue)) } + public static var cookiesDate: Verify { return Verify(method: .p_cookiesDate_get) } + public static func cookiesDate(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesDate_set(newValue)) } + public static var reviewLastShownVersion: Verify { return Verify(method: .p_reviewLastShownVersion_get) } + public static func reviewLastShownVersion(set newValue: Parameter) -> Verify { return Verify(method: .p_reviewLastShownVersion_set(newValue)) } + public static var lastReviewDate: Verify { return Verify(method: .p_lastReviewDate_get) } + public static func lastReviewDate(set newValue: Parameter) -> Verify { return Verify(method: .p_lastReviewDate_set(newValue)) } + public static var user: Verify { return Verify(method: .p_user_get) } + public static func user(set newValue: Parameter) -> Verify { return Verify(method: .p_user_set(newValue)) } + public static var userSettings: Verify { return Verify(method: .p_userSettings_get) } + public static func userSettings(set newValue: Parameter) -> Verify { return Verify(method: .p_userSettings_set(newValue)) } + public static var resetAppSupportDirectoryUserData: Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_get) } + public static func resetAppSupportDirectoryUserData(set newValue: Parameter) -> Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_set(newValue)) } + public static var useRelativeDates: Verify { return Verify(method: .p_useRelativeDates_get) } + public static func useRelativeDates(set newValue: Parameter) -> Verify { return Verify(method: .p_useRelativeDates_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func clear(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_clear, performs: perform) } } @@ -1609,32 +3617,80 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { - open func getMyCourses(page: Int) throws -> [CourseItem] { - addInvocation(.m_getMyCourses__page_page(Parameter.value(`page`))) - let perform = methodPerformValue(.m_getMyCourses__page_page(Parameter.value(`page`))) as? (Int) -> Void + open func getEnrollments(page: Int) throws -> [CourseItem] { + addInvocation(.m_getEnrollments__page_page(Parameter.value(`page`))) + let perform = methodPerformValue(.m_getEnrollments__page_page(Parameter.value(`page`))) as? (Int) -> Void perform?(`page`) var __value: [CourseItem] do { - __value = try methodReturnValue(.m_getMyCourses__page_page(Parameter.value(`page`))).casted() + __value = try methodReturnValue(.m_getEnrollments__page_page(Parameter.value(`page`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getMyCourses(page: Int). Use given") - Failure("Stub return value not specified for getMyCourses(page: Int). Use given") + onFatalFailure("Stub return value not specified for getEnrollments(page: Int). Use given") + Failure("Stub return value not specified for getEnrollments(page: Int). Use given") } catch { throw error } return __value } - open func discoveryOffline() throws -> [CourseItem] { - addInvocation(.m_discoveryOffline) - let perform = methodPerformValue(.m_discoveryOffline) as? () -> Void + open func getEnrollmentsOffline() throws -> [CourseItem] { + addInvocation(.m_getEnrollmentsOffline) + let perform = methodPerformValue(.m_getEnrollmentsOffline) as? () -> Void perform?() var __value: [CourseItem] do { - __value = try methodReturnValue(.m_discoveryOffline).casted() + __value = try methodReturnValue(.m_getEnrollmentsOffline).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getEnrollmentsOffline(). Use given") + Failure("Stub return value not specified for getEnrollmentsOffline(). Use given") + } catch { + throw error + } + return __value + } + + open func getPrimaryEnrollment(pageSize: Int) throws -> PrimaryEnrollment { + addInvocation(.m_getPrimaryEnrollment__pageSize_pageSize(Parameter.value(`pageSize`))) + let perform = methodPerformValue(.m_getPrimaryEnrollment__pageSize_pageSize(Parameter.value(`pageSize`))) as? (Int) -> Void + perform?(`pageSize`) + var __value: PrimaryEnrollment + do { + __value = try methodReturnValue(.m_getPrimaryEnrollment__pageSize_pageSize(Parameter.value(`pageSize`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getPrimaryEnrollment(pageSize: Int). Use given") + Failure("Stub return value not specified for getPrimaryEnrollment(pageSize: Int). Use given") + } catch { + throw error + } + return __value + } + + open func getPrimaryEnrollmentOffline() throws -> PrimaryEnrollment { + addInvocation(.m_getPrimaryEnrollmentOffline) + let perform = methodPerformValue(.m_getPrimaryEnrollmentOffline) as? () -> Void + perform?() + var __value: PrimaryEnrollment + do { + __value = try methodReturnValue(.m_getPrimaryEnrollmentOffline).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getPrimaryEnrollmentOffline(). Use given") + Failure("Stub return value not specified for getPrimaryEnrollmentOffline(). Use given") + } catch { + throw error + } + return __value + } + + open func getAllCourses(filteredBy: String, page: Int) throws -> PrimaryEnrollment { + addInvocation(.m_getAllCourses__filteredBy_filteredBypage_page(Parameter.value(`filteredBy`), Parameter.value(`page`))) + let perform = methodPerformValue(.m_getAllCourses__filteredBy_filteredBypage_page(Parameter.value(`filteredBy`), Parameter.value(`page`))) as? (String, Int) -> Void + perform?(`filteredBy`, `page`) + var __value: PrimaryEnrollment + do { + __value = try methodReturnValue(.m_getAllCourses__filteredBy_filteredBypage_page(Parameter.value(`filteredBy`), Parameter.value(`page`))).casted() } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for discoveryOffline(). Use given") - Failure("Stub return value not specified for discoveryOffline(). Use given") + onFatalFailure("Stub return value not specified for getAllCourses(filteredBy: String, page: Int). Use given") + Failure("Stub return value not specified for getAllCourses(filteredBy: String, page: Int). Use given") } catch { throw error } @@ -1643,31 +3699,53 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { fileprivate enum MethodType { - case m_getMyCourses__page_page(Parameter) - case m_discoveryOffline + case m_getEnrollments__page_page(Parameter) + case m_getEnrollmentsOffline + case m_getPrimaryEnrollment__pageSize_pageSize(Parameter) + case m_getPrimaryEnrollmentOffline + case m_getAllCourses__filteredBy_filteredBypage_page(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { - case (.m_getMyCourses__page_page(let lhsPage), .m_getMyCourses__page_page(let rhsPage)): + case (.m_getEnrollments__page_page(let lhsPage), .m_getEnrollments__page_page(let rhsPage)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPage, rhs: rhsPage, with: matcher), lhsPage, rhsPage, "page")) return Matcher.ComparisonResult(results) - case (.m_discoveryOffline, .m_discoveryOffline): return .match + case (.m_getEnrollmentsOffline, .m_getEnrollmentsOffline): return .match + + case (.m_getPrimaryEnrollment__pageSize_pageSize(let lhsPagesize), .m_getPrimaryEnrollment__pageSize_pageSize(let rhsPagesize)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPagesize, rhs: rhsPagesize, with: matcher), lhsPagesize, rhsPagesize, "pageSize")) + return Matcher.ComparisonResult(results) + + case (.m_getPrimaryEnrollmentOffline, .m_getPrimaryEnrollmentOffline): return .match + + case (.m_getAllCourses__filteredBy_filteredBypage_page(let lhsFilteredby, let lhsPage), .m_getAllCourses__filteredBy_filteredBypage_page(let rhsFilteredby, let rhsPage)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFilteredby, rhs: rhsFilteredby, with: matcher), lhsFilteredby, rhsFilteredby, "filteredBy")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPage, rhs: rhsPage, with: matcher), lhsPage, rhsPage, "page")) + return Matcher.ComparisonResult(results) default: return .none } } func intValue() -> Int { switch self { - case let .m_getMyCourses__page_page(p0): return p0.intValue - case .m_discoveryOffline: return 0 + case let .m_getEnrollments__page_page(p0): return p0.intValue + case .m_getEnrollmentsOffline: return 0 + case let .m_getPrimaryEnrollment__pageSize_pageSize(p0): return p0.intValue + case .m_getPrimaryEnrollmentOffline: return 0 + case let .m_getAllCourses__filteredBy_filteredBypage_page(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { - case .m_getMyCourses__page_page: return ".getMyCourses(page:)" - case .m_discoveryOffline: return ".discoveryOffline()" + case .m_getEnrollments__page_page: return ".getEnrollments(page:)" + case .m_getEnrollmentsOffline: return ".getEnrollmentsOffline()" + case .m_getPrimaryEnrollment__pageSize_pageSize: return ".getPrimaryEnrollment(pageSize:)" + case .m_getPrimaryEnrollmentOffline: return ".getPrimaryEnrollmentOffline()" + case .m_getAllCourses__filteredBy_filteredBypage_page: return ".getAllCourses(filteredBy:page:)" } } } @@ -1681,50 +3759,101 @@ open class DashboardInteractorProtocolMock: DashboardInteractorProtocol, Mock { } - public static func getMyCourses(page: Parameter, willReturn: [CourseItem]...) -> MethodStub { - return Given(method: .m_getMyCourses__page_page(`page`), products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func getEnrollments(page: Parameter, willReturn: [CourseItem]...) -> MethodStub { + return Given(method: .m_getEnrollments__page_page(`page`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getEnrollmentsOffline(willReturn: [CourseItem]...) -> MethodStub { + return Given(method: .m_getEnrollmentsOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getPrimaryEnrollment(pageSize: Parameter, willReturn: PrimaryEnrollment...) -> MethodStub { + return Given(method: .m_getPrimaryEnrollment__pageSize_pageSize(`pageSize`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func discoveryOffline(willReturn: [CourseItem]...) -> MethodStub { - return Given(method: .m_discoveryOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) + public static func getPrimaryEnrollmentOffline(willReturn: PrimaryEnrollment...) -> MethodStub { + return Given(method: .m_getPrimaryEnrollmentOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getMyCourses(page: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_getMyCourses__page_page(`page`), products: willThrow.map({ StubProduct.throw($0) })) + public static func getAllCourses(filteredBy: Parameter, page: Parameter, willReturn: PrimaryEnrollment...) -> MethodStub { + return Given(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getMyCourses(page: Parameter, willProduce: (StubberThrows<[CourseItem]>) -> Void) -> MethodStub { + public static func getEnrollments(page: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getEnrollments__page_page(`page`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getEnrollments(page: Parameter, willProduce: (StubberThrows<[CourseItem]>) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getMyCourses__page_page(`page`), products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getEnrollments__page_page(`page`), products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: ([CourseItem]).self) willProduce(stubber) return given } - public static func discoveryOffline(willThrow: Error...) -> MethodStub { - return Given(method: .m_discoveryOffline, products: willThrow.map({ StubProduct.throw($0) })) + public static func getEnrollmentsOffline(willThrow: Error...) -> MethodStub { + return Given(method: .m_getEnrollmentsOffline, products: willThrow.map({ StubProduct.throw($0) })) } - public static func discoveryOffline(willProduce: (StubberThrows<[CourseItem]>) -> Void) -> MethodStub { + public static func getEnrollmentsOffline(willProduce: (StubberThrows<[CourseItem]>) -> Void) -> MethodStub { let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_discoveryOffline, products: willThrow.map({ StubProduct.throw($0) })) }() + let given: Given = { return Given(method: .m_getEnrollmentsOffline, products: willThrow.map({ StubProduct.throw($0) })) }() let stubber = given.stubThrows(for: ([CourseItem]).self) willProduce(stubber) return given } + public static func getPrimaryEnrollment(pageSize: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getPrimaryEnrollment__pageSize_pageSize(`pageSize`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getPrimaryEnrollment(pageSize: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getPrimaryEnrollment__pageSize_pageSize(`pageSize`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (PrimaryEnrollment).self) + willProduce(stubber) + return given + } + public static func getPrimaryEnrollmentOffline(willThrow: Error...) -> MethodStub { + return Given(method: .m_getPrimaryEnrollmentOffline, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getPrimaryEnrollmentOffline(willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getPrimaryEnrollmentOffline, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (PrimaryEnrollment).self) + willProduce(stubber) + return given + } + public static func getAllCourses(filteredBy: Parameter, page: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getAllCourses(filteredBy: Parameter, page: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (PrimaryEnrollment).self) + willProduce(stubber) + return given + } } public struct Verify { fileprivate var method: MethodType - public static func getMyCourses(page: Parameter) -> Verify { return Verify(method: .m_getMyCourses__page_page(`page`))} - public static func discoveryOffline() -> Verify { return Verify(method: .m_discoveryOffline)} + public static func getEnrollments(page: Parameter) -> Verify { return Verify(method: .m_getEnrollments__page_page(`page`))} + public static func getEnrollmentsOffline() -> Verify { return Verify(method: .m_getEnrollmentsOffline)} + public static func getPrimaryEnrollment(pageSize: Parameter) -> Verify { return Verify(method: .m_getPrimaryEnrollment__pageSize_pageSize(`pageSize`))} + public static func getPrimaryEnrollmentOffline() -> Verify { return Verify(method: .m_getPrimaryEnrollmentOffline)} + public static func getAllCourses(filteredBy: Parameter, page: Parameter) -> Verify { return Verify(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`))} } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func getMyCourses(page: Parameter, perform: @escaping (Int) -> Void) -> Perform { - return Perform(method: .m_getMyCourses__page_page(`page`), performs: perform) + public static func getEnrollments(page: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_getEnrollments__page_page(`page`), performs: perform) } - public static func discoveryOffline(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_discoveryOffline, performs: perform) + public static func getEnrollmentsOffline(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getEnrollmentsOffline, performs: perform) + } + public static func getPrimaryEnrollment(pageSize: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_getPrimaryEnrollment__pageSize_pageSize(`pageSize`), performs: perform) + } + public static func getPrimaryEnrollmentOffline(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getPrimaryEnrollmentOffline, performs: perform) + } + public static func getAllCourses(filteredBy: Parameter, page: Parameter, perform: @escaping (String, Int) -> Void) -> Perform { + return Perform(method: .m_getAllCourses__filteredBy_filteredBypage_page(`filteredBy`, `page`), performs: perform) } } @@ -1996,6 +4125,20 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func updateUnzippedFileSize(for sequentials: [CourseSequential]) -> [CourseSequential] { + addInvocation(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) + let perform = methodPerformValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) as? ([CourseSequential]) -> Void + perform?(`sequentials`) + var __value: [CourseSequential] + do { + __value = try methodReturnValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))).casted() + } catch { + onFatalFailure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + Failure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + } + return __value + } + open func resumeDownloading() throws { addInvocation(.m_resumeDownloading) let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void @@ -2023,6 +4166,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func removeAppSupportDirectoryUnusedContent() { + addInvocation(.m_removeAppSupportDirectoryUnusedContent) + let perform = methodPerformValue(.m_removeAppSupportDirectoryUnusedContent) as? () -> Void + perform?() + } + fileprivate enum MethodType { case m_publisher @@ -2037,8 +4186,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>) case m_resumeDownloading case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_removeAppSupportDirectoryUnusedContent case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2089,12 +4240,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + case (.m_updateUnzippedFileSize__for_sequentials(let lhsSequentials), .m_updateUnzippedFileSize__for_sequentials(let rhsSequentials)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSequentials, rhs: rhsSequentials, with: matcher), lhsSequentials, rhsSequentials, "for sequentials")) + return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) + + case (.m_removeAppSupportDirectoryUnusedContent, .m_removeAppSupportDirectoryUnusedContent): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -2114,8 +4272,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_updateUnzippedFileSize__for_sequentials(p0): return p0.intValue case .m_resumeDownloading: return 0 case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_removeAppSupportDirectoryUnusedContent: return 0 case .p_currentDownloadTask_get: return 0 } } @@ -2133,8 +4293,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_updateUnzippedFileSize__for_sequentials: return ".updateUnzippedFileSize(for:)" case .m_resumeDownloading: return ".resumeDownloading()" case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_removeAppSupportDirectoryUnusedContent: return ".removeAppSupportDirectoryUnusedContent()" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -2167,6 +4329,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willReturn: [CourseSequential]...) -> MethodStub { + return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -2205,6 +4370,13 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willProduce: (Stubber<[CourseSequential]>) -> Void) -> MethodStub { + let willReturn: [[CourseSequential]] = [] + let given: Given = { return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseSequential]).self) + willProduce(stubber) + return given + } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { let willReturn: [Bool] = [] let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -2289,8 +4461,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>) -> Verify { return Verify(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func removeAppSupportDirectoryUnusedContent() -> Verify { return Verify(method: .m_removeAppSupportDirectoryUnusedContent)} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -2334,12 +4508,217 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, perform: @escaping ([CourseSequential]) -> Void) -> Perform { + return Perform(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), performs: perform) + } public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } + public static func removeAppSupportDirectoryUnusedContent(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeAppSupportDirectoryUnusedContent, performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - OfflineSyncInteractorProtocol + +open class OfflineSyncInteractorProtocolMock: OfflineSyncInteractorProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func submitOfflineProgress(courseID: String, blockID: String, data: String) throws -> Bool { + addInvocation(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) + let perform = methodPerformValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) as? (String, String, String) -> Void + perform?(`courseID`, `blockID`, `data`) + var __value: Bool + do { + __value = try methodReturnValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + Failure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + } catch { + throw error + } + return __value + } + + + fileprivate enum MethodType { + case m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter, Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let lhsCourseid, let lhsBlockid, let lhsData), .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let rhsCourseid, let rhsBlockid, let rhsData)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsData, rhs: rhsData, with: matcher), lhsData, rhsData, "data")) + return Matcher.ComparisonResult(results) + } + } + + func intValue() -> Int { + switch self { + case let .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + } + } + func assertionName() -> String { + switch self { + case .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data: return ".submitOfflineProgress(courseID:blockID:data:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Bool).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter) -> Verify { return Verify(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { + return Perform(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), performs: perform) + } } public func given(_ method: Given) { diff --git a/Dashboard/DashboardTests/Presentation/AllCoursesViewModelTests.swift b/Dashboard/DashboardTests/Presentation/AllCoursesViewModelTests.swift new file mode 100644 index 000000000..abd7c7e6e --- /dev/null +++ b/Dashboard/DashboardTests/Presentation/AllCoursesViewModelTests.swift @@ -0,0 +1,222 @@ +// +// AllCoursesViewModelTests.swift +// Dashboard +// +// Created by Ivan Stepanok on 30.10.2024. +// + + +import SwiftyMocky +import XCTest +@testable import Core +@testable import Dashboard +import Combine +import SwiftUI + +final class AllCoursesViewModelTests: XCTestCase { + + var interactor: DashboardInteractorProtocolMock! + var connectivity: ConnectivityProtocolMock! + var analytics: DashboardAnalyticsMock! + var storage: CoreStorageMock! + + override func setUp() { + super.setUp() + interactor = DashboardInteractorProtocolMock() + connectivity = ConnectivityProtocolMock() + analytics = DashboardAnalyticsMock() + storage = CoreStorageMock() + } + + let mockEnrollment = PrimaryEnrollment( + primaryCourse: PrimaryCourse.init( + name: "Primary Course", + org: "OpenEdX", + courseID: "1", + hasAccess: true, + courseStart: Date(), + courseEnd: nil, + courseBanner: "https://example.com/banner.jpg", + futureAssignments: [], + pastAssignments: [], + progressEarned: 0, + progressPossible: 1, + lastVisitedBlockID: nil, + resumeTitle: nil + ), + courses: [ + CourseItem.init( + name: "Course", + org: "OpenEdX", + shortDescription: "short description", + imageURL: "https://examlpe.com/image.jpg", + hasAccess: true, + courseStart: nil, + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil, + courseID: "2", + numPages: 1, + coursesCount: 3, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 2 + ), + CourseItem.init( + name: "Course", + org: "OpenEdX", + shortDescription: "short description", + imageURL: "https://examlpe.com/image.jpg", + hasAccess: true, + courseStart: nil, + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil, + courseID: "3", + numPages: 1, + coursesCount: 3, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 2 + ), + CourseItem.init( + name: "Course", + org: "OpenEdX", + shortDescription: "short description", + imageURL: "https://examlpe.com/image.jpg", + hasAccess: true, + courseStart: nil, + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil, + courseID: "4", + numPages: 1, + coursesCount: 3, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 2 + ) + ], + totalPages: 2, + count: 1 + ) + + func testGetCoursesSuccess() async throws { + // Given + let viewModel = AllCoursesViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: storage + ) + + Given(interactor, .getAllCourses(filteredBy: .any, page: .any, willReturn: mockEnrollment)) + + // When + await viewModel.getCourses(page: 1) + + // Then + Verify(interactor, 1, .getAllCourses(filteredBy: .any, page: .value(1))) + XCTAssertEqual(viewModel.myEnrollments?.courses.count, 3) + XCTAssertEqual(viewModel.nextPage, 2) + XCTAssertEqual(viewModel.totalPages, 2) + XCTAssertFalse(viewModel.fetchInProgress) + XCTAssertFalse(viewModel.showError) + } + + func testGetCoursesWithPagination() async throws { + // Given + let viewModel = AllCoursesViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: storage + ) + + Given(interactor, .getAllCourses(filteredBy: .any, page: .any, willReturn: mockEnrollment)) + + // When + await viewModel.getCourses(page: 1) + await viewModel.getCourses(page: 2) + + // Then + Verify(interactor, 2, .getAllCourses(filteredBy: .any, page: .any)) + XCTAssertEqual(viewModel.nextPage, 3) + } + + func testGetCoursesNoCachedDataError() async throws { + // Given + let viewModel = AllCoursesViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: storage + ) + + Given(interactor, .getAllCourses(filteredBy: .any, page: .any, willThrow: NoCachedDataError())) + + // When + await viewModel.getCourses(page: 1) + + // Then + XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.noCachedData) + XCTAssertTrue(viewModel.showError) + XCTAssertFalse(viewModel.fetchInProgress) + } + + func testGetCoursesUnknownError() async throws { + // Given + let viewModel = AllCoursesViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: storage + ) + + Given(interactor, .getAllCourses(filteredBy: .any, page: .any, willThrow: NSError())) + + // When + await viewModel.getCourses(page: 1) + + // Then + XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) + XCTAssertTrue(viewModel.showError) + XCTAssertFalse(viewModel.fetchInProgress) + } + + func testGetMyCoursesPagination() async { + // Given + let viewModel = AllCoursesViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: storage + ) + + Given(interactor, .getAllCourses(filteredBy: .any, page: .any, willReturn: mockEnrollment)) + + // When + await viewModel.getCourses(page: 1) + await viewModel.getMyCoursesPagination(index: 0) + await viewModel.getMyCoursesPagination(index: mockEnrollment.courses.count - 3) + + // Then + Verify(interactor, 2, .getAllCourses(filteredBy: .any, page: .any)) + } + + func testTrackDashboardCourseClicked() { + // Given + let viewModel = AllCoursesViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: storage + ) + + // When + viewModel.trackDashboardCourseClicked(courseID: "test-id", courseName: "Test Course") + + // Then + Verify(analytics, 1, .dashboardCourseClicked(courseID: .value("test-id"), courseName: .value("Test Course"))) + } +} diff --git a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift index d3261b52d..e053c19c7 100644 --- a/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift +++ b/Dashboard/DashboardTests/Presentation/DashboardViewModelTests.swift @@ -1,5 +1,5 @@ // -// DashboardViewModelTests.swift +// ListDashboardViewModelTests.swift // DashboardTests // // Created by  Stepanok Ivan on 18.01.2023. @@ -12,47 +12,58 @@ import XCTest import Alamofire import SwiftUI -final class DashboardViewModelTests: XCTestCase { +final class ListDashboardViewModelTests: XCTestCase { func testGetMyCoursesSuccess() async throws { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock() + ) let items = [ CourseItem(name: "Test", org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 2), + coursesCount: 2, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 2) + coursesCount: 2, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0) ] Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .getMyCourses(page: .any, willReturn: items)) + Given(interactor, .getEnrollments(page: .any, willReturn: items)) await viewModel.getMyCourses(page: 1) - Verify(interactor, 1, .getMyCourses(page: .value(1))) + Verify(interactor, 1, .getEnrollments(page: .value(1))) XCTAssertTrue(viewModel.courses == items) XCTAssertNil(viewModel.errorMessage) @@ -63,41 +74,52 @@ final class DashboardViewModelTests: XCTestCase { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock() + ) let items = [ CourseItem(name: "Test", org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 2), + coursesCount: 2, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 2) + coursesCount: 2, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0) ] Given(connectivity, .isInternetAvaliable(getter: false)) - Given(interactor, .discoveryOffline(willReturn: items)) + Given(interactor, .getEnrollmentsOffline(willReturn: items)) await viewModel.getMyCourses(page: 1) - Verify(interactor, 1, .discoveryOffline()) + Verify(interactor, 1, .getEnrollmentsOffline()) XCTAssertTrue(viewModel.courses == items) XCTAssertNil(viewModel.errorMessage) @@ -108,14 +130,19 @@ final class DashboardViewModelTests: XCTestCase { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock() + ) Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .getMyCourses(page: .any, willThrow: NoCachedDataError()) ) + Given(interactor, .getEnrollments(page: .any, willThrow: NoCachedDataError()) ) await viewModel.getMyCourses(page: 1) - Verify(interactor, 1, .getMyCourses(page: .value(1))) + Verify(interactor, 1, .getEnrollments(page: .value(1))) XCTAssertTrue(viewModel.courses.isEmpty) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.noCachedData) @@ -126,14 +153,19 @@ final class DashboardViewModelTests: XCTestCase { let interactor = DashboardInteractorProtocolMock() let connectivity = ConnectivityProtocolMock() let analytics = DashboardAnalyticsMock() - let viewModel = DashboardViewModel(interactor: interactor, connectivity: connectivity, analytics: analytics) + let viewModel = ListDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + storage: CoreStorageMock() + ) Given(connectivity, .isInternetAvaliable(getter: true)) - Given(interactor, .getMyCourses(page: .any, willThrow: NSError()) ) + Given(interactor, .getEnrollments(page: .any, willThrow: NSError()) ) await viewModel.getMyCourses(page: 1) - Verify(interactor, 1, .getMyCourses(page: .value(1))) + Verify(interactor, 1, .getEnrollments(page: .value(1))) XCTAssertTrue(viewModel.courses.isEmpty) XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) diff --git a/Dashboard/DashboardTests/Presentation/PrimaryCourseDashboardViewModelTests.swift b/Dashboard/DashboardTests/Presentation/PrimaryCourseDashboardViewModelTests.swift new file mode 100644 index 000000000..0f6aff8c5 --- /dev/null +++ b/Dashboard/DashboardTests/Presentation/PrimaryCourseDashboardViewModelTests.swift @@ -0,0 +1,214 @@ +// +// PrimaryCourseDashboardViewModelTests.swift +// Dashboard +// +// Created by Ivan Stepanok on 30.10.2024. +// + + +import SwiftyMocky +import XCTest +@testable import Core +@testable import Dashboard +import Combine +import SwiftUI + +final class PrimaryCourseDashboardViewModelTests: XCTestCase { + + var interactor: DashboardInteractorProtocolMock! + var connectivity: ConnectivityProtocolMock! + var analytics: DashboardAnalyticsMock! + var storage: CoreStorageMock! + var config: ConfigProtocolMock! + + override func setUp() { + super.setUp() + interactor = DashboardInteractorProtocolMock() + connectivity = ConnectivityProtocolMock() + analytics = DashboardAnalyticsMock() + storage = CoreStorageMock() + config = ConfigProtocolMock() + interactor = DashboardInteractorProtocolMock() + } + + let enrollment = PrimaryEnrollment( + primaryCourse: PrimaryCourse.init( + name: "Primary Course", + org: "OpenEdX", + courseID: "1", + hasAccess: true, + courseStart: Date(), + courseEnd: nil, + courseBanner: "https://example.com/banner.jpg", + futureAssignments: [], + pastAssignments: [], + progressEarned: 0, + progressPossible: 1, + lastVisitedBlockID: nil, + resumeTitle: nil + ), + courses: [ + CourseItem.init( + name: "Course", + org: "OpenEdX", + shortDescription: "short description", + imageURL: "https://examlpe.com/image.jpg", + hasAccess: true, + courseStart: nil, + courseEnd: nil, + enrollmentStart: nil, + enrollmentEnd: nil, + courseID: "2", + numPages: 1, + coursesCount: 3, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 2 + ) + ], + totalPages: 1, + count: 1 + ) + + func testGetEnrollmentsSuccess() async throws { + // Given + let viewModel = PrimaryCourseDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + config: config, + storage: storage + ) + + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(interactor, .getPrimaryEnrollment(pageSize: .any, willReturn: enrollment)) + + // When + await viewModel.getEnrollments() + + // Then + Verify(interactor, 1, .getPrimaryEnrollment(pageSize: .value(UIDevice.current.userInterfaceIdiom == .pad ? 7 : 5))) + XCTAssertEqual(viewModel.enrollments, enrollment) + XCTAssertNil(viewModel.errorMessage) + XCTAssertFalse(viewModel.showError) + XCTAssertFalse(viewModel.fetchInProgress) + } + + func testGetEnrollmentsOfflineSuccess() async throws { + // Given + let viewModel = PrimaryCourseDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + config: config, + storage: storage + ) + + Given(connectivity, .isInternetAvaliable(getter: false)) + Given(interactor, .getPrimaryEnrollmentOffline(willReturn: enrollment)) + + // When + await viewModel.getEnrollments() + + // Then + Verify(interactor, 1, .getPrimaryEnrollmentOffline()) + XCTAssertEqual(viewModel.enrollments, enrollment) + XCTAssertNil(viewModel.errorMessage) + XCTAssertFalse(viewModel.showError) + XCTAssertFalse(viewModel.fetchInProgress) + } + + func testGetEnrollmentsNoCacheError() async throws { + // Given + let viewModel = PrimaryCourseDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + config: config, + storage: storage + ) + + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(interactor, .getPrimaryEnrollment(pageSize: .any, willThrow: NoCachedDataError())) + + // When + await viewModel.getEnrollments() + + // Then + Verify(interactor, 1, .getPrimaryEnrollment(pageSize: .value(UIDevice.current.userInterfaceIdiom == .pad ? 7 : 5))) + XCTAssertNil(viewModel.enrollments) + XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.noCachedData) + XCTAssertTrue(viewModel.showError) + XCTAssertFalse(viewModel.fetchInProgress) + } + + func testGetEnrollmentsUnknownError() async throws { + // Given + let viewModel = PrimaryCourseDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + config: config, + storage: storage + ) + + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(interactor, .getPrimaryEnrollment(pageSize: .any, willThrow: NSError())) + + // When + await viewModel.getEnrollments() + + // Then + Verify(interactor, 1, .getPrimaryEnrollment(pageSize: .value(UIDevice.current.userInterfaceIdiom == .pad ? 7 : 5))) + XCTAssertNil(viewModel.enrollments) + XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) + XCTAssertTrue(viewModel.showError) + XCTAssertFalse(viewModel.fetchInProgress) + } + + func testTrackDashboardCourseClicked() { + // Given + let viewModel = PrimaryCourseDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + config: config, + storage: storage + ) + + let courseID = "test-course-id" + let courseName = "Test Course" + + // When + viewModel.trackDashboardCourseClicked(courseID: courseID, courseName: courseName) + + // Then + Verify(analytics, 1, .dashboardCourseClicked(courseID: .value(courseID), courseName: .value(courseName))) + } + + func testNotificationCenterSubscriptions() async { + // Given + let viewModel = PrimaryCourseDashboardViewModel( + interactor: interactor, + connectivity: connectivity, + analytics: analytics, + config: config, + storage: storage + ) + + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(interactor, .getPrimaryEnrollment(pageSize: .any, willReturn: enrollment)) + + // When + NotificationCenter.default.post(name: .onCourseEnrolled, object: nil) + NotificationCenter.default.post(name: .onblockCompletionRequested, object: nil) + NotificationCenter.default.post(name: .refreshEnrollments, object: nil) + + // Wait a bit for async operations to complete + try? await Task.sleep(nanoseconds: 100_000_000) + + // Then + // Verify that getEnrollments was called multiple times due to notifications + Verify(interactor, .getPrimaryEnrollment(pageSize: .any)) + } +} diff --git a/Dashboard/Mockfile b/Dashboard/Mockfile index f747b41e0..276791466 100644 --- a/Dashboard/Mockfile +++ b/Dashboard/Mockfile @@ -14,4 +14,5 @@ unit.tests.mock: - Dashboard - Foundation - SwiftUI - - Combine \ No newline at end of file + - Combine + - OEXFoundation \ No newline at end of file diff --git a/Discovery/Discovery.xcodeproj/project.pbxproj b/Discovery/Discovery.xcodeproj/project.pbxproj index 769376097..8c3d40080 100644 --- a/Discovery/Discovery.xcodeproj/project.pbxproj +++ b/Discovery/Discovery.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 1402A0CA2B61012F00A0A00B /* ProgramWebviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1402A0C82B61012F00A0A00B /* ProgramWebviewViewModel.swift */; }; 63C6E9CBBF5E33B8B9B4DFEC /* Pods_App_Discovery_DiscoveryUnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 780FC373E1D479E58870BD85 /* Pods_App_Discovery_DiscoveryUnitTests.framework */; }; 9F47BCC672941A9854404EC7 /* Pods_App_Discovery.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 919E55130969D91EF03C4C0B /* Pods_App_Discovery.framework */; }; + CE7CAF312CC155FD00E0AC9D /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CE7CAF302CC155FD00E0AC9D /* OEXFoundation */; }; + CEB1E26A2CC14E7900921517 /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CEB1E2692CC14E7900921517 /* OEXFoundation */; }; CFC8494C299A66080055E497 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CFC8494E299A66080055E497 /* Localizable.stringsdict */; }; CFC84950299BE52C0055E497 /* SearchViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC8494F299BE52C0055E497 /* SearchViewModelTests.swift */; }; E0B9F69C2B4D57F800168366 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0B9F6962B4D57F800168366 /* SearchView.swift */; }; @@ -55,6 +57,19 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + CE7CAF332CC155FE00E0AC9D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 022D04872976D7E100E0059B /* DiscoveryUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DiscoveryUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 022D04892976D7E100E0059B /* DiscoveryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryViewModelTests.swift; sourceTree = ""; }; @@ -67,7 +82,6 @@ 029242EA2AE6AB7B00A940EC /* UpdateNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateNotificationView.swift; sourceTree = ""; }; 0297373F2949FB070051696B /* DiscoveryCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DiscoveryCoreModel.xcdatamodel; sourceTree = ""; }; 029737412949FB3B0051696B /* DiscoveryPersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryPersistenceProtocol.swift; sourceTree = ""; }; - 02ED50C729A649C9008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02ED50C829A649C9008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 02EF39D028D867690058F6BD /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; 02EF39D828D86A380058F6BD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -122,6 +136,7 @@ buildActionMask = 2147483647; files = ( 022D048B2976D7E100E0059B /* Discovery.framework in Frameworks */, + CE7CAF312CC155FD00E0AC9D /* OEXFoundation in Frameworks */, 63C6E9CBBF5E33B8B9B4DFEC /* Pods_App_Discovery_DiscoveryUnitTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -131,6 +146,7 @@ buildActionMask = 2147483647; files = ( 072787AD28D34D15002E9142 /* Core.framework in Frameworks */, + CEB1E26A2CC14E7900921517 /* OEXFoundation in Frameworks */, 9F47BCC672941A9854404EC7 /* Pods_App_Discovery.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -359,6 +375,7 @@ 022D04842976D7E100E0059B /* Frameworks */, 022D04852976D7E100E0059B /* Resources */, 7A53F60C849FA0F910D22A82 /* [CP] Copy Pods Resources */, + CE7CAF332CC155FE00E0AC9D /* Embed Frameworks */, ); buildRules = ( ); @@ -419,6 +436,9 @@ uk, ); mainGroup = 0727878F28D34C03002E9142; + packageReferences = ( + CEB1E2682CC14E7900921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */, + ); productRefGroup = 0727879A28D34C03002E9142 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -591,7 +611,6 @@ isa = PBXVariantGroup; children = ( 02EF39D828D86A380058F6BD /* en */, - 02ED50C729A649C9008341CD /* uk */, ); name = Localizable.strings; sourceTree = ""; @@ -616,7 +635,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -637,7 +656,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -658,7 +677,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -679,7 +698,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -700,7 +719,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -721,7 +740,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -806,7 +825,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -814,7 +833,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -842,7 +861,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -921,7 +940,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -929,7 +948,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -956,7 +975,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscoveryUnitTests; @@ -1099,7 +1118,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1107,7 +1126,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1135,7 +1154,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1143,7 +1162,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1234,7 +1253,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1242,7 +1261,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1334,7 +1353,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1342,7 +1361,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1428,7 +1447,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1436,7 +1455,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1521,7 +1540,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1529,7 +1548,7 @@ INFOPLIST_FILE = Discovery/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1598,6 +1617,30 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + CEB1E2682CC14E7900921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; + requirement = { + kind = exactVersion; + version = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + CE7CAF302CC155FD00E0AC9D /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E2682CC14E7900921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; + CEB1E2692CC14E7900921517 /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E2682CC14E7900921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; +/* End XCSwiftPackageProductDependency section */ + /* Begin XCVersionGroup section */ 0297373E2949FB070051696B /* DiscoveryCoreModel.xcdatamodeld */ = { isa = XCVersionGroup; diff --git a/Discovery/Discovery/Data/DiscoveryRepository.swift b/Discovery/Discovery/Data/DiscoveryRepository.swift index 3b20f84a3..d08571071 100644 --- a/Discovery/Discovery/Data/DiscoveryRepository.swift +++ b/Discovery/Discovery/Data/DiscoveryRepository.swift @@ -7,13 +7,14 @@ import Foundation import Core +import OEXFoundation import CoreData import Alamofire public protocol DiscoveryRepositoryProtocol { func getDiscovery(page: Int) async throws -> [CourseItem] func searchCourses(page: Int, searchTerm: String) async throws -> [CourseItem] - func getDiscoveryOffline() throws -> [CourseItem] + func getDiscoveryOffline() async throws -> [CourseItem] func getCourseDetails(courseID: String) async throws -> CourseDetails func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails func enrollToCourse(courseID: String) async throws -> Bool @@ -44,8 +45,8 @@ public class DiscoveryRepository: DiscoveryRepositoryProtocol { return discoveryResponse } - public func getDiscoveryOffline() throws -> [CourseItem] { - return try persistence.loadDiscovery() + public func getDiscoveryOffline() async throws -> [CourseItem] { + try await persistence.loadDiscovery() } public func searchCourses(page: Int, searchTerm: String) async throws -> [CourseItem] { @@ -68,7 +69,7 @@ public class DiscoveryRepository: DiscoveryRepositoryProtocol { } public func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails { - return try persistence.loadCourseDetails(courseID: courseID) + try await persistence.loadCourseDetails(courseID: courseID) } public func enrollToCourse(courseID: String) async throws -> Bool { @@ -94,7 +95,8 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { isEnrolled: false, overviewHTML: "Course description

Lorem ipsum", courseBannerURL: "courseBannerURL", - courseVideoURL: nil + courseVideoURL: nil, + courseRawImage: nil ) } @@ -111,7 +113,8 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { isEnrolled: false, overviewHTML: "Course description

Lorem ipsum", courseBannerURL: "courseBannerURL", - courseVideoURL: nil + courseVideoURL: nil, + courseRawImage: nil ) } @@ -128,13 +131,16 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: true, + hasAccess: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, courseID: "course_id_\(i)", - numPages: 1, coursesCount: 10 + numPages: 1, coursesCount: 10, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0 ) ) } @@ -150,13 +156,16 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: nil, + hasAccess: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, courseID: "course_id_\(i)", - numPages: 1, coursesCount: 10 + numPages: 1, coursesCount: 10, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0 ) ) } @@ -172,14 +181,17 @@ class DiscoveryRepositoryMock: DiscoveryRepositoryProtocol { org: "Organization", shortDescription: "shortDescription", imageURL: "", - isActive: true, + hasAccess: true, courseStart: nil, courseEnd: nil, enrollmentStart: nil, enrollmentEnd: nil, courseID: "course_id_\(i)", numPages: 1, - coursesCount: 10 + coursesCount: 10, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0 ) ) } diff --git a/Discovery/Discovery/Data/Model/CourseDetails.swift b/Discovery/Discovery/Data/Model/CourseDetails.swift index 6769aff53..fb67340aa 100644 --- a/Discovery/Discovery/Data/Model/CourseDetails.swift +++ b/Discovery/Discovery/Data/Model/CourseDetails.swift @@ -20,6 +20,7 @@ public struct CourseDetails { public var overviewHTML: String public let courseBannerURL: String public let courseVideoURL: String? + public let courseRawImage: String? public init(courseID: String, org: String, @@ -32,7 +33,9 @@ public struct CourseDetails { isEnrolled: Bool, overviewHTML: String, courseBannerURL: String, - courseVideoURL: String?) { + courseVideoURL: String?, + courseRawImage: String? + ) { self.courseID = courseID self.org = org self.courseTitle = courseTitle @@ -45,5 +48,6 @@ public struct CourseDetails { self.overviewHTML = overviewHTML self.courseBannerURL = courseBannerURL self.courseVideoURL = courseVideoURL + self.courseRawImage = courseRawImage } } diff --git a/Discovery/Discovery/Data/Model/Data_CourseDetailsResponse.swift b/Discovery/Discovery/Data/Model/Data_CourseDetailsResponse.swift index 1047727e8..9b9e2522b 100644 --- a/Discovery/Discovery/Data/Model/Data_CourseDetailsResponse.swift +++ b/Discovery/Discovery/Data/Model/Data_CourseDetailsResponse.swift @@ -75,6 +75,8 @@ public extension DataLayer.CourseDetailsResponse { isEnrolled: isEnrolled, overviewHTML: overview, courseBannerURL: imageURL, - courseVideoURL: media.courseVideo?.url) + courseVideoURL: media.courseVideo?.url, + courseRawImage: media.image?.raw + ) } } diff --git a/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift b/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift index 957f03112..2d111b847 100644 --- a/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift +++ b/Discovery/Discovery/Data/Network/DiscoveryEndpoint.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import Alamofire enum DiscoveryEndpoint: EndPointType { diff --git a/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents b/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents index 154df9ca8..f508a975a 100644 --- a/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents +++ b/Discovery/Discovery/Data/Persistence/DiscoveryCoreModel.xcdatamodeld/DiscoveryCoreModel.xcdatamodel/contents @@ -1,10 +1,11 @@ - + + @@ -23,12 +24,13 @@ + + - diff --git a/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift b/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift index 1c8b3fd6c..0445a690c 100644 --- a/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift +++ b/Discovery/Discovery/Data/Persistence/DiscoveryPersistenceProtocol.swift @@ -9,9 +9,9 @@ import CoreData import Core public protocol DiscoveryPersistenceProtocol { - func loadDiscovery() throws -> [CourseItem] + func loadDiscovery() async throws -> [CourseItem] func saveDiscovery(items: [CourseItem]) - func loadCourseDetails(courseID: String) throws -> CourseDetails + func loadCourseDetails(courseID: String) async throws -> CourseDetails func saveCourseDetails(course: CourseDetails) } diff --git a/Discovery/Discovery/Domain/DiscoveryInteractor.swift b/Discovery/Discovery/Domain/DiscoveryInteractor.swift index a0bffe3ca..403463dc5 100644 --- a/Discovery/Discovery/Domain/DiscoveryInteractor.swift +++ b/Discovery/Discovery/Domain/DiscoveryInteractor.swift @@ -11,7 +11,7 @@ import Core //sourcery: AutoMockable public protocol DiscoveryInteractorProtocol { func discovery(page: Int) async throws -> [CourseItem] - func discoveryOffline() throws -> [CourseItem] + func discoveryOffline() async throws -> [CourseItem] func search(page: Int, searchTerm: String) async throws -> [CourseItem] func getLoadedCourseDetails(courseID: String) async throws -> CourseDetails func getCourseDetails(courseID: String) async throws -> CourseDetails @@ -34,8 +34,8 @@ public class DiscoveryInteractor: DiscoveryInteractorProtocol { return try await repository.searchCourses(page: page, searchTerm: searchTerm) } - public func discoveryOffline() throws -> [CourseItem] { - return try repository.getDiscoveryOffline() + public func discoveryOffline() async throws -> [CourseItem] { + try await repository.getDiscoveryOffline() } public func getCourseDetails(courseID: String) async throws -> CourseDetails { diff --git a/Discovery/Discovery/Info.plist b/Discovery/Discovery/Info.plist index f72a0f657..0c67376eb 100644 --- a/Discovery/Discovery/Info.plist +++ b/Discovery/Discovery/Info.plist @@ -1,12 +1,5 @@ - diff --git a/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift b/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift index bfc9e7075..d5c722bd3 100644 --- a/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift +++ b/Discovery/Discovery/Presentation/DiscoveryAnalytics.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation //sourcery: AutoMockable public protocol DiscoveryAnalytics { @@ -18,7 +19,7 @@ public protocol DiscoveryAnalytics { func courseEnrollSuccess(courseId: String, courseName: String) func externalLinkOpen(url: String, screen: String) func externalLinkOpenAction(url: String, screen: String, action: String) - func discoveryEvent(event: AnalyticsEvent, biValue: EventBIValue) + func discoveryScreenEvent(event: AnalyticsEvent, biValue: EventBIValue) } #if DEBUG @@ -31,6 +32,6 @@ class DiscoveryAnalyticsMock: DiscoveryAnalytics { public func courseEnrollSuccess(courseId: String, courseName: String) {} public func externalLinkOpen(url: String, screen: String) {} public func externalLinkOpenAction(url: String, screen: String, action: String) {} - public func discoveryEvent(event: AnalyticsEvent, biValue: EventBIValue) {} + public func discoveryScreenEvent(event: AnalyticsEvent, biValue: EventBIValue) {} } #endif diff --git a/Discovery/Discovery/Presentation/DiscoveryRouter.swift b/Discovery/Discovery/Presentation/DiscoveryRouter.swift index cca463e95..6c9651c78 100644 --- a/Discovery/Discovery/Presentation/DiscoveryRouter.swift +++ b/Discovery/Discovery/Presentation/DiscoveryRouter.swift @@ -20,12 +20,15 @@ public protocol DiscoveryRouter: BaseRouter { func showDiscoverySearch(searchQuery: String?) func showCourseScreens( courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String + title: String, + courseRawImage: String?, + showDates: Bool, + lastVisitedBlockID: String? ) func showWebProgramDetails( @@ -51,12 +54,15 @@ public class DiscoveryRouterMock: BaseRouterMock, DiscoveryRouter { public func showDiscoverySearch(searchQuery: String? = nil) {} public func showCourseScreens( courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String + title: String, + courseRawImage: String?, + showDates: Bool, + lastVisitedBlockID: String? ) {} public func showWebProgramDetails( diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift index 427cd4ade..092242d43 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/CourseDetailsView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Kingfisher import WebKit import Theme @@ -47,12 +48,10 @@ public struct CourseDetailsView: View { ProgressBar(size: 40, lineWidth: 8) .padding(.top, 200) .padding(.horizontal) - .accessibilityIdentifier("progressbar") + .accessibilityIdentifier("progress_bar") }.frame(width: proxy.size.width) } else { - RefreshableScrollViewCompat(action: { - await viewModel.getCourseDetail(courseID: courseID, withProgress: false) - }) { + ScrollView { VStack(alignment: .leading) { if let courseDetails = viewModel.courseDetails { @@ -132,13 +131,18 @@ public struct CourseDetailsView: View { ProgressBar(size: 40, lineWidth: 8) .padding(.top, 20) .frame(maxWidth: .infinity) - .accessibilityIdentifier("progressbar") + .accessibilityIdentifier("progress_bar") } } } } .frameLimit(width: proxy.size.width) } + .refreshable { + Task { + await viewModel.getCourseDetail(courseID: courseID, withProgress: false) + } + } .onRightSwipeGesture { viewModel.router.back() } @@ -162,6 +166,13 @@ public struct CourseDetailsView: View { viewModel.courseDetails?.courseTitle ?? "" ) ) + case .signInWithSSO: + viewModel.router.showLoginScreen( + sourceScreen: .courseDetail( + courseID, + viewModel.courseDetails?.courseTitle ?? "" + ) + ) } } } @@ -275,12 +286,15 @@ private struct CourseStateView: View { ) viewModel.router.showCourseScreens( courseID: courseDetails.courseID, - isActive: nil, + hasAccess: nil, courseStart: courseDetails.courseStart, courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: title + title: title, + courseRawImage: courseDetails.courseRawImage, + showDates: false, + lastVisitedBlockID: nil ) } }) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift index b8d9aa860..b92770c28 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Theme public struct DiscoveryView: View { @@ -19,7 +20,7 @@ public struct DiscoveryView: View { private var sourceScreen: LogistrationSourceScreen - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal @Environment(\.presentationMode) private var presentationMode private let discoveryNew: some View = VStack(alignment: .leading) { @@ -92,13 +93,7 @@ public struct DiscoveryView: View { .accessibilityLabel(DiscoveryLocalization.search) ZStack { - RefreshableScrollViewCompat(action: { - viewModel.totalPages = 1 - viewModel.nextPage = 1 - Task { - await viewModel.discovery(page: 1, withProgress: false) - } - }) { + ScrollView { LazyVStack(spacing: 0) { HStack { discoveryNew @@ -106,12 +101,14 @@ public struct DiscoveryView: View { .padding(.bottom, 20) Spacer() }.padding(.leading, 10) + let useRelativeDates = viewModel.storage.useRelativeDates ForEach(Array(viewModel.courses.enumerated()), id: \.offset) { index, course in CourseCellView( model: course, type: .discovery, index: index, - cellsCount: viewModel.courses.count + cellsCount: viewModel.courses.count, + useRelativeDates: useRelativeDates ).padding(.horizontal, 24) .onAppear { Task { @@ -141,60 +138,68 @@ public struct DiscoveryView: View { VStack {}.frame(height: 40) } .frameLimit(width: proxy.size.width) - } - }.accessibilityAction {} - - if !viewModel.userloggedIn { - LogistrationBottomView { buttonAction in - switch buttonAction { - case .signIn: - viewModel.router.showLoginScreen(sourceScreen: .discovery) - case .register: - viewModel.router.showRegisterScreen(sourceScreen: .discovery) + }.refreshable { + viewModel.totalPages = 1 + viewModel.nextPage = 1 + Task { + await viewModel.discovery(page: 1, withProgress: false) } } } - }.padding(.top, 8) - - // MARK: - Offline mode SnackBar - OfflineSnackBarView( - connectivity: viewModel.connectivity, - reloadAction: { - await viewModel.discovery(page: 1, withProgress: false) - }) - - // MARK: - Error Alert - if viewModel.showError { - VStack { - Spacer() - SnackBarView(message: viewModel.errorMessage) - } - .padding(.bottom, viewModel.connectivity.isInternetAvaliable - ? 0 : OfflineSnackBarView.height) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil + }.accessibilityAction {} + + if !viewModel.userloggedIn { + LogistrationBottomView { buttonAction in + switch buttonAction { + case .signIn: + viewModel.router.showLoginScreen(sourceScreen: .discovery) + case .register: + viewModel.router.showRegisterScreen(sourceScreen: .discovery) + case .signInWithSSO: + viewModel.router.showLoginScreen(sourceScreen: .discovery) } } } - } - .navigationBarHidden(sourceScreen != .startup) - .onFirstAppear { - if !(searchQuery.isEmpty) { - router.showDiscoverySearch(searchQuery: searchQuery) - searchQuery = "" + }.padding(.top, 8) + + // MARK: - Offline mode SnackBar + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.discovery(page: 1, withProgress: false) + }) + + // MARK: - Error Alert + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) } - Task { - await viewModel.discovery(page: 1) - if case let .courseDetail(courseID, courseTitle) = sourceScreen { - viewModel.router.showCourseDetais(courseID: courseID, title: courseTitle) + .padding(.bottom, viewModel.connectivity.isInternetAvaliable + ? 0 : OfflineSnackBarView.height) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil } } - viewModel.setupNotifications() } - .background(Theme.Colors.background.ignoresSafeArea()) } + .navigationBarHidden(sourceScreen != .startup) + .onFirstAppear { + if !(searchQuery.isEmpty) { + router.showDiscoverySearch(searchQuery: searchQuery) + searchQuery = "" + } + Task { + await viewModel.discovery(page: 1) + if case let .courseDetail(courseID, courseTitle) = sourceScreen { + viewModel.router.showCourseDetais(courseID: courseID, title: courseTitle) + } + } + viewModel.setupNotifications() + } + .background(Theme.Colors.background.ignoresSafeArea()) } } diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift index bcb8f1022..9c091f847 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/DiscoveryViewModel.swift @@ -37,7 +37,7 @@ public class DiscoveryViewModel: ObservableObject { let connectivity: ConnectivityProtocol private let interactor: DiscoveryInteractorProtocol private let analytics: DiscoveryAnalytics - private let storage: CoreStorage + let storage: CoreStorage public init( router: DiscoveryRouter, @@ -112,7 +112,7 @@ public class DiscoveryViewModel: ObservableObject { fetchInProgress = false } else { - courses = try interactor.discoveryOffline() + courses = try await interactor.discoveryOffline() self.nextPage += 1 fetchInProgress = false } diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift index 265631697..b2f8ef371 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Theme public struct SearchView: View { @@ -110,12 +111,16 @@ public struct SearchView: View { LazyVStack { let searchResults = viewModel.searchResults.enumerated() + let useRelativeDates = viewModel.storage.useRelativeDates ForEach( Array(searchResults), id: \.offset) { index, course in - CourseCellView(model: course, - type: .discovery, - index: index, - cellsCount: viewModel.searchResults.count) + CourseCellView( + model: course, + type: .discovery, + index: index, + cellsCount: viewModel.searchResults.count, + useRelativeDates: useRelativeDates + ) .padding(.horizontal, 24) .onAppear { Task { @@ -173,8 +178,8 @@ public struct SearchView: View { .onDisappear { viewModel.searchText = "" } - .background(Theme.Colors.background.ignoresSafeArea()) .avoidKeyboard(dismissKeyboardByTap: true) + .background(Theme.Colors.background.ignoresSafeArea()) } } @@ -219,7 +224,8 @@ struct SearchView_Previews: PreviewProvider { interactor: DiscoveryInteractor.mock, connectivity: Connectivity(), router: router, - analytics: DiscoveryAnalyticsMock(), + analytics: DiscoveryAnalyticsMock(), + storage: CoreStorageMock(), debounce: .searchDebounce ) diff --git a/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift b/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift index 8f0c6ff1c..76f3ea137 100644 --- a/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift +++ b/Discovery/Discovery/Presentation/NativeDiscovery/SearchViewModel.swift @@ -32,6 +32,7 @@ public class SearchViewModel: ObservableObject { let router: DiscoveryRouter let analytics: DiscoveryAnalytics + let storage: CoreStorage private let interactor: DiscoveryInteractorProtocol let connectivity: ConnectivityProtocol @@ -39,12 +40,14 @@ public class SearchViewModel: ObservableObject { connectivity: ConnectivityProtocol, router: DiscoveryRouter, analytics: DiscoveryAnalytics, + storage: CoreStorage, debounce: Debounce ) { self.interactor = interactor self.connectivity = connectivity self.router = router self.analytics = analytics + self.storage = storage self.debounce = debounce $searchText diff --git a/Discovery/Discovery/Presentation/UpdateViews/UpdateRecommendedView.swift b/Discovery/Discovery/Presentation/UpdateViews/UpdateRecommendedView.swift index c2716aa82..f017d4ac4 100644 --- a/Discovery/Discovery/Presentation/UpdateViews/UpdateRecommendedView.swift +++ b/Discovery/Discovery/Presentation/UpdateViews/UpdateRecommendedView.swift @@ -11,7 +11,7 @@ import Theme public struct UpdateRecommendedView: View { - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal private let router: DiscoveryRouter private let config: ConfigProtocol diff --git a/Discovery/Discovery/Presentation/UpdateViews/UpdateRequiredView.swift b/Discovery/Discovery/Presentation/UpdateViews/UpdateRequiredView.swift index cbadd0ebf..2c3c53a79 100644 --- a/Discovery/Discovery/Presentation/UpdateViews/UpdateRequiredView.swift +++ b/Discovery/Discovery/Presentation/UpdateViews/UpdateRequiredView.swift @@ -11,7 +11,7 @@ import Theme public struct UpdateRequiredView: View { - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal private let router: DiscoveryRouter private let config: ConfigProtocol private let showAccountLink: Bool diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryURIDetails.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryURIDetails.swift index 2fa9071bd..08310d9b8 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryURIDetails.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryURIDetails.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation // Define your uri scheme public enum URIString: String { diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift index 84052bc6d..14f5395e9 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebview.swift @@ -9,18 +9,30 @@ import Foundation import SwiftUI import Theme import Core +import OEXFoundation public enum DiscoveryWebviewType: Equatable { case discovery case courseDetail(String) case programDetail(String) + + var rawValue: String { + switch self { + case .discovery: + return "discovery" + case .courseDetail(let value): + return "courseDetail(\(value))" + case .programDetail(let value): + return "programDetail(\(value))" + } + } } public struct DiscoveryWebview: View { @State private var searchQuery: String = "" @State private var isLoading: Bool = true - @ObservedObject private var viewModel: DiscoveryWebviewViewModel + @StateObject private var viewModel: DiscoveryWebviewViewModel private var router: DiscoveryRouter private var discoveryType: DiscoveryWebviewType public var pathID: String @@ -70,78 +82,90 @@ public struct DiscoveryWebview: View { discoveryType: DiscoveryWebviewType = .discovery, pathID: String = "" ) { - self.viewModel = viewModel + self._viewModel = .init(wrappedValue: viewModel) self.router = router self._searchQuery = State(initialValue: searchQuery ?? "") self.discoveryType = discoveryType self.pathID = pathID - - if let url = URL(string: URLString) { - viewModel.request = URLRequest(url: url) - } } public var body: some View { GeometryReader { proxy in - VStack(alignment: .center) { - WebView( - viewModel: .init( - url: URLString, - baseURL: "" - ), - isLoading: $isLoading, - refreshCookies: {}, - navigationDelegate: viewModel - ) - .accessibilityIdentifier("discovery_webview") - - if isLoading || viewModel.showProgress { - HStack(alignment: .center) { - ProgressBar( - size: 40, - lineWidth: 8 - ) - .padding(.vertical, proxy.size.height / 2) - .accessibilityIdentifier("progressbar") + ZStack(alignment: .center) { + VStack(alignment: .center) { + WebView( + viewModel: .init( + url: URLString, + baseURL: "", + openFile: {_ in} + ), + isLoading: $isLoading, + refreshCookies: {}, + navigationDelegate: viewModel, + connectivity: viewModel.connectivity, + webViewType: discoveryType.rawValue + ) + .accessibilityIdentifier("discovery_webview") + + if isLoading || viewModel.showProgress { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(.vertical, proxy.size.height / 2) + .accessibilityIdentifier("progress_bar") + } + .frame(width: proxy.size.width, height: proxy.size.height) } - .frame(width: proxy.size.width, height: proxy.size.height) - } - - // MARK: - Show Error - if viewModel.showError { - VStack { - SnackBarView(message: viewModel.errorMessage) + + // MARK: - Show Error + if viewModel.showError { + VStack { + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, 20) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } } - .padding(.bottom, 20) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil + + if !viewModel.userloggedIn, !isLoading { + LogistrationBottomView { buttonAction in + switch buttonAction { + case .signIn: + viewModel.router.showLoginScreen(sourceScreen: sourceScreen) + case .signInWithSSO: + viewModel.router.showLoginScreen(sourceScreen: sourceScreen) + case .register: + viewModel.router.showRegisterScreen(sourceScreen: sourceScreen) + } } } } - - if !viewModel.userloggedIn, !isLoading { - LogistrationBottomView { buttonAction in - switch buttonAction { - case .signIn: - viewModel.router.showLoginScreen(sourceScreen: sourceScreen) - case .register: - viewModel.router.showRegisterScreen(sourceScreen: sourceScreen) + + if viewModel.webViewError { + FullScreenErrorView( + type: viewModel.connectivity.isInternetAvaliable ? .generic : .noInternetWithReload + ) { + if viewModel.connectivity.isInternetAvaliable { + viewModel.webViewError = false + NotificationCenter.default.post( + name: Notification.Name(discoveryType.rawValue), + object: nil + ) } } } } - - // MARK: - Offline mode SnackBar - OfflineSnackBarView( - connectivity: viewModel.connectivity, - reloadAction: { - NotificationCenter.default.post( - name: .webviewReloadNotification, - object: nil - ) - }) + .onFirstAppear { + if let url = URL(string: URLString) { + viewModel.request = URLRequest(url: url) + } + } } .navigationBarHidden(viewModel.sourceScreen == .default && discoveryType == .discovery) .navigationTitle(CoreLocalization.Mainscreen.discovery) diff --git a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift index 18e91733b..8b9163dc1 100644 --- a/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebDiscovery/DiscoveryWebviewViewModel.swift @@ -14,6 +14,8 @@ public class DiscoveryWebviewViewModel: ObservableObject { @Published var courseDetails: CourseDetails? @Published private(set) var showProgress = false @Published var showError: Bool = false + @Published var webViewError: Bool = false + var errorMessage: String? { didSet { withAnimation { @@ -135,25 +137,14 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { } if let url = request.url, outsideLink || capturedLink || externalLink, UIApplication.shared.canOpenURL(url) { - analytics.externalLinkOpen(url: url.absoluteString, screen: sourceScreen.value ?? "") router.presentAlert( alertTitle: DiscoveryLocalization.Alert.leavingAppTitle, alertMessage: DiscoveryLocalization.Alert.leavingAppMessage, positiveAction: CoreLocalization.Webview.Alert.continue, onCloseTapped: { [weak self] in self?.router.dismiss(animated: true) - self?.analytics.externalLinkOpenAction( - url: url.absoluteString, - screen: self?.sourceScreen.value ?? "", - action: "cancel" - ) - }, okTapped: { [weak self] in + }, okTapped: { UIApplication.shared.open(url, options: [:]) - self?.analytics.externalLinkOpenAction( - url: url.absoluteString, - screen: self?.sourceScreen.value ?? "", - action: "continue" - ) }, type: .default(positiveAction: CoreLocalization.Webview.Alert.continue, image: nil) ) return true @@ -187,7 +178,7 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { case .programDetail: guard let pathID = programDetailPathId(from: url) else { return false } - analytics.discoveryEvent(event: .discoveryProgramInfo, biValue: .discoveryProgramInfo) + analytics.discoveryScreenEvent(event: .discoveryProgramInfo, biValue: .discoveryProgramInfo) router.showWebDiscoveryDetails( pathID: pathID, discoveryType: .programDetail(pathID), @@ -231,12 +222,15 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { router.showCourseScreens( courseID: courseDetails.courseID, - isActive: nil, + hasAccess: nil, courseStart: courseDetails.courseStart, courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: courseDetails.courseTitle + title: courseDetails.courseTitle, + courseRawImage: courseDetails.courseRawImage, + showDates: false, + lastVisitedBlockID: nil ) return true @@ -245,4 +239,8 @@ extension DiscoveryWebviewViewModel: WebViewNavigationDelegate { private func isValidAppURLScheme(_ url: URL) -> Bool { return url.scheme ?? "" == config.URIScheme } + + public func showWebViewError() { + self.webViewError = true + } } diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift index f97dd2c8c..5f5078238 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewView.swift @@ -9,8 +9,9 @@ import Foundation import SwiftUI import Theme import Core +import OEXFoundation -public enum ProgramViewType: Equatable { +public enum ProgramViewType: String, Equatable { case program case programDetail } @@ -18,11 +19,11 @@ public enum ProgramViewType: Equatable { public struct ProgramWebviewView: View { @State private var isLoading: Bool = true - @ObservedObject private var viewModel: ProgramWebviewViewModel + @StateObject private var viewModel: ProgramWebviewViewModel private var router: DiscoveryRouter private var viewType: ProgramViewType public var pathID: String - + private var URLString: String { switch viewType { case .program: @@ -42,71 +43,86 @@ public struct ProgramWebviewView: View { viewType: ProgramViewType = .program, pathID: String = "" ) { - self.viewModel = viewModel + self._viewModel = .init(wrappedValue: viewModel) self.router = router self.viewType = viewType self.pathID = pathID - - if let url = URL(string: URLString) { - viewModel.request = URLRequest(url: url) - } } public var body: some View { GeometryReader { proxy in - VStack(alignment: .center) { - WebView( - viewModel: .init( - url: URLString, - baseURL: "", - injections: [.colorInversionCss] - ), - isLoading: $isLoading, - refreshCookies: { - await viewModel.updateCookies( - force: true - ) - }, - navigationDelegate: viewModel - ) - .accessibilityIdentifier("program_webview") - - if isLoading || viewModel.showProgress || viewModel.updatingCookies { - HStack(alignment: .center) { - ProgressBar( - size: 40, - lineWidth: 8 - ) - .padding(.vertical, proxy.size.height / 2) - .accessibilityIdentifier("progressbar") + ZStack(alignment: .center) { + VStack(alignment: .center) { + WebView( + viewModel: .init( + url: URLString, + baseURL: "", + openFile: {_ in}, + injections: [.colorInversionCss] + ), + isLoading: $isLoading, + refreshCookies: { + await viewModel.updateCookies( + force: true + ) + }, + navigationDelegate: viewModel, + connectivity: viewModel.connectivity, + webViewType: viewType.rawValue + ) + .accessibilityIdentifier("program_webview") + + let shouldShowProgress = ( + isLoading || + viewModel.showProgress || + viewModel.updatingCookies + ) + if shouldShowProgress { + HStack(alignment: .center) { + ProgressBar( + size: 40, + lineWidth: 8 + ) + .padding(.vertical, proxy.size.height / 2) + .accessibilityIdentifier("progress_bar") + } + .frame(width: proxy.size.width, height: proxy.size.height) + } + + // MARK: - Show Error + if viewModel.showError { + VStack { + SnackBarView(message: viewModel.errorMessage) + } + .padding(.bottom, 20) + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } } - .frame(width: proxy.size.width, height: proxy.size.height) } - // MARK: - Show Error - if viewModel.showError { - VStack { - SnackBarView(message: viewModel.errorMessage) - } - .padding(.bottom, 20) - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil + if viewModel.webViewError { + FullScreenErrorView( + type: viewModel.connectivity.isInternetAvaliable ? .generic : .noInternetWithReload + ) { + if viewModel.connectivity.isInternetAvaliable { + viewModel.webViewError = false + NotificationCenter.default.post( + name: Notification.Name(viewType.rawValue), + object: nil + ) } } } } - - // MARK: - Offline mode SnackBar - OfflineSnackBarView( - connectivity: viewModel.connectivity, - reloadAction: { - NotificationCenter.default.post( - name: .webviewReloadNotification, - object: nil - ) - }) + .onFirstAppear { + if let url = URL(string: URLString) { + viewModel.request = URLRequest(url: url) + } + } } .navigationBarHidden(viewType == .program) .navigationTitle(CoreLocalization.Mainscreen.programs) @@ -114,3 +130,23 @@ public struct ProgramWebviewView: View { .animation(.default, value: viewModel.showError) } } + +#if DEBUG +struct ProgramWebviewView_Previews: PreviewProvider { + static var previews: some View { + ProgramWebviewView( + viewModel: ProgramWebviewViewModel( + router: DiscoveryRouterMock(), + config: ConfigMock(), + interactor: DiscoveryInteractor.mock, + connectivity: Connectivity(), + analytics: DiscoveryAnalyticsMock(), + authInteractor: AuthInteractor.mock + ), + router: DiscoveryRouterMock(), + viewType: .program, + pathID: "" + ) + } +} +#endif diff --git a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift index 82c234882..ed636378b 100644 --- a/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift +++ b/Discovery/Discovery/Presentation/WebPrograms/ProgramWebviewViewModel.swift @@ -14,6 +14,7 @@ public class ProgramWebviewViewModel: ObservableObject, WebviewCookiesUpdateProt @Published var courseDetails: CourseDetails? @Published private(set) var showProgress = false @Published var showError: Bool = false + @Published var webViewError: Bool = false @Published public var updatingCookies: Bool = false @Published public var cookiesReady: Bool = false @@ -219,12 +220,15 @@ extension ProgramWebviewViewModel: WebViewNavigationDelegate { router.showCourseScreens( courseID: courseDetails.courseID, - isActive: nil, + hasAccess: nil, courseStart: courseDetails.courseStart, courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: courseDetails.courseTitle + title: courseDetails.courseTitle, + courseRawImage: courseDetails.courseRawImage, + showDates: false, + lastVisitedBlockID: nil ) return true @@ -233,4 +237,8 @@ extension ProgramWebviewViewModel: WebViewNavigationDelegate { private func isValidAppURLScheme(_ url: URL) -> Bool { return url.scheme ?? "" == config.URIScheme } + + public func showWebViewError() { + self.webViewError = true + } } diff --git a/Discovery/Discovery/uk.lproj/Localizable.strings b/Discovery/Discovery/uk.lproj/Localizable.strings deleted file mode 100644 index 25f73bf53..000000000 --- a/Discovery/Discovery/uk.lproj/Localizable.strings +++ /dev/null @@ -1,37 +0,0 @@ -/* - Localizable.strings - Discovery - - Created by  Stepanok Ivan on 19.09.2022. - -*/ - -"TITLE" = "Всі курси"; -"SEARCH" = "Пошук"; -"HEADER.TITLE_1" = "Всі курси"; -"HEADER.TITLE_2" = "Давайте знайдемо нові курси для вас"; - -"SEARCH.TITLE" = "Результати пошуку"; -"SEARCH.EMPTY_DESCRIPTION" = "Почніть вводити текст, щоб знайти курс"; - -"UPDATE_REQUIRED_TITLE" = "Потрібне оновлення додатка"; -"UPDATE_REQUIRED_DESCRIPTION" = "Ця версія додатка OpenEdX застаріла. Щоб продовжити навчання та отримати останні функції та виправлення, оновіться до останньої версії."; -"UPDATE_WHY_NEED" = "Чому я маю оновити програму?"; -"UPDATE_DEPRECATED_APP" = "Застаріла версія додатка"; -"UPDATE_BUTTON" = "Оновити"; -"UPDATE_ACCOUNT_SETTINGS" = "Налаштування"; - -"UPDATE_NEEDED_TITLE" = "Оновлення додатку"; -"UPDATE_NEEDED_DESCRIPTION" = "Ми рекомендуємо вам оновити додаток до останньої версії. Оновіть зараз, щоб отримати нові функції та виправлення."; -"UPDATE_NEEDED_NOT_NOW" = "Не зараз"; - -"UPDATE_NEW_AVALIABLE" = "Доступне нове оновлення! Оновіть зараз, щоб отримати найновіші функції та виправлення"; - -"ALERT.LEAVING_APP_TITLE" = "Leaving the app"; -"ALERT.LEAVING_APP_MESSAGE" = "You are now leaving the app and opening a browser"; - -"DETAILS.TITLE" = "Деталі курсу"; -"DETAILS.VIEW_COURSE" = "Переглянути курс"; -"DETAILS.ENROLL_NOW" = "Зареєструватися"; -"DETAILS.ENROLLMENT_DATE_IS_OVER" = "Ви не можете зареєструватися на цей курс, оскільки дата реєстрації закінчилася."; -"DETAILS.ENROLLMENT_NO_INTERNET" = "Щоб зареєструватися на цьому курсі, переконайтеся, що ви підключені до Інтернету."; diff --git a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift index 28f8eba5a..285c4e619 100644 --- a/Discovery/DiscoveryTests/DiscoveryMock.generated.swift +++ b/Discovery/DiscoveryTests/DiscoveryMock.generated.swift @@ -13,6 +13,7 @@ import Discovery import Foundation import SwiftUI import Combine +import OEXFoundation // MARK: - AuthInteractorProtocol @@ -93,6 +94,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + open func login(ssoToken: String) throws -> User { + addInvocation(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) + let perform = methodPerformValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) as? (String) -> Void + perform?(`ssoToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(ssoToken: String). Use given") + Failure("Stub return value not specified for login(ssoToken: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -174,6 +191,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) + case m_login__ssoToken_ssoToken(Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields @@ -194,6 +212,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) return Matcher.ComparisonResult(results) + case (.m_login__ssoToken_ssoToken(let lhsSsotoken), .m_login__ssoToken_ssoToken(let rhsSsotoken)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSsotoken, rhs: rhsSsotoken, with: matcher), lhsSsotoken, rhsSsotoken, "ssoToken")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -224,6 +247,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue + case let .m_login__ssoToken_ssoToken(p0): return p0.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 @@ -235,6 +259,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" + case .m_login__ssoToken_ssoToken: return ".login(ssoToken:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" @@ -261,6 +286,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func login(ssoToken: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -297,6 +325,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + public static func login(ssoToken: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func login(ssoToken: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -356,6 +394,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} @discardableResult public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} + public static func login(ssoToken: Parameter) -> Verify { return Verify(method: .m_login__ssoToken_ssoToken(`ssoToken`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} @@ -375,6 +414,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) } + public static func login(ssoToken: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_login__ssoToken_ssoToken(`ssoToken`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -581,6 +623,12 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -619,6 +667,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showSSOWebBrowser__title_title(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -679,6 +728,11 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) return Matcher.ComparisonResult(results) + case (.m_showSSOWebBrowser__title_title(let lhsTitle), .m_showSSOWebBrowser__title_title(let rhsTitle)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -732,6 +786,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showSSOWebBrowser__title_title(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -752,6 +807,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showSSOWebBrowser__title_title: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -786,6 +842,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} + public static func showSSOWebBrowser(title: Parameter) -> Verify { return Verify(method: .m_showSSOWebBrowser__title_title(`title`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`))} @@ -832,6 +889,9 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showSSOWebBrowser__title_title(`title`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -919,9 +979,9 @@ open class BaseRouterMock: BaseRouter, Mock { } } -// MARK: - ConnectivityProtocol +// MARK: - CalendarManagerProtocol -open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { +open class CalendarManagerProtocolMock: CalendarManagerProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -959,51 +1019,176 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } - public var isInternetAvaliable: Bool { - get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } - } - private var __p_isInternetAvaliable: (Bool)? - public var isMobileData: Bool { - get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } - } - private var __p_isMobileData: (Bool)? - public var internetReachableSubject: CurrentValueSubject { - get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } - } - private var __p_internetReachableSubject: (CurrentValueSubject)? + open func createCalendarIfNeeded() { + addInvocation(.m_createCalendarIfNeeded) + let perform = methodPerformValue(.m_createCalendarIfNeeded) as? () -> Void + perform?() + } + + open func filterCoursesBySelected(fetchedCourses: [CourseForSync]) -> [CourseForSync] { + addInvocation(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) + let perform = methodPerformValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) as? ([CourseForSync]) -> Void + perform?(`fetchedCourses`) + var __value: [CourseForSync] + do { + __value = try methodReturnValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))).casted() + } catch { + onFatalFailure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + Failure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + } + return __value + } + + open func removeOldCalendar() { + addInvocation(.m_removeOldCalendar) + let perform = methodPerformValue(.m_removeOldCalendar) as? () -> Void + perform?() + } + + open func removeOutdatedEvents(courseID: String) { + addInvocation(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + } + + open func syncCourse(courseID: String, courseName: String, dates: CourseDates) { + addInvocation(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) + let perform = methodPerformValue(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) as? (String, String, CourseDates) -> Void + perform?(`courseID`, `courseName`, `dates`) + } + + open func requestAccess() -> Bool { + addInvocation(.m_requestAccess) + let perform = methodPerformValue(.m_requestAccess) as? () -> Void + perform?() + var __value: Bool + do { + __value = try methodReturnValue(.m_requestAccess).casted() + } catch { + onFatalFailure("Stub return value not specified for requestAccess(). Use given") + Failure("Stub return value not specified for requestAccess(). Use given") + } + return __value + } + + open func courseStatus(courseID: String) -> SyncStatus { + addInvocation(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: SyncStatus + do { + __value = try methodReturnValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch { + onFatalFailure("Stub return value not specified for courseStatus(courseID: String). Use given") + Failure("Stub return value not specified for courseStatus(courseID: String). Use given") + } + return __value + } + open func clearAllData(removeCalendar: Bool) { + addInvocation(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) + let perform = methodPerformValue(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) as? (Bool) -> Void + perform?(`removeCalendar`) + } + open func isDatesChanged(courseID: String, checksum: String) -> Bool { + addInvocation(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) + let perform = methodPerformValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) as? (String, String) -> Void + perform?(`courseID`, `checksum`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + Failure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + } + return __value + } fileprivate enum MethodType { - case p_isInternetAvaliable_get - case p_isMobileData_get - case p_internetReachableSubject_get + case m_createCalendarIfNeeded + case m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>) + case m_removeOldCalendar + case m_removeOutdatedEvents__courseID_courseID(Parameter) + case m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter, Parameter, Parameter) + case m_requestAccess + case m_courseStatus__courseID_courseID(Parameter) + case m_clearAllData__removeCalendar_removeCalendar(Parameter) + case m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match - case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match - case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + switch (lhs, rhs) { + case (.m_createCalendarIfNeeded, .m_createCalendarIfNeeded): return .match + + case (.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let lhsFetchedcourses), .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let rhsFetchedcourses)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFetchedcourses, rhs: rhsFetchedcourses, with: matcher), lhsFetchedcourses, rhsFetchedcourses, "fetchedCourses")) + return Matcher.ComparisonResult(results) + + case (.m_removeOldCalendar, .m_removeOldCalendar): return .match + + case (.m_removeOutdatedEvents__courseID_courseID(let lhsCourseid), .m_removeOutdatedEvents__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let lhsCourseid, let lhsCoursename, let lhsDates), .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let rhsCourseid, let rhsCoursename, let rhsDates)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDates, rhs: rhsDates, with: matcher), lhsDates, rhsDates, "dates")) + return Matcher.ComparisonResult(results) + + case (.m_requestAccess, .m_requestAccess): return .match + + case (.m_courseStatus__courseID_courseID(let lhsCourseid), .m_courseStatus__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_clearAllData__removeCalendar_removeCalendar(let lhsRemovecalendar), .m_clearAllData__removeCalendar_removeCalendar(let rhsRemovecalendar)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRemovecalendar, rhs: rhsRemovecalendar, with: matcher), lhsRemovecalendar, rhsRemovecalendar, "removeCalendar")) + return Matcher.ComparisonResult(results) + + case (.m_isDatesChanged__courseID_courseIDchecksum_checksum(let lhsCourseid, let lhsChecksum), .m_isDatesChanged__courseID_courseIDchecksum_checksum(let rhsCourseid, let rhsChecksum)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsChecksum, rhs: rhsChecksum, with: matcher), lhsChecksum, rhsChecksum, "checksum")) + return Matcher.ComparisonResult(results) default: return .none } } func intValue() -> Int { switch self { - case .p_isInternetAvaliable_get: return 0 - case .p_isMobileData_get: return 0 - case .p_internetReachableSubject_get: return 0 + case .m_createCalendarIfNeeded: return 0 + case let .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(p0): return p0.intValue + case .m_removeOldCalendar: return 0 + case let .m_removeOutdatedEvents__courseID_courseID(p0): return p0.intValue + case let .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case .m_requestAccess: return 0 + case let .m_courseStatus__courseID_courseID(p0): return p0.intValue + case let .m_clearAllData__removeCalendar_removeCalendar(p0): return p0.intValue + case let .m_isDatesChanged__courseID_courseIDchecksum_checksum(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { - case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" - case .p_isMobileData_get: return "[get] .isMobileData" - case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + case .m_createCalendarIfNeeded: return ".createCalendarIfNeeded()" + case .m_filterCoursesBySelected__fetchedCourses_fetchedCourses: return ".filterCoursesBySelected(fetchedCourses:)" + case .m_removeOldCalendar: return ".removeOldCalendar()" + case .m_removeOutdatedEvents__courseID_courseID: return ".removeOutdatedEvents(courseID:)" + case .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates: return ".syncCourse(courseID:courseName:dates:)" + case .m_requestAccess: return ".requestAccess()" + case .m_courseStatus__courseID_courseID: return ".courseStatus(courseID:)" + case .m_clearAllData__removeCalendar_removeCalendar: return ".clearAllData(removeCalendar:)" + case .m_isDatesChanged__courseID_courseIDchecksum_checksum: return ".isDatesChanged(courseID:checksum:)" } } } @@ -1016,30 +1201,94 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { super.init(products) } - public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willReturn: [CourseForSync]...) -> MethodStub { + return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func requestAccess(willReturn: Bool...) -> MethodStub { + return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { - return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func courseStatus(courseID: Parameter, willReturn: SyncStatus...) -> MethodStub { + return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willProduce: (Stubber<[CourseForSync]>) -> Void) -> MethodStub { + let willReturn: [[CourseForSync]] = [] + let given: Given = { return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseForSync]).self) + willProduce(stubber) + return given + } + public static func requestAccess(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given + } + public static func courseStatus(courseID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [SyncStatus] = [] + let given: Given = { return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (SyncStatus).self) + willProduce(stubber) + return given + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given } - } public struct Verify { fileprivate var method: MethodType - public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } - public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } - public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + public static func createCalendarIfNeeded() -> Verify { return Verify(method: .m_createCalendarIfNeeded)} + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>) -> Verify { return Verify(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`))} + public static func removeOldCalendar() -> Verify { return Verify(method: .m_removeOldCalendar)} + public static func removeOutdatedEvents(courseID: Parameter) -> Verify { return Verify(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`))} + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter) -> Verify { return Verify(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`))} + public static func requestAccess() -> Verify { return Verify(method: .m_requestAccess)} + public static func courseStatus(courseID: Parameter) -> Verify { return Verify(method: .m_courseStatus__courseID_courseID(`courseID`))} + public static func clearAllData(removeCalendar: Parameter) -> Verify { return Verify(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`))} + public static func isDatesChanged(courseID: Parameter, checksum: Parameter) -> Verify { return Verify(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`))} } public struct Perform { fileprivate var method: MethodType var performs: Any + public static func createCalendarIfNeeded(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_createCalendarIfNeeded, performs: perform) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, perform: @escaping ([CourseForSync]) -> Void) -> Perform { + return Perform(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), performs: perform) + } + public static func removeOldCalendar(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeOldCalendar, performs: perform) + } + public static func removeOutdatedEvents(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`), performs: perform) + } + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter, perform: @escaping (String, String, CourseDates) -> Void) -> Perform { + return Perform(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`), performs: perform) + } + public static func requestAccess(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_requestAccess, performs: perform) + } + public static func courseStatus(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_courseStatus__courseID_courseID(`courseID`), performs: perform) + } + public static func clearAllData(removeCalendar: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`), performs: perform) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), performs: perform) + } } public func given(_ method: Given) { @@ -1115,9 +1364,9 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } -// MARK: - CoreAnalytics +// MARK: - ConfigProtocol -open class CoreAnalyticsMock: CoreAnalytics, Mock { +open class ConfigProtocolMock: ConfigProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -1155,118 +1404,1837 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var baseURL: URL { + get { invocations.append(.p_baseURL_get); return __p_baseURL ?? givenGetterValue(.p_baseURL_get, "ConfigProtocolMock - stub value for baseURL was not defined") } + } + private var __p_baseURL: (URL)? + public var baseSSOURL: URL { + get { invocations.append(.p_baseSSOURL_get); return __p_baseSSOURL ?? givenGetterValue(.p_baseSSOURL_get, "ConfigProtocolMock - stub value for baseSSOURL was not defined") } + } + private var __p_baseSSOURL: (URL)? + public var ssoFinishedURL: URL { + get { invocations.append(.p_ssoFinishedURL_get); return __p_ssoFinishedURL ?? givenGetterValue(.p_ssoFinishedURL_get, "ConfigProtocolMock - stub value for ssoFinishedURL was not defined") } + } + private var __p_ssoFinishedURL: (URL)? + public var ssoButtonTitle: [String: Any] { + get { invocations.append(.p_ssoButtonTitle_get); return __p_ssoButtonTitle ?? givenGetterValue(.p_ssoButtonTitle_get, "ConfigProtocolMock - stub value for ssoButtonTitle was not defined") } + } + private var __p_ssoButtonTitle: ([String: Any])? - open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void - perform?(`event`, `parameters`) - } + public var oAuthClientId: String { + get { invocations.append(.p_oAuthClientId_get); return __p_oAuthClientId ?? givenGetterValue(.p_oAuthClientId_get, "ConfigProtocolMock - stub value for oAuthClientId was not defined") } + } + private var __p_oAuthClientId: (String)? - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void - perform?(`event`, `biValue`, `parameters`) - } + public var tokenType: TokenType { + get { invocations.append(.p_tokenType_get); return __p_tokenType ?? givenGetterValue(.p_tokenType_get, "ConfigProtocolMock - stub value for tokenType was not defined") } + } + private var __p_tokenType: (TokenType)? - open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { - addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) - let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void - perform?(`event`, `biValue`, `action`, `rating`) - } + public var feedbackEmail: String { + get { invocations.append(.p_feedbackEmail_get); return __p_feedbackEmail ?? givenGetterValue(.p_feedbackEmail_get, "ConfigProtocolMock - stub value for feedbackEmail was not defined") } + } + private var __p_feedbackEmail: (String)? - open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { - addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) - let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void - perform?(`event`, `bivalue`, `value`, `oldValue`) - } + public var appStoreLink: String { + get { invocations.append(.p_appStoreLink_get); return __p_appStoreLink ?? givenGetterValue(.p_appStoreLink_get, "ConfigProtocolMock - stub value for appStoreLink was not defined") } + } + private var __p_appStoreLink: (String)? - open func trackEvent(_ event: AnalyticsEvent) { - addInvocation(.m_trackEvent__event(Parameter.value(`event`))) - let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void - perform?(`event`) - } + public var faq: URL? { + get { invocations.append(.p_faq_get); return __p_faq ?? optionalGivenGetterValue(.p_faq_get, "ConfigProtocolMock - stub value for faq was not defined") } + } + private var __p_faq: (URL)? - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void - perform?(`event`, `biValue`) - } + public var platformName: String { + get { invocations.append(.p_platformName_get); return __p_platformName ?? givenGetterValue(.p_platformName_get, "ConfigProtocolMock - stub value for platformName was not defined") } + } + private var __p_platformName: (String)? + public var agreement: AgreementConfig { + get { invocations.append(.p_agreement_get); return __p_agreement ?? givenGetterValue(.p_agreement_get, "ConfigProtocolMock - stub value for agreement was not defined") } + } + private var __p_agreement: (AgreementConfig)? - fileprivate enum MethodType { - case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) - case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) - case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) - case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) - case m_trackEvent__event(Parameter) - case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + public var firebase: FirebaseConfig { + get { invocations.append(.p_firebase_get); return __p_firebase ?? givenGetterValue(.p_firebase_get, "ConfigProtocolMock - stub value for firebase was not defined") } + } + private var __p_firebase: (FirebaseConfig)? - static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { - case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + public var facebook: FacebookConfig { + get { invocations.append(.p_facebook_get); return __p_facebook ?? givenGetterValue(.p_facebook_get, "ConfigProtocolMock - stub value for facebook was not defined") } + } + private var __p_facebook: (FacebookConfig)? - case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + public var microsoft: MicrosoftConfig { + get { invocations.append(.p_microsoft_get); return __p_microsoft ?? givenGetterValue(.p_microsoft_get, "ConfigProtocolMock - stub value for microsoft was not defined") } + } + private var __p_microsoft: (MicrosoftConfig)? - case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) - return Matcher.ComparisonResult(results) + public var google: GoogleConfig { + get { invocations.append(.p_google_get); return __p_google ?? givenGetterValue(.p_google_get, "ConfigProtocolMock - stub value for google was not defined") } + } + private var __p_google: (GoogleConfig)? - case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) - return Matcher.ComparisonResult(results) + public var appleSignIn: AppleSignInConfig { + get { invocations.append(.p_appleSignIn_get); return __p_appleSignIn ?? givenGetterValue(.p_appleSignIn_get, "ConfigProtocolMock - stub value for appleSignIn was not defined") } + } + private var __p_appleSignIn: (AppleSignInConfig)? + + public var features: FeaturesConfig { + get { invocations.append(.p_features_get); return __p_features ?? givenGetterValue(.p_features_get, "ConfigProtocolMock - stub value for features was not defined") } + } + private var __p_features: (FeaturesConfig)? + + public var theme: ThemeConfig { + get { invocations.append(.p_theme_get); return __p_theme ?? givenGetterValue(.p_theme_get, "ConfigProtocolMock - stub value for theme was not defined") } + } + private var __p_theme: (ThemeConfig)? + + public var uiComponents: UIComponentsConfig { + get { invocations.append(.p_uiComponents_get); return __p_uiComponents ?? givenGetterValue(.p_uiComponents_get, "ConfigProtocolMock - stub value for uiComponents was not defined") } + } + private var __p_uiComponents: (UIComponentsConfig)? + + public var discovery: DiscoveryConfig { + get { invocations.append(.p_discovery_get); return __p_discovery ?? givenGetterValue(.p_discovery_get, "ConfigProtocolMock - stub value for discovery was not defined") } + } + private var __p_discovery: (DiscoveryConfig)? + + public var dashboard: DashboardConfig { + get { invocations.append(.p_dashboard_get); return __p_dashboard ?? givenGetterValue(.p_dashboard_get, "ConfigProtocolMock - stub value for dashboard was not defined") } + } + private var __p_dashboard: (DashboardConfig)? + + public var braze: BrazeConfig { + get { invocations.append(.p_braze_get); return __p_braze ?? givenGetterValue(.p_braze_get, "ConfigProtocolMock - stub value for braze was not defined") } + } + private var __p_braze: (BrazeConfig)? + + public var branch: BranchConfig { + get { invocations.append(.p_branch_get); return __p_branch ?? givenGetterValue(.p_branch_get, "ConfigProtocolMock - stub value for branch was not defined") } + } + private var __p_branch: (BranchConfig)? + + public var program: DiscoveryConfig { + get { invocations.append(.p_program_get); return __p_program ?? givenGetterValue(.p_program_get, "ConfigProtocolMock - stub value for program was not defined") } + } + private var __p_program: (DiscoveryConfig)? + + public var URIScheme: String { + get { invocations.append(.p_URIScheme_get); return __p_URIScheme ?? givenGetterValue(.p_URIScheme_get, "ConfigProtocolMock - stub value for URIScheme was not defined") } + } + private var __p_URIScheme: (String)? + + + + + + + fileprivate enum MethodType { + case p_baseURL_get + case p_baseSSOURL_get + case p_ssoFinishedURL_get + case p_ssoButtonTitle_get + case p_oAuthClientId_get + case p_tokenType_get + case p_feedbackEmail_get + case p_appStoreLink_get + case p_faq_get + case p_platformName_get + case p_agreement_get + case p_firebase_get + case p_facebook_get + case p_microsoft_get + case p_google_get + case p_appleSignIn_get + case p_features_get + case p_theme_get + case p_uiComponents_get + case p_discovery_get + case p_dashboard_get + case p_braze_get + case p_branch_get + case p_program_get + case p_URIScheme_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_baseURL_get,.p_baseURL_get): return Matcher.ComparisonResult.match + case (.p_baseSSOURL_get,.p_baseSSOURL_get): return Matcher.ComparisonResult.match + case (.p_ssoFinishedURL_get,.p_ssoFinishedURL_get): return Matcher.ComparisonResult.match + case (.p_ssoButtonTitle_get,.p_ssoButtonTitle_get): return Matcher.ComparisonResult.match + case (.p_oAuthClientId_get,.p_oAuthClientId_get): return Matcher.ComparisonResult.match + case (.p_tokenType_get,.p_tokenType_get): return Matcher.ComparisonResult.match + case (.p_feedbackEmail_get,.p_feedbackEmail_get): return Matcher.ComparisonResult.match + case (.p_appStoreLink_get,.p_appStoreLink_get): return Matcher.ComparisonResult.match + case (.p_faq_get,.p_faq_get): return Matcher.ComparisonResult.match + case (.p_platformName_get,.p_platformName_get): return Matcher.ComparisonResult.match + case (.p_agreement_get,.p_agreement_get): return Matcher.ComparisonResult.match + case (.p_firebase_get,.p_firebase_get): return Matcher.ComparisonResult.match + case (.p_facebook_get,.p_facebook_get): return Matcher.ComparisonResult.match + case (.p_microsoft_get,.p_microsoft_get): return Matcher.ComparisonResult.match + case (.p_google_get,.p_google_get): return Matcher.ComparisonResult.match + case (.p_appleSignIn_get,.p_appleSignIn_get): return Matcher.ComparisonResult.match + case (.p_features_get,.p_features_get): return Matcher.ComparisonResult.match + case (.p_theme_get,.p_theme_get): return Matcher.ComparisonResult.match + case (.p_uiComponents_get,.p_uiComponents_get): return Matcher.ComparisonResult.match + case (.p_discovery_get,.p_discovery_get): return Matcher.ComparisonResult.match + case (.p_dashboard_get,.p_dashboard_get): return Matcher.ComparisonResult.match + case (.p_braze_get,.p_braze_get): return Matcher.ComparisonResult.match + case (.p_branch_get,.p_branch_get): return Matcher.ComparisonResult.match + case (.p_program_get,.p_program_get): return Matcher.ComparisonResult.match + case (.p_URIScheme_get,.p_URIScheme_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_baseURL_get: return 0 + case .p_baseSSOURL_get: return 0 + case .p_ssoFinishedURL_get: return 0 + case .p_ssoButtonTitle_get: return 0 + case .p_oAuthClientId_get: return 0 + case .p_tokenType_get: return 0 + case .p_feedbackEmail_get: return 0 + case .p_appStoreLink_get: return 0 + case .p_faq_get: return 0 + case .p_platformName_get: return 0 + case .p_agreement_get: return 0 + case .p_firebase_get: return 0 + case .p_facebook_get: return 0 + case .p_microsoft_get: return 0 + case .p_google_get: return 0 + case .p_appleSignIn_get: return 0 + case .p_features_get: return 0 + case .p_theme_get: return 0 + case .p_uiComponents_get: return 0 + case .p_discovery_get: return 0 + case .p_dashboard_get: return 0 + case .p_braze_get: return 0 + case .p_branch_get: return 0 + case .p_program_get: return 0 + case .p_URIScheme_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_baseURL_get: return "[get] .baseURL" + case .p_baseSSOURL_get: return "[get] .baseSSOURL" + case .p_ssoFinishedURL_get: return "[get] .ssoFinishedURL" + case .p_ssoButtonTitle_get: return "[get] .ssoButtonTitle" + case .p_oAuthClientId_get: return "[get] .oAuthClientId" + case .p_tokenType_get: return "[get] .tokenType" + case .p_feedbackEmail_get: return "[get] .feedbackEmail" + case .p_appStoreLink_get: return "[get] .appStoreLink" + case .p_faq_get: return "[get] .faq" + case .p_platformName_get: return "[get] .platformName" + case .p_agreement_get: return "[get] .agreement" + case .p_firebase_get: return "[get] .firebase" + case .p_facebook_get: return "[get] .facebook" + case .p_microsoft_get: return "[get] .microsoft" + case .p_google_get: return "[get] .google" + case .p_appleSignIn_get: return "[get] .appleSignIn" + case .p_features_get: return "[get] .features" + case .p_theme_get: return "[get] .theme" + case .p_uiComponents_get: return "[get] .uiComponents" + case .p_discovery_get: return "[get] .discovery" + case .p_dashboard_get: return "[get] .dashboard" + case .p_braze_get: return "[get] .braze" + case .p_branch_get: return "[get] .branch" + case .p_program_get: return "[get] .program" + case .p_URIScheme_get: return "[get] .URIScheme" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func baseURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func baseSSOURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseSSOURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoFinishedURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_ssoFinishedURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoButtonTitle(getter defaultValue: [String: Any]...) -> PropertyStub { + return Given(method: .p_ssoButtonTitle_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func oAuthClientId(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_oAuthClientId_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func tokenType(getter defaultValue: TokenType...) -> PropertyStub { + return Given(method: .p_tokenType_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func feedbackEmail(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_feedbackEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appStoreLink(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_appStoreLink_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func faq(getter defaultValue: URL?...) -> PropertyStub { + return Given(method: .p_faq_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func platformName(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_platformName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func agreement(getter defaultValue: AgreementConfig...) -> PropertyStub { + return Given(method: .p_agreement_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func firebase(getter defaultValue: FirebaseConfig...) -> PropertyStub { + return Given(method: .p_firebase_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func facebook(getter defaultValue: FacebookConfig...) -> PropertyStub { + return Given(method: .p_facebook_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func microsoft(getter defaultValue: MicrosoftConfig...) -> PropertyStub { + return Given(method: .p_microsoft_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func google(getter defaultValue: GoogleConfig...) -> PropertyStub { + return Given(method: .p_google_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignIn(getter defaultValue: AppleSignInConfig...) -> PropertyStub { + return Given(method: .p_appleSignIn_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func features(getter defaultValue: FeaturesConfig...) -> PropertyStub { + return Given(method: .p_features_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func theme(getter defaultValue: ThemeConfig...) -> PropertyStub { + return Given(method: .p_theme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func uiComponents(getter defaultValue: UIComponentsConfig...) -> PropertyStub { + return Given(method: .p_uiComponents_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func discovery(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_discovery_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func dashboard(getter defaultValue: DashboardConfig...) -> PropertyStub { + return Given(method: .p_dashboard_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func braze(getter defaultValue: BrazeConfig...) -> PropertyStub { + return Given(method: .p_braze_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func branch(getter defaultValue: BranchConfig...) -> PropertyStub { + return Given(method: .p_branch_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func program(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_program_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func URIScheme(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_URIScheme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var baseURL: Verify { return Verify(method: .p_baseURL_get) } + public static var baseSSOURL: Verify { return Verify(method: .p_baseSSOURL_get) } + public static var ssoFinishedURL: Verify { return Verify(method: .p_ssoFinishedURL_get) } + public static var ssoButtonTitle: Verify { return Verify(method: .p_ssoButtonTitle_get) } + public static var oAuthClientId: Verify { return Verify(method: .p_oAuthClientId_get) } + public static var tokenType: Verify { return Verify(method: .p_tokenType_get) } + public static var feedbackEmail: Verify { return Verify(method: .p_feedbackEmail_get) } + public static var appStoreLink: Verify { return Verify(method: .p_appStoreLink_get) } + public static var faq: Verify { return Verify(method: .p_faq_get) } + public static var platformName: Verify { return Verify(method: .p_platformName_get) } + public static var agreement: Verify { return Verify(method: .p_agreement_get) } + public static var firebase: Verify { return Verify(method: .p_firebase_get) } + public static var facebook: Verify { return Verify(method: .p_facebook_get) } + public static var microsoft: Verify { return Verify(method: .p_microsoft_get) } + public static var google: Verify { return Verify(method: .p_google_get) } + public static var appleSignIn: Verify { return Verify(method: .p_appleSignIn_get) } + public static var features: Verify { return Verify(method: .p_features_get) } + public static var theme: Verify { return Verify(method: .p_theme_get) } + public static var uiComponents: Verify { return Verify(method: .p_uiComponents_get) } + public static var discovery: Verify { return Verify(method: .p_discovery_get) } + public static var dashboard: Verify { return Verify(method: .p_dashboard_get) } + public static var braze: Verify { return Verify(method: .p_braze_get) } + public static var branch: Verify { return Verify(method: .p_branch_get) } + public static var program: Verify { return Verify(method: .p_program_get) } + public static var URIScheme: Verify { return Verify(method: .p_URIScheme_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - ConnectivityProtocol + +open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var isInternetAvaliable: Bool { + get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } + } + private var __p_isInternetAvaliable: (Bool)? + + public var isMobileData: Bool { + get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } + } + private var __p_isMobileData: (Bool)? + + public var internetReachableSubject: CurrentValueSubject { + get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } + } + private var __p_internetReachableSubject: (CurrentValueSubject)? + + + + + + + fileprivate enum MethodType { + case p_isInternetAvaliable_get + case p_isMobileData_get + case p_internetReachableSubject_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match + case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match + case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_isInternetAvaliable_get: return 0 + case .p_isMobileData_get: return 0 + case .p_internetReachableSubject_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" + case .p_isMobileData_get: return "[get] .isMobileData" + case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { + return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } + public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } + public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreAnalytics + +open class CoreAnalyticsMock: CoreAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { + addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) + let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void + perform?(`event`, `biValue`, `action`, `rating`) + } + + open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { + addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) + let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void + perform?(`event`, `bivalue`, `value`, `oldValue`) + } + + open func trackEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + + fileprivate enum MethodType { + case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) + case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) + case m_trackEvent__event(Parameter) + case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) + return Matcher.ComparisonResult(results) + + case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_trackEvent__event(p0): return p0.intValue + case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" + case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" + case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" + case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" + case .m_trackEvent__event: return ".trackEvent(_:)" + case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} + public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { + return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) + } + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { + return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) + } + public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackEvent__event(`event`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CorePersistenceProtocol + +open class CorePersistenceProtocolMock: CorePersistenceProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func set(userId: Int) { + addInvocation(.m_set__userId_userId(Parameter.value(`userId`))) + let perform = methodPerformValue(.m_set__userId_userId(Parameter.value(`userId`))) as? (Int) -> Void + perform?(`userId`) + } + + open func getUserID() -> Int? { + addInvocation(.m_getUserID) + let perform = methodPerformValue(.m_getUserID) as? () -> Void + perform?() + var __value: Int? = nil + do { + __value = try methodReturnValue(.m_getUserID).casted() + } catch { + // do nothing + } + return __value + } + + open func publisher() -> AnyPublisher { + addInvocation(.m_publisher) + let perform = methodPerformValue(.m_publisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_publisher).casted() + } catch { + onFatalFailure("Stub return value not specified for publisher(). Use given") + Failure("Stub return value not specified for publisher(). Use given") + } + return __value + } + + open func addToDownloadQueue(tasks: [DownloadDataTask]) { + addInvocation(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) as? ([DownloadDataTask]) -> Void + perform?(`tasks`) + } + + open func saveOfflineProgress(progress: OfflineProgress) { + addInvocation(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) + let perform = methodPerformValue(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) as? (OfflineProgress) -> Void + perform?(`progress`) + } + + open func loadProgress(for blockID: String) -> OfflineProgress? { + addInvocation(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + var __value: OfflineProgress? = nil + do { + __value = try methodReturnValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))).casted() + } catch { + // do nothing + } + return __value + } + + open func loadAllOfflineProgress() -> [OfflineProgress] { + addInvocation(.m_loadAllOfflineProgress) + let perform = methodPerformValue(.m_loadAllOfflineProgress) as? () -> Void + perform?() + var __value: [OfflineProgress] + do { + __value = try methodReturnValue(.m_loadAllOfflineProgress).casted() + } catch { + onFatalFailure("Stub return value not specified for loadAllOfflineProgress(). Use given") + Failure("Stub return value not specified for loadAllOfflineProgress(). Use given") + } + return __value + } + + open func deleteProgress(for blockID: String) { + addInvocation(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + } + + open func deleteAllProgress() { + addInvocation(.m_deleteAllProgress) + let perform = methodPerformValue(.m_deleteAllProgress) as? () -> Void + perform?() + } + + open func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) { + addInvocation(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) as? ([CourseBlock], DownloadQuality) -> Void + perform?(`blocks`, `downloadQuality`) + } + + open func nextBlockForDownloading() -> DownloadDataTask? { + addInvocation(.m_nextBlockForDownloading) + let perform = methodPerformValue(.m_nextBlockForDownloading) as? () -> Void + perform?() + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_nextBlockForDownloading).casted() + } catch { + // do nothing + } + return __value + } + + open func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) { + addInvocation(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) + let perform = methodPerformValue(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) as? (String, DownloadState, Data?) -> Void + perform?(`id`, `state`, `resumeData`) + } + + open func deleteDownloadDataTask(id: String) throws { + addInvocation(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) + let perform = methodPerformValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) as? (String) -> Void + perform?(`id`) + do { + _ = try methodReturnValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func saveDownloadDataTask(_ task: DownloadDataTask) { + addInvocation(.m_saveDownloadDataTask__task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_saveDownloadDataTask__task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) + } + + open func downloadDataTask(for blockId: String) -> DownloadDataTask? { + addInvocation(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) + let perform = methodPerformValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void + perform?(`blockId`) + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))).casted() + } catch { + // do nothing + } + return __value + } + + open func getDownloadDataTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasks) + let perform = methodPerformValue(.m_getDownloadDataTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasks(). Use given") + Failure("Stub return value not specified for getDownloadDataTasks(). Use given") + } + return __value + } + + open func getDownloadDataTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + } + return __value + } + + + fileprivate enum MethodType { + case m_set__userId_userId(Parameter) + case m_getUserID + case m_publisher + case m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>) + case m_saveOfflineProgress__progress_progress(Parameter) + case m_loadProgress__for_blockID(Parameter) + case m_loadAllOfflineProgress + case m_deleteProgress__for_blockID(Parameter) + case m_deleteAllProgress + case m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>, Parameter) + case m_nextBlockForDownloading + case m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter, Parameter, Parameter) + case m_deleteDownloadDataTask__id_id(Parameter) + case m_saveDownloadDataTask__task(Parameter) + case m_downloadDataTask__for_blockId(Parameter) + case m_getDownloadDataTasks + case m_getDownloadDataTasksForCourse__courseId(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_set__userId_userId(let lhsUserid), .m_set__userId_userId(let rhsUserid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUserid, rhs: rhsUserid, with: matcher), lhsUserid, rhsUserid, "userId")) + return Matcher.ComparisonResult(results) + + case (.m_getUserID, .m_getUserID): return .match + + case (.m_publisher, .m_publisher): return .match + + case (.m_addToDownloadQueue__tasks_tasks(let lhsTasks), .m_addToDownloadQueue__tasks_tasks(let rhsTasks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTasks, rhs: rhsTasks, with: matcher), lhsTasks, rhsTasks, "tasks")) + return Matcher.ComparisonResult(results) + + case (.m_saveOfflineProgress__progress_progress(let lhsProgress), .m_saveOfflineProgress__progress_progress(let rhsProgress)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsProgress, rhs: rhsProgress, with: matcher), lhsProgress, rhsProgress, "progress")) + return Matcher.ComparisonResult(results) + + case (.m_loadProgress__for_blockID(let lhsBlockid), .m_loadProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_loadAllOfflineProgress, .m_loadAllOfflineProgress): return .match + + case (.m_deleteProgress__for_blockID(let lhsBlockid), .m_deleteProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_deleteAllProgress, .m_deleteAllProgress): return .match + + case (.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let lhsBlocks, let lhsDownloadquality), .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let rhsBlocks, let rhsDownloadquality)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDownloadquality, rhs: rhsDownloadquality, with: matcher), lhsDownloadquality, rhsDownloadquality, "downloadQuality")) + return Matcher.ComparisonResult(results) + + case (.m_nextBlockForDownloading, .m_nextBlockForDownloading): return .match + + case (.m_updateDownloadState__id_idstate_stateresumeData_resumeData(let lhsId, let lhsState, let lhsResumedata), .m_updateDownloadState__id_idstate_stateresumeData_resumeData(let rhsId, let rhsState, let rhsResumedata)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsState, rhs: rhsState, with: matcher), lhsState, rhsState, "state")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResumedata, rhs: rhsResumedata, with: matcher), lhsResumedata, rhsResumedata, "resumeData")) + return Matcher.ComparisonResult(results) + + case (.m_deleteDownloadDataTask__id_id(let lhsId), .m_deleteDownloadDataTask__id_id(let rhsId)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + return Matcher.ComparisonResult(results) + + case (.m_saveDownloadDataTask__task(let lhsTask), .m_saveDownloadDataTask__task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "_ task")) + return Matcher.ComparisonResult(results) + + case (.m_downloadDataTask__for_blockId(let lhsBlockid), .m_downloadDataTask__for_blockId(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) + return Matcher.ComparisonResult(results) + + case (.m_getDownloadDataTasks, .m_getDownloadDataTasks): return .match + + case (.m_getDownloadDataTasksForCourse__courseId(let lhsCourseid), .m_getDownloadDataTasksForCourse__courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_set__userId_userId(p0): return p0.intValue + case .m_getUserID: return 0 + case .m_publisher: return 0 + case let .m_addToDownloadQueue__tasks_tasks(p0): return p0.intValue + case let .m_saveOfflineProgress__progress_progress(p0): return p0.intValue + case let .m_loadProgress__for_blockID(p0): return p0.intValue + case .m_loadAllOfflineProgress: return 0 + case let .m_deleteProgress__for_blockID(p0): return p0.intValue + case .m_deleteAllProgress: return 0 + case let .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(p0, p1): return p0.intValue + p1.intValue + case .m_nextBlockForDownloading: return 0 + case let .m_updateDownloadState__id_idstate_stateresumeData_resumeData(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_deleteDownloadDataTask__id_id(p0): return p0.intValue + case let .m_saveDownloadDataTask__task(p0): return p0.intValue + case let .m_downloadDataTask__for_blockId(p0): return p0.intValue + case .m_getDownloadDataTasks: return 0 + case let .m_getDownloadDataTasksForCourse__courseId(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_set__userId_userId: return ".set(userId:)" + case .m_getUserID: return ".getUserID()" + case .m_publisher: return ".publisher()" + case .m_addToDownloadQueue__tasks_tasks: return ".addToDownloadQueue(tasks:)" + case .m_saveOfflineProgress__progress_progress: return ".saveOfflineProgress(progress:)" + case .m_loadProgress__for_blockID: return ".loadProgress(for:)" + case .m_loadAllOfflineProgress: return ".loadAllOfflineProgress()" + case .m_deleteProgress__for_blockID: return ".deleteProgress(for:)" + case .m_deleteAllProgress: return ".deleteAllProgress()" + case .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality: return ".addToDownloadQueue(blocks:downloadQuality:)" + case .m_nextBlockForDownloading: return ".nextBlockForDownloading()" + case .m_updateDownloadState__id_idstate_stateresumeData_resumeData: return ".updateDownloadState(id:state:resumeData:)" + case .m_deleteDownloadDataTask__id_id: return ".deleteDownloadDataTask(id:)" + case .m_saveDownloadDataTask__task: return ".saveDownloadDataTask(_:)" + case .m_downloadDataTask__for_blockId: return ".downloadDataTask(for:)" + case .m_getDownloadDataTasks: return ".getDownloadDataTasks()" + case .m_getDownloadDataTasksForCourse__courseId: return ".getDownloadDataTasksForCourse(_:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func getUserID(willReturn: Int?...) -> MethodStub { + return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func publisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadProgress(for blockID: Parameter, willReturn: OfflineProgress?...) -> MethodStub { + return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadAllOfflineProgress(willReturn: [OfflineProgress]...) -> MethodStub { + return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func nextBlockForDownloading(willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func downloadDataTask(for blockId: Parameter, willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getUserID(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Int?] = [] + let given: Given = { return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Int?).self) + willProduce(stubber) + return given + } + public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func loadProgress(for blockID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [OfflineProgress?] = [] + let given: Given = { return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (OfflineProgress?).self) + willProduce(stubber) + return given + } + public static func loadAllOfflineProgress(willProduce: (Stubber<[OfflineProgress]>) -> Void) -> MethodStub { + let willReturn: [[OfflineProgress]] = [] + let given: Given = { return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([OfflineProgress]).self) + willProduce(stubber) + return given + } + public static func nextBlockForDownloading(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func downloadDataTask(for blockId: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func deleteDownloadDataTask(id: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func deleteDownloadDataTask(id: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func set(userId: Parameter) -> Verify { return Verify(method: .m_set__userId_userId(`userId`))} + public static func getUserID() -> Verify { return Verify(method: .m_getUserID)} + public static func publisher() -> Verify { return Verify(method: .m_publisher)} + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>) -> Verify { return Verify(method: .m_addToDownloadQueue__tasks_tasks(`tasks`))} + public static func saveOfflineProgress(progress: Parameter) -> Verify { return Verify(method: .m_saveOfflineProgress__progress_progress(`progress`))} + public static func loadProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_loadProgress__for_blockID(`blockID`))} + public static func loadAllOfflineProgress() -> Verify { return Verify(method: .m_loadAllOfflineProgress)} + public static func deleteProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_deleteProgress__for_blockID(`blockID`))} + public static func deleteAllProgress() -> Verify { return Verify(method: .m_deleteAllProgress)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`))} + public static func nextBlockForDownloading() -> Verify { return Verify(method: .m_nextBlockForDownloading)} + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter) -> Verify { return Verify(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`))} + public static func deleteDownloadDataTask(id: Parameter) -> Verify { return Verify(method: .m_deleteDownloadDataTask__id_id(`id`))} + public static func saveDownloadDataTask(_ task: Parameter) -> Verify { return Verify(method: .m_saveDownloadDataTask__task(`task`))} + public static func downloadDataTask(for blockId: Parameter) -> Verify { return Verify(method: .m_downloadDataTask__for_blockId(`blockId`))} + public static func getDownloadDataTasks() -> Verify { return Verify(method: .m_getDownloadDataTasks)} + public static func getDownloadDataTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func set(userId: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_set__userId_userId(`userId`), performs: perform) + } + public static func getUserID(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getUserID, performs: perform) + } + public static func publisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_publisher, performs: perform) + } + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>, perform: @escaping ([DownloadDataTask]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__tasks_tasks(`tasks`), performs: perform) + } + public static func saveOfflineProgress(progress: Parameter, perform: @escaping (OfflineProgress) -> Void) -> Perform { + return Perform(method: .m_saveOfflineProgress__progress_progress(`progress`), performs: perform) + } + public static func loadProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_loadProgress__for_blockID(`blockID`), performs: perform) + } + public static func loadAllOfflineProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_loadAllOfflineProgress, performs: perform) + } + public static func deleteProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteProgress__for_blockID(`blockID`), performs: perform) + } + public static func deleteAllProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteAllProgress, performs: perform) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter, perform: @escaping ([CourseBlock], DownloadQuality) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`), performs: perform) + } + public static func nextBlockForDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_nextBlockForDownloading, performs: perform) + } + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter, perform: @escaping (String, DownloadState, Data?) -> Void) -> Perform { + return Perform(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`), performs: perform) + } + public static func deleteDownloadDataTask(id: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteDownloadDataTask__id_id(`id`), performs: perform) + } + public static func saveDownloadDataTask(_ task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_saveDownloadDataTask__task(`task`), performs: perform) + } + public static func downloadDataTask(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_downloadDataTask__for_blockId(`blockId`), performs: perform) + } + public static func getDownloadDataTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasks, performs: perform) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreStorage + +open class CoreStorageMock: CoreStorage, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var accessToken: String? { + get { invocations.append(.p_accessToken_get); return __p_accessToken ?? optionalGivenGetterValue(.p_accessToken_get, "CoreStorageMock - stub value for accessToken was not defined") } + set { invocations.append(.p_accessToken_set(.value(newValue))); __p_accessToken = newValue } + } + private var __p_accessToken: (String)? + + public var refreshToken: String? { + get { invocations.append(.p_refreshToken_get); return __p_refreshToken ?? optionalGivenGetterValue(.p_refreshToken_get, "CoreStorageMock - stub value for refreshToken was not defined") } + set { invocations.append(.p_refreshToken_set(.value(newValue))); __p_refreshToken = newValue } + } + private var __p_refreshToken: (String)? + + public var pushToken: String? { + get { invocations.append(.p_pushToken_get); return __p_pushToken ?? optionalGivenGetterValue(.p_pushToken_get, "CoreStorageMock - stub value for pushToken was not defined") } + set { invocations.append(.p_pushToken_set(.value(newValue))); __p_pushToken = newValue } + } + private var __p_pushToken: (String)? + + public var appleSignFullName: String? { + get { invocations.append(.p_appleSignFullName_get); return __p_appleSignFullName ?? optionalGivenGetterValue(.p_appleSignFullName_get, "CoreStorageMock - stub value for appleSignFullName was not defined") } + set { invocations.append(.p_appleSignFullName_set(.value(newValue))); __p_appleSignFullName = newValue } + } + private var __p_appleSignFullName: (String)? + + public var appleSignEmail: String? { + get { invocations.append(.p_appleSignEmail_get); return __p_appleSignEmail ?? optionalGivenGetterValue(.p_appleSignEmail_get, "CoreStorageMock - stub value for appleSignEmail was not defined") } + set { invocations.append(.p_appleSignEmail_set(.value(newValue))); __p_appleSignEmail = newValue } + } + private var __p_appleSignEmail: (String)? + + public var cookiesDate: Date? { + get { invocations.append(.p_cookiesDate_get); return __p_cookiesDate ?? optionalGivenGetterValue(.p_cookiesDate_get, "CoreStorageMock - stub value for cookiesDate was not defined") } + set { invocations.append(.p_cookiesDate_set(.value(newValue))); __p_cookiesDate = newValue } + } + private var __p_cookiesDate: (Date)? + + public var reviewLastShownVersion: String? { + get { invocations.append(.p_reviewLastShownVersion_get); return __p_reviewLastShownVersion ?? optionalGivenGetterValue(.p_reviewLastShownVersion_get, "CoreStorageMock - stub value for reviewLastShownVersion was not defined") } + set { invocations.append(.p_reviewLastShownVersion_set(.value(newValue))); __p_reviewLastShownVersion = newValue } + } + private var __p_reviewLastShownVersion: (String)? + + public var lastReviewDate: Date? { + get { invocations.append(.p_lastReviewDate_get); return __p_lastReviewDate ?? optionalGivenGetterValue(.p_lastReviewDate_get, "CoreStorageMock - stub value for lastReviewDate was not defined") } + set { invocations.append(.p_lastReviewDate_set(.value(newValue))); __p_lastReviewDate = newValue } + } + private var __p_lastReviewDate: (Date)? + + public var user: DataLayer.User? { + get { invocations.append(.p_user_get); return __p_user ?? optionalGivenGetterValue(.p_user_get, "CoreStorageMock - stub value for user was not defined") } + set { invocations.append(.p_user_set(.value(newValue))); __p_user = newValue } + } + private var __p_user: (DataLayer.User)? + + public var userSettings: UserSettings? { + get { invocations.append(.p_userSettings_get); return __p_userSettings ?? optionalGivenGetterValue(.p_userSettings_get, "CoreStorageMock - stub value for userSettings was not defined") } + set { invocations.append(.p_userSettings_set(.value(newValue))); __p_userSettings = newValue } + } + private var __p_userSettings: (UserSettings)? + + public var resetAppSupportDirectoryUserData: Bool? { + get { invocations.append(.p_resetAppSupportDirectoryUserData_get); return __p_resetAppSupportDirectoryUserData ?? optionalGivenGetterValue(.p_resetAppSupportDirectoryUserData_get, "CoreStorageMock - stub value for resetAppSupportDirectoryUserData was not defined") } + set { invocations.append(.p_resetAppSupportDirectoryUserData_set(.value(newValue))); __p_resetAppSupportDirectoryUserData = newValue } + } + private var __p_resetAppSupportDirectoryUserData: (Bool)? + + public var useRelativeDates: Bool { + get { invocations.append(.p_useRelativeDates_get); return __p_useRelativeDates ?? givenGetterValue(.p_useRelativeDates_get, "CoreStorageMock - stub value for useRelativeDates was not defined") } + set { invocations.append(.p_useRelativeDates_set(.value(newValue))); __p_useRelativeDates = newValue } + } + private var __p_useRelativeDates: (Bool)? + + + + + + open func clear() { + addInvocation(.m_clear) + let perform = methodPerformValue(.m_clear) as? () -> Void + perform?() + } - case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - return Matcher.ComparisonResult(results) - case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - return Matcher.ComparisonResult(results) + fileprivate enum MethodType { + case m_clear + case p_accessToken_get + case p_accessToken_set(Parameter) + case p_refreshToken_get + case p_refreshToken_set(Parameter) + case p_pushToken_get + case p_pushToken_set(Parameter) + case p_appleSignFullName_get + case p_appleSignFullName_set(Parameter) + case p_appleSignEmail_get + case p_appleSignEmail_set(Parameter) + case p_cookiesDate_get + case p_cookiesDate_set(Parameter) + case p_reviewLastShownVersion_get + case p_reviewLastShownVersion_set(Parameter) + case p_lastReviewDate_get + case p_lastReviewDate_set(Parameter) + case p_user_get + case p_user_set(Parameter) + case p_userSettings_get + case p_userSettings_set(Parameter) + case p_resetAppSupportDirectoryUserData_get + case p_resetAppSupportDirectoryUserData_set(Parameter) + case p_useRelativeDates_get + case p_useRelativeDates_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_clear, .m_clear): return .match + case (.p_accessToken_get,.p_accessToken_get): return Matcher.ComparisonResult.match + case (.p_accessToken_set(let left),.p_accessToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_refreshToken_get,.p_refreshToken_get): return Matcher.ComparisonResult.match + case (.p_refreshToken_set(let left),.p_refreshToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_pushToken_get,.p_pushToken_get): return Matcher.ComparisonResult.match + case (.p_pushToken_set(let left),.p_pushToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignFullName_get,.p_appleSignFullName_get): return Matcher.ComparisonResult.match + case (.p_appleSignFullName_set(let left),.p_appleSignFullName_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignEmail_get,.p_appleSignEmail_get): return Matcher.ComparisonResult.match + case (.p_appleSignEmail_set(let left),.p_appleSignEmail_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_cookiesDate_get,.p_cookiesDate_get): return Matcher.ComparisonResult.match + case (.p_cookiesDate_set(let left),.p_cookiesDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_reviewLastShownVersion_get,.p_reviewLastShownVersion_get): return Matcher.ComparisonResult.match + case (.p_reviewLastShownVersion_set(let left),.p_reviewLastShownVersion_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_lastReviewDate_get,.p_lastReviewDate_get): return Matcher.ComparisonResult.match + case (.p_lastReviewDate_set(let left),.p_lastReviewDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_user_get,.p_user_get): return Matcher.ComparisonResult.match + case (.p_user_set(let left),.p_user_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_userSettings_get,.p_userSettings_get): return Matcher.ComparisonResult.match + case (.p_userSettings_set(let left),.p_userSettings_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_resetAppSupportDirectoryUserData_get,.p_resetAppSupportDirectoryUserData_get): return Matcher.ComparisonResult.match + case (.p_resetAppSupportDirectoryUserData_set(let left),.p_resetAppSupportDirectoryUserData_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_useRelativeDates_get,.p_useRelativeDates_get): return Matcher.ComparisonResult.match + case (.p_useRelativeDates_set(let left),.p_useRelativeDates_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) default: return .none } } func intValue() -> Int { switch self { - case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue - case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue - case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue - case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue - case let .m_trackEvent__event(p0): return p0.intValue - case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case .m_clear: return 0 + case .p_accessToken_get: return 0 + case .p_accessToken_set(let newValue): return newValue.intValue + case .p_refreshToken_get: return 0 + case .p_refreshToken_set(let newValue): return newValue.intValue + case .p_pushToken_get: return 0 + case .p_pushToken_set(let newValue): return newValue.intValue + case .p_appleSignFullName_get: return 0 + case .p_appleSignFullName_set(let newValue): return newValue.intValue + case .p_appleSignEmail_get: return 0 + case .p_appleSignEmail_set(let newValue): return newValue.intValue + case .p_cookiesDate_get: return 0 + case .p_cookiesDate_set(let newValue): return newValue.intValue + case .p_reviewLastShownVersion_get: return 0 + case .p_reviewLastShownVersion_set(let newValue): return newValue.intValue + case .p_lastReviewDate_get: return 0 + case .p_lastReviewDate_set(let newValue): return newValue.intValue + case .p_user_get: return 0 + case .p_user_set(let newValue): return newValue.intValue + case .p_userSettings_get: return 0 + case .p_userSettings_set(let newValue): return newValue.intValue + case .p_resetAppSupportDirectoryUserData_get: return 0 + case .p_resetAppSupportDirectoryUserData_set(let newValue): return newValue.intValue + case .p_useRelativeDates_get: return 0 + case .p_useRelativeDates_set(let newValue): return newValue.intValue } } func assertionName() -> String { switch self { - case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" - case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" - case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" - case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" - case .m_trackEvent__event: return ".trackEvent(_:)" - case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_clear: return ".clear()" + case .p_accessToken_get: return "[get] .accessToken" + case .p_accessToken_set: return "[set] .accessToken" + case .p_refreshToken_get: return "[get] .refreshToken" + case .p_refreshToken_set: return "[set] .refreshToken" + case .p_pushToken_get: return "[get] .pushToken" + case .p_pushToken_set: return "[set] .pushToken" + case .p_appleSignFullName_get: return "[get] .appleSignFullName" + case .p_appleSignFullName_set: return "[set] .appleSignFullName" + case .p_appleSignEmail_get: return "[get] .appleSignEmail" + case .p_appleSignEmail_set: return "[set] .appleSignEmail" + case .p_cookiesDate_get: return "[get] .cookiesDate" + case .p_cookiesDate_set: return "[set] .cookiesDate" + case .p_reviewLastShownVersion_get: return "[get] .reviewLastShownVersion" + case .p_reviewLastShownVersion_set: return "[set] .reviewLastShownVersion" + case .p_lastReviewDate_get: return "[get] .lastReviewDate" + case .p_lastReviewDate_set: return "[set] .lastReviewDate" + case .p_user_get: return "[get] .user" + case .p_user_set: return "[set] .user" + case .p_userSettings_get: return "[get] .userSettings" + case .p_userSettings_set: return "[set] .userSettings" + case .p_resetAppSupportDirectoryUserData_get: return "[get] .resetAppSupportDirectoryUserData" + case .p_resetAppSupportDirectoryUserData_set: return "[set] .resetAppSupportDirectoryUserData" + case .p_useRelativeDates_get: return "[get] .useRelativeDates" + case .p_useRelativeDates_set: return "[set] .useRelativeDates" } } } @@ -1279,41 +3247,81 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { super.init(products) } + public static func accessToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_accessToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func refreshToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_refreshToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func pushToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_pushToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignFullName(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignFullName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignEmail(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_cookiesDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func reviewLastShownVersion(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_reviewLastShownVersion_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func lastReviewDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_lastReviewDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func user(getter defaultValue: DataLayer.User?...) -> PropertyStub { + return Given(method: .p_user_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func userSettings(getter defaultValue: UserSettings?...) -> PropertyStub { + return Given(method: .p_userSettings_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func resetAppSupportDirectoryUserData(getter defaultValue: Bool?...) -> PropertyStub { + return Given(method: .p_resetAppSupportDirectoryUserData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func useRelativeDates(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_useRelativeDates_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } } public struct Verify { fileprivate var method: MethodType - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} - public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func clear() -> Verify { return Verify(method: .m_clear)} + public static var accessToken: Verify { return Verify(method: .p_accessToken_get) } + public static func accessToken(set newValue: Parameter) -> Verify { return Verify(method: .p_accessToken_set(newValue)) } + public static var refreshToken: Verify { return Verify(method: .p_refreshToken_get) } + public static func refreshToken(set newValue: Parameter) -> Verify { return Verify(method: .p_refreshToken_set(newValue)) } + public static var pushToken: Verify { return Verify(method: .p_pushToken_get) } + public static func pushToken(set newValue: Parameter) -> Verify { return Verify(method: .p_pushToken_set(newValue)) } + public static var appleSignFullName: Verify { return Verify(method: .p_appleSignFullName_get) } + public static func appleSignFullName(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignFullName_set(newValue)) } + public static var appleSignEmail: Verify { return Verify(method: .p_appleSignEmail_get) } + public static func appleSignEmail(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignEmail_set(newValue)) } + public static var cookiesDate: Verify { return Verify(method: .p_cookiesDate_get) } + public static func cookiesDate(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesDate_set(newValue)) } + public static var reviewLastShownVersion: Verify { return Verify(method: .p_reviewLastShownVersion_get) } + public static func reviewLastShownVersion(set newValue: Parameter) -> Verify { return Verify(method: .p_reviewLastShownVersion_set(newValue)) } + public static var lastReviewDate: Verify { return Verify(method: .p_lastReviewDate_get) } + public static func lastReviewDate(set newValue: Parameter) -> Verify { return Verify(method: .p_lastReviewDate_set(newValue)) } + public static var user: Verify { return Verify(method: .p_user_get) } + public static func user(set newValue: Parameter) -> Verify { return Verify(method: .p_user_set(newValue)) } + public static var userSettings: Verify { return Verify(method: .p_userSettings_get) } + public static func userSettings(set newValue: Parameter) -> Verify { return Verify(method: .p_userSettings_set(newValue)) } + public static var resetAppSupportDirectoryUserData: Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_get) } + public static func resetAppSupportDirectoryUserData(set newValue: Parameter) -> Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_set(newValue)) } + public static var useRelativeDates: Verify { return Verify(method: .p_useRelativeDates_get) } + public static func useRelativeDates(set newValue: Parameter) -> Verify { return Verify(method: .p_useRelativeDates_set(newValue)) } } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) - } - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) - } - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { - return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) - } - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { - return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) - } - public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { - return Perform(method: .m_trackEvent__event(`event`), performs: perform) - } - public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + public static func clear(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_clear, performs: perform) } } @@ -1482,9 +3490,9 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { perform?(`url`, `screen`, `action`) } - open func discoveryEvent(event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_discoveryEvent__event_eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) - let perform = methodPerformValue(.m_discoveryEvent__event_eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + open func discoveryScreenEvent(event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_discoveryScreenEvent__event_eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_discoveryScreenEvent__event_eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void perform?(`event`, `biValue`) } @@ -1498,7 +3506,7 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { case m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(Parameter, Parameter) case m_externalLinkOpen__url_urlscreen_screen(Parameter, Parameter) case m_externalLinkOpenAction__url_urlscreen_screenaction_action(Parameter, Parameter, Parameter) - case m_discoveryEvent__event_eventbiValue_biValue(Parameter, Parameter) + case m_discoveryScreenEvent__event_eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { @@ -1547,7 +3555,7 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) return Matcher.ComparisonResult(results) - case (.m_discoveryEvent__event_eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_discoveryEvent__event_eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + case (.m_discoveryScreenEvent__event_eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_discoveryScreenEvent__event_eventbiValue_biValue(let rhsEvent, let rhsBivalue)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "event")) results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) @@ -1566,7 +3574,7 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { case let .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(p0, p1): return p0.intValue + p1.intValue case let .m_externalLinkOpen__url_urlscreen_screen(p0, p1): return p0.intValue + p1.intValue case let .m_externalLinkOpenAction__url_urlscreen_screenaction_action(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue - case let .m_discoveryEvent__event_eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_discoveryScreenEvent__event_eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { @@ -1579,7 +3587,7 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { case .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName: return ".courseEnrollSuccess(courseId:courseName:)" case .m_externalLinkOpen__url_urlscreen_screen: return ".externalLinkOpen(url:screen:)" case .m_externalLinkOpenAction__url_urlscreen_screenaction_action: return ".externalLinkOpenAction(url:screen:action:)" - case .m_discoveryEvent__event_eventbiValue_biValue: return ".discoveryEvent(event:biValue:)" + case .m_discoveryScreenEvent__event_eventbiValue_biValue: return ".discoveryScreenEvent(event:biValue:)" } } } @@ -1606,7 +3614,7 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { public static func courseEnrollSuccess(courseId: Parameter, courseName: Parameter) -> Verify { return Verify(method: .m_courseEnrollSuccess__courseId_courseIdcourseName_courseName(`courseId`, `courseName`))} public static func externalLinkOpen(url: Parameter, screen: Parameter) -> Verify { return Verify(method: .m_externalLinkOpen__url_urlscreen_screen(`url`, `screen`))} public static func externalLinkOpenAction(url: Parameter, screen: Parameter, action: Parameter) -> Verify { return Verify(method: .m_externalLinkOpenAction__url_urlscreen_screenaction_action(`url`, `screen`, `action`))} - public static func discoveryEvent(event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_discoveryEvent__event_eventbiValue_biValue(`event`, `biValue`))} + public static func discoveryScreenEvent(event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_discoveryScreenEvent__event_eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { @@ -1637,8 +3645,8 @@ open class DiscoveryAnalyticsMock: DiscoveryAnalytics, Mock { public static func externalLinkOpenAction(url: Parameter, screen: Parameter, action: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { return Perform(method: .m_externalLinkOpenAction__url_urlscreen_screenaction_action(`url`, `screen`, `action`), performs: perform) } - public static func discoveryEvent(event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { - return Perform(method: .m_discoveryEvent__event_eventbiValue_biValue(`event`, `biValue`), performs: perform) + public static func discoveryScreenEvent(event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_discoveryScreenEvent__event_eventbiValue_biValue(`event`, `biValue`), performs: perform) } } @@ -2311,6 +4319,20 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func updateUnzippedFileSize(for sequentials: [CourseSequential]) -> [CourseSequential] { + addInvocation(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) + let perform = methodPerformValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) as? ([CourseSequential]) -> Void + perform?(`sequentials`) + var __value: [CourseSequential] + do { + __value = try methodReturnValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))).casted() + } catch { + onFatalFailure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + Failure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + } + return __value + } + open func resumeDownloading() throws { addInvocation(.m_resumeDownloading) let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void @@ -2338,6 +4360,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func removeAppSupportDirectoryUnusedContent() { + addInvocation(.m_removeAppSupportDirectoryUnusedContent) + let perform = methodPerformValue(.m_removeAppSupportDirectoryUnusedContent) as? () -> Void + perform?() + } + fileprivate enum MethodType { case m_publisher @@ -2352,8 +4380,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>) case m_resumeDownloading case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_removeAppSupportDirectoryUnusedContent case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -2404,12 +4434,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + case (.m_updateUnzippedFileSize__for_sequentials(let lhsSequentials), .m_updateUnzippedFileSize__for_sequentials(let rhsSequentials)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSequentials, rhs: rhsSequentials, with: matcher), lhsSequentials, rhsSequentials, "for sequentials")) + return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) + + case (.m_removeAppSupportDirectoryUnusedContent, .m_removeAppSupportDirectoryUnusedContent): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -2429,8 +4466,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_updateUnzippedFileSize__for_sequentials(p0): return p0.intValue case .m_resumeDownloading: return 0 case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_removeAppSupportDirectoryUnusedContent: return 0 case .p_currentDownloadTask_get: return 0 } } @@ -2448,8 +4487,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_updateUnzippedFileSize__for_sequentials: return ".updateUnzippedFileSize(for:)" case .m_resumeDownloading: return ".resumeDownloading()" case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_removeAppSupportDirectoryUnusedContent: return ".removeAppSupportDirectoryUnusedContent()" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -2482,6 +4523,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willReturn: [CourseSequential]...) -> MethodStub { + return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -2520,6 +4564,13 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willProduce: (Stubber<[CourseSequential]>) -> Void) -> MethodStub { + let willReturn: [[CourseSequential]] = [] + let given: Given = { return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseSequential]).self) + willProduce(stubber) + return given + } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { let willReturn: [Bool] = [] let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -2604,8 +4655,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>) -> Verify { return Verify(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func removeAppSupportDirectoryUnusedContent() -> Verify { return Verify(method: .m_removeAppSupportDirectoryUnusedContent)} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -2649,12 +4702,217 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, perform: @escaping ([CourseSequential]) -> Void) -> Perform { + return Perform(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), performs: perform) + } public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } + public static func removeAppSupportDirectoryUnusedContent(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeAppSupportDirectoryUnusedContent, performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - OfflineSyncInteractorProtocol + +open class OfflineSyncInteractorProtocolMock: OfflineSyncInteractorProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func submitOfflineProgress(courseID: String, blockID: String, data: String) throws -> Bool { + addInvocation(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) + let perform = methodPerformValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) as? (String, String, String) -> Void + perform?(`courseID`, `blockID`, `data`) + var __value: Bool + do { + __value = try methodReturnValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + Failure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + } catch { + throw error + } + return __value + } + + + fileprivate enum MethodType { + case m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter, Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let lhsCourseid, let lhsBlockid, let lhsData), .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let rhsCourseid, let rhsBlockid, let rhsData)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsData, rhs: rhsData, with: matcher), lhsData, rhsData, "data")) + return Matcher.ComparisonResult(results) + } + } + + func intValue() -> Int { + switch self { + case let .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + } + } + func assertionName() -> String { + switch self { + case .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data: return ".submitOfflineProgress(courseID:blockID:data:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Bool).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter) -> Verify { return Verify(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { + return Perform(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), performs: perform) + } } public func given(_ method: Given) { diff --git a/Discovery/DiscoveryTests/Presentation/CourseDetailsViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/CourseDetailsViewModelTests.swift index ba30a66ce..35d61d57e 100644 --- a/Discovery/DiscoveryTests/Presentation/CourseDetailsViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/CourseDetailsViewModelTests.swift @@ -44,7 +44,8 @@ final class CourseDetailsViewModelTests: XCTestCase { isEnrolled: true, overviewHTML: "", courseBannerURL: "", - courseVideoURL: nil + courseVideoURL: nil, + courseRawImage: nil ) @@ -90,7 +91,8 @@ final class CourseDetailsViewModelTests: XCTestCase { isEnrolled: true, overviewHTML: "", courseBannerURL: "", - courseVideoURL: nil + courseVideoURL: nil, + courseRawImage: nil ) Given(interactor, .getLoadedCourseDetails(courseID: "123", diff --git a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift index 30b6b5706..b41d901be 100644 --- a/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/DiscoveryViewModelTests.swift @@ -38,26 +38,32 @@ final class DiscoveryViewModelTests: XCTestCase { org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 2), + coursesCount: 2, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 2) + coursesCount: 2, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0) ] viewModel.courses = items + items + items viewModel.totalPages = 2 @@ -87,26 +93,32 @@ final class DiscoveryViewModelTests: XCTestCase { org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 0), + coursesCount: 0, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 0) + coursesCount: 0, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0) ] Given(interactor, .discovery(page: 1, willReturn: items)) @@ -135,26 +147,32 @@ final class DiscoveryViewModelTests: XCTestCase { org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 2), + coursesCount: 2, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 2) + coursesCount: 2, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0) ] Given(connectivity, .isInternetAvaliable(getter: false)) diff --git a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift index 3baf45321..aac3408c5 100644 --- a/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift +++ b/Discovery/DiscoveryTests/Presentation/SearchViewModelTests.swift @@ -31,7 +31,8 @@ final class SearchViewModelTests: XCTestCase { interactor: interactor, connectivity: connectivity, router: router, - analytics: analytics, + analytics: analytics, + storage: CoreStorageMock(), debounce: .test ) @@ -40,26 +41,32 @@ final class SearchViewModelTests: XCTestCase { org: "org", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "123", numPages: 2, - coursesCount: 0), + coursesCount: 0, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0), CourseItem(name: "Test2", org: "org2", shortDescription: "", imageURL: "", - isActive: true, + hasAccess: true, courseStart: Date(), courseEnd: nil, enrollmentStart: Date(), enrollmentEnd: Date(), courseID: "1243", numPages: 1, - coursesCount: 0) + coursesCount: 0, + courseRawImage: nil, + progressEarned: 0, + progressPossible: 0) ] Given(interactor, .search(page: 1, searchTerm: .any, willReturn: items)) @@ -90,6 +97,7 @@ final class SearchViewModelTests: XCTestCase { connectivity: connectivity, router: router, analytics: analytics, + storage: CoreStorageMock(), debounce: .test ) @@ -118,6 +126,7 @@ final class SearchViewModelTests: XCTestCase { connectivity: connectivity, router: router, analytics: analytics, + storage: CoreStorageMock(), debounce: .test ) @@ -151,6 +160,7 @@ final class SearchViewModelTests: XCTestCase { connectivity: connectivity, router: router, analytics: analytics, + storage: CoreStorageMock(), debounce: .test ) diff --git a/Discovery/Mockfile b/Discovery/Mockfile index 638dccd32..dcd8b112b 100644 --- a/Discovery/Mockfile +++ b/Discovery/Mockfile @@ -14,4 +14,5 @@ unit.tests.mock: - Discovery - Foundation - SwiftUI - - Combine \ No newline at end of file + - Combine + - OEXFoundation \ No newline at end of file diff --git a/Discussion/Discussion.xcodeproj/project.pbxproj b/Discussion/Discussion.xcodeproj/project.pbxproj index 2f63c3d88..c0ee0a64c 100644 --- a/Discussion/Discussion.xcodeproj/project.pbxproj +++ b/Discussion/Discussion.xcodeproj/project.pbxproj @@ -60,6 +60,8 @@ 9FC0EF907C0334E383C300C4 /* Pods_App_Discussion_DiscussionTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C40A586C6164140DC2079231 /* Pods_App_Discussion_DiscussionTests.framework */; }; BA3C45672BA9E13000672C96 /* Data_DiscussionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3C45662BA9E13000672C96 /* Data_DiscussionInfo.swift */; }; BA3C45692BA9E18D00672C96 /* DiscussionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3C45682BA9E18D00672C96 /* DiscussionInfo.swift */; }; + CE7CAF472CC1566D00E0AC9D /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CE7CAF462CC1566D00E0AC9D /* OEXFoundation */; }; + CEB1E2762CC14ED700921517 /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CEB1E2752CC14ED700921517 /* OEXFoundation */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -72,6 +74,29 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + CE7CAF452CC1564E00E0AC9D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + CE7CAF492CC1566D00E0AC9D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 0201771B29883E96003AC5EF /* ThreadViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewModelTests.swift; sourceTree = ""; }; 020767652989393200B976DE /* ResponsesViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponsesViewModelTests.swift; sourceTree = ""; }; @@ -107,7 +132,6 @@ 02D1267528F76F5D00C8E689 /* DiscussionTopicsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionTopicsView.swift; sourceTree = ""; }; 02D1267728F76FF200C8E689 /* DiscussionTopicsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionTopicsViewModel.swift; sourceTree = ""; }; 02E4F18029A8C2FD00F31684 /* DiscussionSearchTopicsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionSearchTopicsViewModelTests.swift; sourceTree = ""; }; - 02ED50D029A64BBF008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02ED50D129A64BBF008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 02F175382A4DD5AA0019CD70 /* DiscussionAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionAnalytics.swift; sourceTree = ""; }; 02F28A5D28FF23E700AFDE1B /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = ""; }; @@ -153,6 +177,7 @@ buildActionMask = 2147483647; files = ( 0218195928F7347200202564 /* Core.framework in Frameworks */, + CEB1E2762CC14ED700921517 /* OEXFoundation in Frameworks */, 7527943BE0D66C33B167A41A /* Pods_App_Discussion.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -162,6 +187,7 @@ buildActionMask = 2147483647; files = ( 0240D8D32987FE1F003CFE50 /* Discussion.framework in Frameworks */, + CE7CAF472CC1566D00E0AC9D /* OEXFoundation in Frameworks */, 9FC0EF907C0334E383C300C4 /* Pods_App_Discussion_DiscussionTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -486,6 +512,7 @@ 0218194928F7344A00202564 /* Sources */, 0218194A28F7344A00202564 /* Frameworks */, 0218194B28F7344A00202564 /* Resources */, + CE7CAF452CC1564E00E0AC9D /* Embed Frameworks */, ); buildRules = ( ); @@ -505,6 +532,7 @@ 0240D8CC2987FE1F003CFE50 /* Frameworks */, 0240D8CD2987FE1F003CFE50 /* Resources */, 3607DC97DFD36C230ED0AB74 /* [CP] Copy Pods Resources */, + CE7CAF492CC1566D00E0AC9D /* Embed Frameworks */, ); buildRules = ( ); @@ -545,6 +573,9 @@ uk, ); mainGroup = 0218194328F7344A00202564; + packageReferences = ( + CEB1E2742CC14ED700921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */, + ); productRefGroup = 0218194E28F7344A00202564 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -733,7 +764,6 @@ isa = PBXVariantGroup; children = ( 0218196128F734CD00202564 /* en */, - 02ED50D029A64BBF008341CD /* uk */, ); name = Localizable.strings; sourceTree = ""; @@ -886,7 +916,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -920,7 +950,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1017,7 +1047,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1115,7 +1145,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1207,7 +1237,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1298,7 +1328,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1325,7 +1355,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1346,7 +1376,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1367,7 +1397,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1388,7 +1418,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1409,7 +1439,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1430,7 +1460,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1521,7 +1551,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1549,7 +1579,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1634,7 +1664,7 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1661,7 +1691,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.DiscussionTests; @@ -1723,6 +1753,30 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + CEB1E2742CC14ED700921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; + requirement = { + kind = exactVersion; + version = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + CE7CAF462CC1566D00E0AC9D /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E2742CC14ED700921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; + CEB1E2752CC14ED700921517 /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E2742CC14ED700921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 0218194428F7344A00202564 /* Project object */; } diff --git a/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift b/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift index 0665d1cac..995917921 100644 --- a/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift +++ b/Discussion/Discussion/Data/Model/Data_CommentsResponse.swift @@ -48,6 +48,7 @@ public extension DataLayer { public let childCount: Int public let children: [String] public let users: Users? + public let profileImage: ProfileImage? enum CodingKeys: String, CodingKey { case id = "id" @@ -71,6 +72,7 @@ public extension DataLayer { case childCount = "child_count" case children = "children" case users + case profileImage = "profile_image" } public init( @@ -94,7 +96,8 @@ public extension DataLayer { endorsedAt: String?, childCount: Int, children: [String], - users: Users? + users: Users?, + profileImage: ProfileImage? ) { self.id = id self.author = author @@ -117,6 +120,7 @@ public extension DataLayer { self.childCount = childCount self.children = children self.users = users + self.profileImage = profileImage } } } @@ -125,7 +129,7 @@ public extension DataLayer.Comments { var domain: UserComment { UserComment( authorName: author ?? DiscussionLocalization.anonymous, - authorAvatar: users?.userName?.profile?.image?.imageURLLarge ?? "", + authorAvatar: users?.userName?.profile?.image?.imageURLLarge ?? profileImage?.imageURLFull ?? "", postDate: Date(iso8601: createdAt), postTitle: "", postBody: rawBody, diff --git a/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift b/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift index 4409f22dc..efa48f022 100644 --- a/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift +++ b/Discussion/Discussion/Data/Network/DiscussionEndpoint.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import Alamofire enum DiscussionEndpoint: EndPointType { @@ -186,7 +187,10 @@ enum DiscussionEndpoint: EndPointType { } return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) case .getThread: - return .requestParameters(parameters: [:], encoding: URLEncoding.queryString) + return .requestParameters( + parameters: ["requested_fields": "profile_image"], + encoding: URLEncoding.queryString + ) case .getTopics: return .requestParameters(encoding: URLEncoding.queryString) case let .getTopic(_, topicID): diff --git a/Discussion/Discussion/Data/Network/DiscussionRepository.swift b/Discussion/Discussion/Data/Network/DiscussionRepository.swift index cc56f88c7..ce8b8170a 100644 --- a/Discussion/Discussion/Data/Network/DiscussionRepository.swift +++ b/Discussion/Discussion/Data/Network/DiscussionRepository.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import Combine public protocol DiscussionRepositoryProtocol { @@ -32,9 +33,6 @@ public protocol DiscussionRepositoryProtocol { func followThread(following: Bool, threadID: String) async throws func createNewThread(newThread: DiscussionNewThread) async throws func readBody(threadID: String) async throws - func renameThreadUser(data: Data) async throws -> DataLayer.ThreadListsResponse - func renameUsers(data: Data) async throws -> DataLayer.CommentsResponse - func renameUsersInJSON(stringJSON: String) -> String } public class DiscussionRepository: DiscussionRepositoryProtocol { @@ -66,21 +64,20 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { let threads = try await api.requestData(DiscussionEndpoint .getThreads(courseID: courseID, type: type, sort: sort, filter: filter, page: page)) - return try await renameThreadUser(data: threads).domain + return try await renameThreadListUser(data: threads).domain } public func getThread(threadID: String) async throws -> UserThread { let thread = try await api.requestData(DiscussionEndpoint .getThread(threadID: threadID)) - .mapResponse(DataLayer.ThreadList.self) - return thread.userThread + return try await renameThreadUser(data: thread).userThread } public func searchThreads(courseID: String, searchText: String, pageNumber: Int) async throws -> ThreadLists { let posts = try await api.requestData(DiscussionEndpoint.searchThreads(courseID: courseID, searchText: searchText, pageNumber: pageNumber)) - return try await renameThreadUser(data: posts).domain + return try await renameThreadListUser(data: posts).domain } public func getTopics(courseID: String) async throws -> Topics { @@ -158,7 +155,7 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { _ = try await api.request(DiscussionEndpoint.readBody(threadID: threadID)) } - public func renameThreadUser(data: Data) async throws -> DataLayer.ThreadListsResponse { + private func renameThreadListUser(data: Data) async throws -> DataLayer.ThreadListsResponse { var modifiedJSON = "" let parsed = try data.mapResponse(DataLayer.ThreadListsResponse.self) @@ -176,7 +173,25 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { } } - public func renameUsers(data: Data) async throws -> DataLayer.CommentsResponse { + private func renameThreadUser(data: Data) async throws -> DataLayer.ThreadList { + var modifiedJSON = "" + let parsed = try data.mapResponse(DataLayer.ThreadList.self) + + if let stringJSON = String(data: data, encoding: .utf8) { + modifiedJSON = renameUsersInJSON(stringJSON: stringJSON) + if let modifiedParsed = try modifiedJSON.data(using: .utf8)?.mapResponse( + DataLayer.ThreadList.self + ) { + return modifiedParsed + } else { + return parsed + } + } else { + return parsed + } + } + + private func renameUsers(data: Data) async throws -> DataLayer.CommentsResponse { var modifiedJSON = "" let parsed = try data.mapResponse(DataLayer.CommentsResponse.self) @@ -192,7 +207,7 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { } } - public func renameUsersInJSON(stringJSON: String) -> String { + private func renameUsersInJSON(stringJSON: String) -> String { var modifiedJSON = stringJSON let userNames = stringJSON.find(from: "\"users\":{\"", to: "\":{\"profile\":") if userNames.count >= 1 { @@ -210,8 +225,8 @@ public class DiscussionRepository: DiscussionRepositoryProtocol { } // Mark - For testing and SwiftUI preview -#if DEBUG // swiftlint:disable all +#if DEBUG public class DiscussionRepositoryMock: DiscussionRepositoryProtocol { public func getCourseDiscussionInfo(courseID: String) async throws -> DiscussionInfo { @@ -478,42 +493,6 @@ public class DiscussionRepositoryMock: DiscussionRepositoryProtocol { public func readBody(threadID: String) async throws { } - - public func renameThreadUser(data: Data) async throws -> DataLayer.ThreadListsResponse { - DataLayer.ThreadListsResponse(threads: [], - textSearchRewrite: "", - pagination: DataLayer.Pagination(next: "", previous: "", count: 0, numPages: 0) ) - } - - public func renameUsers(data: Data) async throws -> DataLayer.CommentsResponse { - DataLayer.CommentsResponse( - comments: [ - DataLayer.Comments(id: "", author: "Bill", - authorLabel: nil, - createdAt: "25.11.2043", - updatedAt: "25.11.2043", - rawBody: "Raw Body", - renderedBody: "Rendered body", - abuseFlagged: false, - voted: true, - voteCount: 2, - editableFields: [], - canDelete: true, - threadID: "", - parentID: nil, - endorsed: false, - endorsedBy: nil, - endorsedByLabel: nil, - endorsedAt: nil, - childCount: 0, - children: [], - users: nil) - ], pagination: DataLayer.Pagination(next: nil, previous: nil, count: 0, numPages: 0)) - } - - public func renameUsersInJSON(stringJSON: String) -> String { - return stringJSON - } } -// swiftlint:enable all #endif +// swiftlint:enable all diff --git a/Discussion/Discussion/Domain/Model/UserThread.swift b/Discussion/Discussion/Domain/Model/UserThread.swift index 81e8dcf0c..f28e2a532 100644 --- a/Discussion/Discussion/Domain/Model/UserThread.swift +++ b/Discussion/Discussion/Domain/Model/UserThread.swift @@ -49,7 +49,7 @@ public struct UserThread { renderedBody: String, voted: Bool, voteCount: Int, - courseID: String, + courseID: String, type: PostType, title: String, pinned: Bool, @@ -87,19 +87,24 @@ public struct UserThread { } public extension UserThread { - func discussionPost(action: @escaping () -> Void) -> DiscussionPost { - return DiscussionPost(id: id, - title: title, - replies: commentCount, - lastPostDate: updatedAt, - lastPostDateFormatted: updatedAt.dateToString(style: .lastPost), - isFavorite: following, - type: type, - unreadCommentCount: unreadCommentCount, - action: action, - hasEndorsed: hasEndorsed, - voteCount: voteCount, - numPages: numPages) + func discussionPost(useRelativeDates: Bool, action: @escaping () -> Void) -> DiscussionPost { + return DiscussionPost( + id: id, + title: title, + replies: commentCount, + lastPostDate: updatedAt, + lastPostDateFormatted: updatedAt.dateToString( + style: .lastPost, + useRelativeDates: useRelativeDates + ), + isFavorite: following, + type: type, + unreadCommentCount: unreadCommentCount, + action: action, + hasEndorsed: hasEndorsed, + voteCount: voteCount, + numPages: numPages + ) } } diff --git a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift index 4a955aa2e..eacf00109 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/BaseResponsesViewModel.swift @@ -44,16 +44,19 @@ public class BaseResponsesViewModel { internal let interactor: DiscussionInteractorProtocol internal let router: DiscussionRouter internal let config: ConfigProtocol + internal let storage: CoreStorage internal let addPostSubject = CurrentValueSubject(nil) init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, - config: ConfigProtocol + config: ConfigProtocol, + storage: CoreStorage ) { self.interactor = interactor self.router = router self.config = config + self.storage = storage } @MainActor diff --git a/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift b/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift index ff11baedd..f4c56c102 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/CommentCell.swift @@ -14,6 +14,7 @@ public struct CommentCell: View { private let comment: Post private let addCommentAvailable: Bool + private let useRelativeDates: Bool private var onAvatarTap: ((String) -> Void) private var onLikeTap: (() -> Void) private var onReportTap: (() -> Void) @@ -26,6 +27,7 @@ public struct CommentCell: View { public init( comment: Post, addCommentAvailable: Bool, + useRelativeDates: Bool, leftLineEnabled: Bool = false, onAvatarTap: @escaping (String) -> Void, onLikeTap: @escaping () -> Void, @@ -35,6 +37,7 @@ public struct CommentCell: View { ) { self.comment = comment self.addCommentAvailable = addCommentAvailable + self.useRelativeDates = useRelativeDates self.leftLineEnabled = leftLineEnabled self.onAvatarTap = onAvatarTap self.onLikeTap = onLikeTap @@ -59,7 +62,7 @@ public struct CommentCell: View { VStack(alignment: .leading) { Text(comment.authorName) .font(Theme.Fonts.titleSmall) - Text(comment.postDate.dateToString(style: .lastPost)) + Text(comment.postDate.dateToString(style: .lastPost, useRelativeDates: useRelativeDates)) .font(Theme.Fonts.labelSmall) .foregroundColor(Theme.Colors.textSecondary) } @@ -118,13 +121,13 @@ public struct CommentCell: View { onLikeTap() }, label: { comment.voted - ? CoreAssets.voted.swiftUIImage - : CoreAssets.vote.swiftUIImage + ? (CoreAssets.voted.swiftUIImage.renderingMode(.template)) + : CoreAssets.vote.swiftUIImage.renderingMode(.template) Text("\(comment.votesCount)") Text(DiscussionLocalization.votesCount(comment.votesCount)) .font(Theme.Fonts.labelLarge) }).foregroundColor(comment.voted - ? Theme.Colors.accentColor + ? Theme.Colors.accentXColor : Theme.Colors.textSecondary) Spacer() @@ -179,15 +182,19 @@ struct CommentView_Previews: PreviewProvider { CommentCell( comment: comment, addCommentAvailable: true, + useRelativeDates: true, leftLineEnabled: false, - onAvatarTap: {_ in}, + onAvatarTap: { + _ in + }, onLikeTap: {}, onReportTap: {}, onCommentsTap: {}, onFetchMore: {}) CommentCell( comment: comment, - addCommentAvailable: true, + addCommentAvailable: true, + useRelativeDates: true, leftLineEnabled: false, onAvatarTap: {_ in}, onLikeTap: {}, @@ -202,7 +209,8 @@ struct CommentView_Previews: PreviewProvider { VStack(spacing: 0) { CommentCell( comment: comment, - addCommentAvailable: true, + addCommentAvailable: true, + useRelativeDates: true, leftLineEnabled: false, onAvatarTap: {_ in}, onLikeTap: {}, @@ -211,7 +219,8 @@ struct CommentView_Previews: PreviewProvider { onFetchMore: {}) CommentCell( comment: comment, - addCommentAvailable: true, + addCommentAvailable: true, + useRelativeDates: true, leftLineEnabled: false, onAvatarTap: {_ in}, onLikeTap: {}, diff --git a/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift b/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift index 23aae3d52..3fce895d6 100644 --- a/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift +++ b/Discussion/Discussion/Presentation/Comments/Base/ParentCommentView.swift @@ -14,6 +14,7 @@ public struct ParentCommentView: View { private let comments: Post private var isThread: Bool + private let useRelativeDates: Bool private var onAvatarTap: ((String) -> Void) private var onLikeTap: (() -> Void) private var onReportTap: (() -> Void) @@ -24,6 +25,7 @@ public struct ParentCommentView: View { public init( comments: Post, isThread: Bool, + useRelativeDates: Bool, onAvatarTap: @escaping (String) -> Void, onLikeTap: @escaping () -> Void, onReportTap: @escaping () -> Void, @@ -31,6 +33,7 @@ public struct ParentCommentView: View { ) { self.comments = comments self.isThread = isThread + self.useRelativeDates = useRelativeDates self.onAvatarTap = onAvatarTap self.onLikeTap = onLikeTap self.onReportTap = onReportTap @@ -55,7 +58,7 @@ public struct ParentCommentView: View { .font(Theme.Fonts.titleMedium) .foregroundColor(Theme.Colors.textPrimary) Text(comments.postDate - .dateToString(style: .lastPost)) + .dateToString(style: .lastPost, useRelativeDates: useRelativeDates)) .font(Theme.Fonts.labelSmall) .foregroundColor(Theme.Colors.textSecondaryLight) } @@ -107,15 +110,15 @@ public struct ParentCommentView: View { onLikeTap() }, label: { comments.voted - ? CoreAssets.voted.swiftUIImage - : CoreAssets.vote.swiftUIImage + ? (CoreAssets.voted.swiftUIImage.renderingMode(.template)) + : (CoreAssets.vote.swiftUIImage.renderingMode(.template)) Text("\(comments.votesCount)") .foregroundColor(Theme.Colors.textPrimary) Text(DiscussionLocalization.votesCount(comments.votesCount)) .font(Theme.Fonts.labelLarge) .foregroundColor(Theme.Colors.textPrimary) }).foregroundColor(comments.voted - ? Theme.Colors.accentColor + ? Theme.Colors.accentXColor : Theme.Colors.textSecondaryLight) Spacer() Button(action: { @@ -169,7 +172,8 @@ struct ParentCommentView_Previews: PreviewProvider { return VStack { ParentCommentView( comments: comment, - isThread: true, + isThread: true, + useRelativeDates: true, onAvatarTap: {_ in}, onLikeTap: {}, onReportTap: {}, diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift index d4dab6464..99c677cd2 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesView.swift @@ -9,6 +9,7 @@ import SwiftUI import Core import Combine import Theme +import OEXFoundation public struct ResponsesView: View { @@ -19,7 +20,7 @@ public struct ResponsesView: View { @ObservedObject private var viewModel: ResponsesViewModel @State private var isShowProgress: Bool = true - + public init( commentID: String, viewModel: ResponsesViewModel, @@ -46,20 +47,14 @@ public struct ResponsesView: View { ScrollViewReader { scroll in VStack { ZStack(alignment: .top) { - RefreshableScrollViewCompat(action: { - viewModel.comments = [] - _ = await viewModel.getResponsesData( - commentID: commentID, - parentComment: parentComment, - page: 1, - refresh: true - ) - }) { + ScrollView { VStack { if let comments = viewModel.postComments { ParentCommentView( comments: comments, - isThread: false, onAvatarTap: { username in + isThread: false, + useRelativeDates: viewModel.storage.useRelativeDates, + onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, onLikeTap: { @@ -99,12 +94,15 @@ public struct ResponsesView: View { .padding(.leading, 24) .font(Theme.Fonts.titleMedium) .foregroundColor(Theme.Colors.textPrimary) + let useRelativeDates = viewModel.storage.useRelativeDates ForEach( Array(comments.comments.enumerated()), id: \.offset ) { index, comment in CommentCell( comment: comment, - addCommentAvailable: false, leftLineEnabled: true, + addCommentAvailable: false, + useRelativeDates: useRelativeDates, + leftLineEnabled: true, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, @@ -157,6 +155,17 @@ public struct ResponsesView: View { } .frameLimit(width: proxy.size.width) } + .refreshable { + viewModel.comments = [] + Task { + _ = await viewModel.getResponsesData( + commentID: commentID, + parentComment: parentComment, + page: 1, + refresh: true + ) + } + } if !(parentComment.closed || viewModel.isBlackedOut) { FlexibleKeyboardInputView( hint: DiscussionLocalization.Response.addComment, @@ -238,6 +247,7 @@ struct ResponsesView_Previews: PreviewProvider { interactor: DiscussionInteractor(repository: DiscussionRepositoryMock()), router: DiscussionRouterMock(), config: ConfigMock(), + storage: CoreStorageMock(), threadStateSubject: .init(nil) ) let post = Post( diff --git a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift index c8992fd8a..b73697264 100644 --- a/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Responses/ResponsesViewModel.swift @@ -20,10 +20,11 @@ public class ResponsesViewModel: BaseResponsesViewModel, ObservableObject { interactor: DiscussionInteractorProtocol, router: DiscussionRouter, config: ConfigProtocol, + storage: CoreStorage, threadStateSubject: CurrentValueSubject ) { self.threadStateSubject = threadStateSubject - super.init(interactor: interactor, router: router, config: config) + super.init(interactor: interactor, router: router, config: config, storage: storage) } func generateCommentsResponses(comments: [UserComment], parentComment: Post) -> Post? { diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift index 84f2b6816..6b6d7a689 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Theme public struct ThreadView: View { @@ -36,14 +37,13 @@ public struct ThreadView: View { ScrollViewReader { scroll in VStack { ZStack(alignment: .top) { - RefreshableScrollViewCompat(action: { - _ = await viewModel.getThreadData(thread: thread, page: 1, refresh: true) - }) { + ScrollView { VStack { if let comments = viewModel.postComments { ParentCommentView( comments: comments, isThread: true, + useRelativeDates: viewModel.storage.useRelativeDates, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, @@ -92,11 +92,13 @@ public struct ThreadView: View { .padding(.leading, 24) .font(Theme.Fonts.titleMedium) .foregroundColor(Theme.Colors.textPrimary) + let useRelativeDates = viewModel.storage.useRelativeDates ForEach(Array(comments.comments.enumerated()), id: \.offset) { index, comment in CommentCell( comment: comment, addCommentAvailable: true, + useRelativeDates: useRelativeDates, onAvatarTap: { username in viewModel.router.showUserDetails(username: username) }, @@ -154,6 +156,11 @@ public struct ThreadView: View { } .frameLimit(width: proxy.size.width) } + .refreshable { + Task { + _ = await viewModel.getThreadData(thread: thread, page: 1, refresh: true) + } + } if !(thread.closed || viewModel.isBlackedOut) { FlexibleKeyboardInputView( hint: DiscussionLocalization.Thread.addResponse, @@ -210,7 +217,7 @@ public struct ThreadView: View { Text(viewModel.alertMessage ?? "") .shadowCardStyle( bgColor: Theme.Colors.accentColor, - textColor: Theme.Colors.white + textColor: Theme.Colors.primaryButtonTextColor ) .padding(.top, 80) Spacer() @@ -281,10 +288,13 @@ struct CommentsView_Previews: PreviewProvider { abuseFlagged: true, hasEndorsed: true, numPages: 3) - let vm = ThreadViewModel(interactor: DiscussionInteractor.mock, - router: DiscussionRouterMock(), - config: ConfigMock(), - postStateSubject: .init(nil)) + let vm = ThreadViewModel( + interactor: DiscussionInteractor.mock, + router: DiscussionRouterMock(), + config: ConfigMock(), + storage: CoreStorageMock(), + postStateSubject: .init(nil) + ) ThreadView(thread: userThread, viewModel: vm) .preferredColorScheme(.light) diff --git a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift index 2fb75b60f..452ea98f2 100644 --- a/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift +++ b/Discussion/Discussion/Presentation/Comments/Thread/ThreadViewModel.swift @@ -16,17 +16,17 @@ public class ThreadViewModel: BaseResponsesViewModel, ObservableObject { internal let threadStateSubject = CurrentValueSubject(nil) private var cancellable: AnyCancellable? private let postStateSubject: CurrentValueSubject - public var isBlackedOut: Bool = false public init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, config: ConfigProtocol, + storage: CoreStorage, postStateSubject: CurrentValueSubject ) { self.postStateSubject = postStateSubject - super.init(interactor: interactor, router: router, config: config) + super.init(interactor: interactor, router: router, config: config, storage: storage) cancellable = threadStateSubject .receive(on: RunLoop.main) diff --git a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift index 7c676df8d..39aa77378 100644 --- a/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift +++ b/Discussion/Discussion/Presentation/CreateNewThread/CreateNewThreadView.swift @@ -141,7 +141,7 @@ public struct CreateNewThreadView: View { .padding(.horizontal, 10) .padding(.vertical, 10) .frame(height: 200) - .hideScrollContentBackground() + .scrollContentBackground(.hidden) .background( Theme.Shapes.textInputShape .fill(Theme.Colors.textInputBackground) diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift index c9fd88dd7..e92727f89 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Theme public struct DiscussionSearchTopicsView: View { @@ -195,7 +196,8 @@ struct DiscussionSearchTopicsView_Previews: PreviewProvider { static var previews: some View { let vm = DiscussionSearchTopicsViewModel( courseID: "123", - interactor: DiscussionInteractor.mock, + interactor: DiscussionInteractor.mock, + storage: CoreStorageMock(), router: DiscussionRouterMock(), debounce: .searchDebounce ) diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift index 9b87fee37..83b7b2741 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModel.swift @@ -39,16 +39,19 @@ public class DiscussionSearchTopicsViewModel: ObservableObject { let router: DiscussionRouter private let interactor: DiscussionInteractorProtocol + private let storage: CoreStorage private let debounce: Debounce public init( courseID: String, interactor: DiscussionInteractorProtocol, + storage: CoreStorage, router: DiscussionRouter, debounce: Debounce ) { self.courseID = courseID self.interactor = interactor + self.storage = storage self.router = router self.debounce = debounce @@ -157,11 +160,11 @@ public class DiscussionSearchTopicsViewModel: ObservableObject { private func generatePosts(threads: [UserThread]) -> [DiscussionPost] { var result: [DiscussionPost] = [] for thread in threads { - result.append(thread.discussionPost(action: { [weak self] in + result.append(thread.discussionPost(useRelativeDates: storage.useRelativeDates, action: { [weak self] in guard let self else { return } self.router.showThread( thread: thread, - postStateSubject: self.postStateSubject, + postStateSubject: self.postStateSubject, isBlackedOut: false, animated: true ) diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift index ad84d6a00..61106f87a 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsView.swift @@ -17,12 +17,14 @@ public struct DiscussionTopicsView: View { private let courseID: String @Binding private var coordinate: CGFloat @Binding private var collapsed: Bool + @Binding private var viewHeight: CGFloat @State private var runOnce: Bool = false public init( courseID: String, coordinate: Binding, collapsed: Binding, + viewHeight: Binding, viewModel: DiscussionTopicsViewModel, router: DiscussionRouter ) { @@ -30,6 +32,7 @@ public struct DiscussionTopicsView: View { self.courseID = courseID self._coordinate = coordinate self._collapsed = collapsed + self._viewHeight = viewHeight self.router = router } @@ -37,137 +40,154 @@ public struct DiscussionTopicsView: View { GeometryReader { proxy in ZStack(alignment: .center) { VStack(alignment: .center) { - RefreshableScrollViewCompat(action: { - await viewModel.getTopics(courseID: self.courseID, withProgress: false) - }) { - DynamicOffsetView( - coordinate: $coordinate, - collapsed: $collapsed - ) - RefreshProgressView(isShowRefresh: $viewModel.isShowRefresh) - // MARK: - Search fake field - if viewModel.isBlackedOut { - bannerDiscussionsDisabled - } - - HStack(spacing: 11) { - Image(systemName: "magnifyingglass") - .foregroundColor(Theme.Colors.textInputTextColor) - .padding(.leading, 16) - .padding(.top, 1) - Text(DiscussionLocalization.Topics.search) - .foregroundColor(Theme.Colors.textInputTextColor) - .font(Theme.Fonts.bodyMedium) - Spacer() - } - .frame(minHeight: 48) - .background( - Theme.Shapes.textInputShape - .fill(Theme.Colors.textInputBackground) - ) - .overlay( - Theme.Shapes.textInputShape - .stroke(lineWidth: 1) - .fill(Theme.Colors.textInputUnfocusedStroke) - ) - .onTapGesture { - viewModel.router.showDiscussionsSearch( - courseID: courseID, - isBlackedOut: viewModel.isBlackedOut + ScrollView { + VStack(spacing: 0) { + DynamicOffsetView( + coordinate: $coordinate, + collapsed: $collapsed, + viewHeight: $viewHeight ) - } - .frameLimit(width: proxy.size.width) - .padding(.horizontal, 24) - .padding(.top, 10) - .accessibilityElement(children: .ignore) - .accessibilityLabel(DiscussionLocalization.Topics.search) - - // MARK: - Page Body - VStack { - ZStack(alignment: .top) { - VStack { - if let topics = viewModel.discussionTopics { - HStack { - Text(DiscussionLocalization.Topics.mainCategories) - .font(Theme.Fonts.titleMedium) - .foregroundColor(Theme.Colors.textSecondary) - .padding(.horizontal, 24) - .padding(.top, 10) - Spacer() - } - HStack(spacing: 8) { - if let allTopics = topics.first(where: { - $0.name == DiscussionLocalization.Topics.allPosts }) { - Button(action: { - allTopics.action() - }, label: { - VStack { - Spacer(minLength: 0) - CoreAssets.allPosts.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.textPrimary) - Text(allTopics.name) - .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) - Spacer(minLength: 0) - } - .frame(maxWidth: .infinity) - }).cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground) - .padding(.trailing, -20) + RefreshProgressView(isShowRefresh: $viewModel.isShowRefresh) + // MARK: - Search fake field + if viewModel.isBlackedOut { + bannerDiscussionsDisabled + } + + if let topics = viewModel.discussionTopics, topics.count > 0 { + HStack(spacing: 11) { + Image(systemName: "magnifyingglass") + .foregroundColor(Theme.Colors.textInputTextColor) + .padding(.leading, 16) + .padding(.top, 1) + Text(DiscussionLocalization.Topics.search) + .foregroundColor(Theme.Colors.textInputTextColor) + .font(Theme.Fonts.bodyMedium) + Spacer() + } + .frame(minHeight: 48) + .background( + Theme.Shapes.textInputShape + .fill(Theme.Colors.textInputBackground) + ) + .overlay( + Theme.Shapes.textInputShape + .stroke(lineWidth: 1) + .fill(Theme.Colors.textInputUnfocusedStroke) + ) + .onTapGesture { + viewModel.router.showDiscussionsSearch( + courseID: courseID, + isBlackedOut: viewModel.isBlackedOut + ) + } + .frameLimit(width: proxy.size.width) + .padding(.horizontal, 24) + .padding(.top, 10) + .accessibilityElement(children: .ignore) + .accessibilityLabel(DiscussionLocalization.Topics.search) + } + + // MARK: - Page Body + VStack { + ZStack(alignment: .top) { + VStack { + if let topics = viewModel.discussionTopics { + HStack { + Text(DiscussionLocalization.Topics.mainCategories) + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textSecondary) + .padding(.horizontal, 24) + .padding(.top, 10) + Spacer() } - if let followed = topics.first(where: { - $0.name == DiscussionLocalization.Topics.postImFollowing}) { - Button(action: { - followed.action() - }, label: { - VStack(alignment: .center) { - Spacer(minLength: 0) - CoreAssets.followed.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.textPrimary) - Text(followed.name) - .font(Theme.Fonts.titleSmall) - .foregroundColor(Theme.Colors.textPrimary) - Spacer(minLength: 0) + HStack(spacing: 8) { + if let allTopics = topics.first(where: { + $0.name == DiscussionLocalization.Topics.allPosts }) { + Button(action: { + allTopics.action() + }, label: { + VStack { + Spacer(minLength: 0) + CoreAssets.allPosts.swiftUIImage.renderingMode(.template) + .foregroundColor(Theme.Colors.textPrimary) + Text(allTopics.name) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.textPrimary) + Spacer(minLength: 0) + } + .frame(maxWidth: .infinity) + }).cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground) + .padding(.trailing, -20) + } + if let followed = topics.first(where: { + $0.name == DiscussionLocalization.Topics.postImFollowing}) { + Button(action: { + followed.action() + }, label: { + VStack(alignment: .center) { + Spacer(minLength: 0) + CoreAssets.followed.swiftUIImage.renderingMode(.template) + .foregroundColor(Theme.Colors.textPrimary) + Text(followed.name) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.textPrimary) + Spacer(minLength: 0) + } + .frame(maxWidth: .infinity) + }).cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground) + .padding(.leading, -20) + + } + }.padding(.bottom, 16) + ForEach(Array(topics.enumerated()), id: \.offset) { _, topic in + if topic.name != DiscussionLocalization.Topics.allPosts + && topic.name != DiscussionLocalization.Topics.postImFollowing { + + if topic.style == .title { + HStack { + Text("\(topic.name):") + .font(Theme.Fonts.titleMedium) + .foregroundColor(Theme.Colors.textSecondary) + Spacer() + }.padding(.top, 12) + .padding(.bottom, 8) + .padding(.horizontal, 24) + } else { + VStack { + TopicCell(topic: topic) + .padding(.vertical, 10) + Divider() + }.padding(.horizontal, 24) } - .frame(maxWidth: .infinity) - }).cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground) - .padding(.leading, -20) - - } - }.padding(.bottom, 16) - ForEach(Array(topics.enumerated()), id: \.offset) { _, topic in - if topic.name != DiscussionLocalization.Topics.allPosts - && topic.name != DiscussionLocalization.Topics.postImFollowing { - - if topic.style == .title { - HStack { - Text("\(topic.name):") - .font(Theme.Fonts.titleMedium) - .foregroundColor(Theme.Colors.textSecondary) - Spacer() - }.padding(.top, 12) - .padding(.bottom, 8) - .padding(.horizontal, 24) - } else { - VStack { - TopicCell(topic: topic) - .padding(.vertical, 10) - Divider() - }.padding(.horizontal, 24) } } + } else if viewModel.isShowProgress == false { + FullScreenErrorView( + type: .noContent( + DiscussionLocalization.Error.unableToLoadDiscussion, + image: CoreAssets.information.swiftUIImage + ) + ) + .frame(maxWidth: .infinity) + .frame(height: proxy.size.height - viewHeight) + Spacer(minLength: -200) } - + Spacer(minLength: 200) } - Spacer(minLength: 200) + .frameLimit(width: proxy.size.width) } - .frameLimit(width: proxy.size.width) - } - .onRightSwipeGesture { - router.back() + .onRightSwipeGesture { + router.back() + } + } - } }.frame(maxWidth: .infinity) + .refreshable { + Task { + await viewModel.getTopics(courseID: self.courseID, withProgress: false) + } + } }.padding(.top, 8) if viewModel.isShowProgress { ProgressBar(size: 40, lineWidth: 8) @@ -222,6 +242,7 @@ struct DiscussionView_Previews: PreviewProvider { courseID: "", coordinate: .constant(0), collapsed: .constant(false), + viewHeight: .constant(0), viewModel: vm, router: router ) @@ -233,6 +254,7 @@ struct DiscussionView_Previews: PreviewProvider { courseID: "", coordinate: .constant(0), collapsed: .constant(false), + viewHeight: .constant(0), viewModel: vm, router: router ) @@ -263,6 +285,7 @@ public struct TopicCell: View { .multilineTextAlignment(.leading) Spacer() Image(systemName: "chevron.right") + .flipsForRightToLeftLayoutDirection(true) .foregroundColor(Theme.Colors.accentColor) } }) diff --git a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift index 51fde8edd..0984ebb6b 100644 --- a/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift +++ b/Discussion/Discussion/Presentation/DiscussionTopics/DiscussionTopicsViewModel.swift @@ -182,14 +182,10 @@ public class DiscussionTopicsViewModel: ObservableObject { discussionTopics = generateTopics(topics: topics) isShowProgress = false isShowRefresh = false - } catch let error { + } catch { isShowProgress = false isShowRefresh = false - if error.isInternetError { - errorMessage = CoreLocalization.Error.slowOrNoInternetConnection - } else { - errorMessage = CoreLocalization.Error.unknownError - } + discussionTopics = nil } } } diff --git a/Discussion/Discussion/Presentation/Posts/PostsView.swift b/Discussion/Discussion/Presentation/Posts/PostsView.swift index 9664d1f19..71912528e 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsView.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsView.swift @@ -102,13 +102,7 @@ public struct PostsView: View { Divider().offset(y: -8) } - RefreshableScrollViewCompat(action: { - viewModel.resetPosts() - _ = await viewModel.getPosts( - pageNumber: 1, - withProgress: false - ) - }) { + ScrollView { let posts = Array(viewModel.filteredPosts.enumerated()) if posts.count >= 1 { LazyVStack { @@ -209,6 +203,15 @@ public struct PostsView: View { } } } + .refreshable { + viewModel.resetPosts() + Task { + _ = await viewModel.getPosts( + pageNumber: 1, + withProgress: false + ) + } + } } .accessibilityAction {} .onRightSwipeGesture { @@ -326,7 +329,8 @@ struct PostsView_Previews: PreviewProvider { let vm = PostsViewModel( interactor: DiscussionInteractor.mock, router: router, - config: ConfigMock() + config: ConfigMock(), + storage: CoreStorageMock() ) PostsView(courseID: "course_id", diff --git a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift index b1676c70b..8115ea0f2 100644 --- a/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift +++ b/Discussion/Discussion/Presentation/Posts/PostsViewModel.swift @@ -80,17 +80,20 @@ public class PostsViewModel: ObservableObject { private let interactor: DiscussionInteractorProtocol private let router: DiscussionRouter private let config: ConfigProtocol + private let storage: CoreStorage internal let postStateSubject = CurrentValueSubject(nil) private var cancellable: AnyCancellable? public init( interactor: DiscussionInteractorProtocol, router: DiscussionRouter, - config: ConfigProtocol + config: ConfigProtocol, + storage: CoreStorage ) { self.interactor = interactor self.router = router self.config = config + self.storage = storage cancellable = postStateSubject .receive(on: RunLoop.main) @@ -130,17 +133,24 @@ public class PostsViewModel: ObservableObject { var result: [DiscussionPost] = [] if let threads = threads?.threads { for thread in threads { - result.append(thread.discussionPost(action: { [weak self] in - guard let self, let actualThread = self.threads.threads - .first(where: {$0.id == thread.id }) else { return } - - self.router.showThread( - thread: actualThread, - postStateSubject: self.postStateSubject, - isBlackedOut: self.isBlackedOut ?? false, - animated: true + result.append( + thread.discussionPost( + useRelativeDates: storage.useRelativeDates, + action: { + [weak self] in + guard let self, + let actualThread = self.threads.threads + .first(where: {$0.id == thread.id }) else { return } + + self.router.showThread( + thread: actualThread, + postStateSubject: self.postStateSubject, + isBlackedOut: self.isBlackedOut ?? false, + animated: true + ) + } ) - })) + ) } } diff --git a/Discussion/Discussion/SwiftGen/Strings.swift b/Discussion/Discussion/SwiftGen/Strings.swift index 487a95f22..3346df120 100644 --- a/Discussion/Discussion/SwiftGen/Strings.swift +++ b/Discussion/Discussion/SwiftGen/Strings.swift @@ -71,6 +71,11 @@ public enum DiscussionLocalization { /// Topic public static let topic = DiscussionLocalization.tr("Localizable", "CREATE_THREAD.TOPIC", fallback: "Topic") } + public enum Error { + /// Unable to load discussions. + /// Try again later. + public static let unableToLoadDiscussion = DiscussionLocalization.tr("Localizable", "ERROR.UNABLE_TO_LOAD_DISCUSSION", fallback: "Unable to load discussions.\nTry again later.") + } public enum Post { /// Last post: public static let lastPost = DiscussionLocalization.tr("Localizable", "POST.LAST_POST", fallback: "Last post:") diff --git a/Discussion/Discussion/en.lproj/Localizable.strings b/Discussion/Discussion/en.lproj/Localizable.strings index 55819f2ae..2c4165378 100644 --- a/Discussion/Discussion/en.lproj/Localizable.strings +++ b/Discussion/Discussion/en.lproj/Localizable.strings @@ -61,3 +61,5 @@ "SEARCH.EMPTY_DESCRIPTION" = "Start typing to find the topics"; "ANONYMOUS" = "Anonymous"; + +"ERROR.UNABLE_TO_LOAD_DISCUSSION" = "Unable to load discussions.\nTry again later."; diff --git a/Discussion/Discussion/uk.lproj/Localizable.strings b/Discussion/Discussion/uk.lproj/Localizable.strings deleted file mode 100644 index fc5223c14..000000000 --- a/Discussion/Discussion/uk.lproj/Localizable.strings +++ /dev/null @@ -1,59 +0,0 @@ -/* - Localizable.strings - Discussion - - Created by  Stepanok Ivan on 12.10.2022. - -*/ - -"TITLE" = "Дискусії"; -"BANNER.DISCUSSIONS_IS_DISABLED" = "Posting in discussions is disabled by the course team"; - -"TOPICS.SEARCH" = "Пошук по всім постам"; -"TOPICS.ALL_POSTS" = "Всі пости"; -"TOPICS.POST_IM_FOLLOWING" = "Улюблені пости"; -"TOPICS.MAIN_CATEGORIES" = "Основні категорії"; -"TOPICS.UNNAMED" = "Unnamed subcategory"; - -"POSTS.SORT.RECENT_ACTIVITY" = "Остання активність"; -"POSTS.SORT.MOST_ACTIVITY" = "Найактивниші"; -"POSTS.SORT.MOST_VOTES" = "Найбільше голосів"; - -"POSTS.FILTER.ALL_POSTS" = "Всі пости"; -"POSTS.FILTER.UNREAD" = "Непрочитаних"; -"POSTS.FILTER.UNANSWERED" = "Без відповіді"; -"POSTS.NO_DISCUSSION.TITLE" = "Ще немає дискусій"; -"POSTS.NO_DISCUSSION.DESCRIPTION" = "Натисніть кнопку нижче, щоб створити свою першу дискусію."; -"POSTS.NO_DISCUSSION.CREATEBUTTON" = "Створити дискусію"; -"POSTS.NO_DISCUSSION.ADD_POST" = "Add a post"; - -"POSTS.CREATE_NEW_POST" = "Створити новий пост"; -"POSTS.ALERT.MAKE_SELECTION" = "Оберіть"; - -"POST.LAST_POST" = "Останній пост:"; - -"THREAD.ALERT.COMMENT_ADDED" = "Коментарій додано"; -"THREAD.ADD_RESPONSE" = "Додати відповідь"; - -"CREATE_THREAD.NEW_POST" = "Створити новий пост"; -"CREATE_THREAD.SELECT_POST_TYPE" = "Обрати тип посту"; -"CREATE_THREAD.TOPIC" = "Тема"; -"CREATE_THREAD.TITLE" = "Назва"; -"CREATE_THREAD.FOLLOW_DISCUSSION" = "Підписатись на дискусію"; -"CREATE_THREAD.FOLLOW_QUESTION" = "Підписатись на питання"; -"CREATE_THREAD.CREATE_DISCUSSION" = "Створити нову дискусію"; -"CREATE_THREAD.CREATE_QUESTION" = "Створити нове питання"; - -"COMMENT.REPORT" = "Поскаржитись"; -"COMMENT.UNREPORT" = "Зняти скаргу"; -"COMMENT.FOLLOW" = "Слідкувати"; -"COMMENT.UNFOLLOW" = "Не слідкувати"; - -"RESPONSE.COMMENTS_RESPONSES" = "Коментарій"; -"RESPONSE.ALERT.COMMENT_ADDED" = "Коментарій додано"; -"RESPONSE.ADD_COMMENT" = "Додати коментарій"; - -"POST_TYPE.QUESTION" = "питання"; -"POST_TYPE.DISCUSSION" = "дискусія"; - -"ANONYMOUS" = "Анонім"; diff --git a/Discussion/DiscussionTests/DiscussionMock.generated.swift b/Discussion/DiscussionTests/DiscussionMock.generated.swift index d8324dd64..dbe21843f 100644 --- a/Discussion/DiscussionTests/DiscussionMock.generated.swift +++ b/Discussion/DiscussionTests/DiscussionMock.generated.swift @@ -13,6 +13,7 @@ import Discussion import Foundation import SwiftUI import Combine +import OEXFoundation // MARK: - AuthInteractorProtocol @@ -93,6 +94,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + open func login(ssoToken: String) throws -> User { + addInvocation(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) + let perform = methodPerformValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) as? (String) -> Void + perform?(`ssoToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(ssoToken: String). Use given") + Failure("Stub return value not specified for login(ssoToken: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -174,6 +191,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) + case m_login__ssoToken_ssoToken(Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields @@ -194,6 +212,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) return Matcher.ComparisonResult(results) + case (.m_login__ssoToken_ssoToken(let lhsSsotoken), .m_login__ssoToken_ssoToken(let rhsSsotoken)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSsotoken, rhs: rhsSsotoken, with: matcher), lhsSsotoken, rhsSsotoken, "ssoToken")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -224,6 +247,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue + case let .m_login__ssoToken_ssoToken(p0): return p0.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 @@ -235,6 +259,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" + case .m_login__ssoToken_ssoToken: return ".login(ssoToken:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" @@ -261,6 +286,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func login(ssoToken: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -297,6 +325,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + public static func login(ssoToken: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func login(ssoToken: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -356,6 +394,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} @discardableResult public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} + public static func login(ssoToken: Parameter) -> Verify { return Verify(method: .m_login__ssoToken_ssoToken(`ssoToken`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} @@ -375,6 +414,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) } + public static func login(ssoToken: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_login__ssoToken_ssoToken(`ssoToken`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -581,6 +623,12 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -619,6 +667,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showSSOWebBrowser__title_title(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -679,6 +728,11 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) return Matcher.ComparisonResult(results) + case (.m_showSSOWebBrowser__title_title(let lhsTitle), .m_showSSOWebBrowser__title_title(let rhsTitle)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -732,6 +786,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showSSOWebBrowser__title_title(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -752,6 +807,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showSSOWebBrowser__title_title: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -786,6 +842,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} + public static func showSSOWebBrowser(title: Parameter) -> Verify { return Verify(method: .m_showSSOWebBrowser__title_title(`title`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`))} @@ -832,6 +889,9 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showSSOWebBrowser__title_title(`title`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -919,9 +979,9 @@ open class BaseRouterMock: BaseRouter, Mock { } } -// MARK: - ConnectivityProtocol +// MARK: - CalendarManagerProtocol -open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { +open class CalendarManagerProtocolMock: CalendarManagerProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -959,51 +1019,176 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } - public var isInternetAvaliable: Bool { - get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } - } - private var __p_isInternetAvaliable: (Bool)? - public var isMobileData: Bool { - get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } - } - private var __p_isMobileData: (Bool)? - public var internetReachableSubject: CurrentValueSubject { - get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } - } - private var __p_internetReachableSubject: (CurrentValueSubject)? + open func createCalendarIfNeeded() { + addInvocation(.m_createCalendarIfNeeded) + let perform = methodPerformValue(.m_createCalendarIfNeeded) as? () -> Void + perform?() + } + + open func filterCoursesBySelected(fetchedCourses: [CourseForSync]) -> [CourseForSync] { + addInvocation(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) + let perform = methodPerformValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) as? ([CourseForSync]) -> Void + perform?(`fetchedCourses`) + var __value: [CourseForSync] + do { + __value = try methodReturnValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))).casted() + } catch { + onFatalFailure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + Failure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + } + return __value + } + + open func removeOldCalendar() { + addInvocation(.m_removeOldCalendar) + let perform = methodPerformValue(.m_removeOldCalendar) as? () -> Void + perform?() + } + + open func removeOutdatedEvents(courseID: String) { + addInvocation(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + } + + open func syncCourse(courseID: String, courseName: String, dates: CourseDates) { + addInvocation(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) + let perform = methodPerformValue(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) as? (String, String, CourseDates) -> Void + perform?(`courseID`, `courseName`, `dates`) + } + + open func requestAccess() -> Bool { + addInvocation(.m_requestAccess) + let perform = methodPerformValue(.m_requestAccess) as? () -> Void + perform?() + var __value: Bool + do { + __value = try methodReturnValue(.m_requestAccess).casted() + } catch { + onFatalFailure("Stub return value not specified for requestAccess(). Use given") + Failure("Stub return value not specified for requestAccess(). Use given") + } + return __value + } + + open func courseStatus(courseID: String) -> SyncStatus { + addInvocation(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: SyncStatus + do { + __value = try methodReturnValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch { + onFatalFailure("Stub return value not specified for courseStatus(courseID: String). Use given") + Failure("Stub return value not specified for courseStatus(courseID: String). Use given") + } + return __value + } + open func clearAllData(removeCalendar: Bool) { + addInvocation(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) + let perform = methodPerformValue(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) as? (Bool) -> Void + perform?(`removeCalendar`) + } + open func isDatesChanged(courseID: String, checksum: String) -> Bool { + addInvocation(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) + let perform = methodPerformValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) as? (String, String) -> Void + perform?(`courseID`, `checksum`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + Failure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + } + return __value + } fileprivate enum MethodType { - case p_isInternetAvaliable_get - case p_isMobileData_get - case p_internetReachableSubject_get + case m_createCalendarIfNeeded + case m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>) + case m_removeOldCalendar + case m_removeOutdatedEvents__courseID_courseID(Parameter) + case m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter, Parameter, Parameter) + case m_requestAccess + case m_courseStatus__courseID_courseID(Parameter) + case m_clearAllData__removeCalendar_removeCalendar(Parameter) + case m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match - case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match - case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + switch (lhs, rhs) { + case (.m_createCalendarIfNeeded, .m_createCalendarIfNeeded): return .match + + case (.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let lhsFetchedcourses), .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let rhsFetchedcourses)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFetchedcourses, rhs: rhsFetchedcourses, with: matcher), lhsFetchedcourses, rhsFetchedcourses, "fetchedCourses")) + return Matcher.ComparisonResult(results) + + case (.m_removeOldCalendar, .m_removeOldCalendar): return .match + + case (.m_removeOutdatedEvents__courseID_courseID(let lhsCourseid), .m_removeOutdatedEvents__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let lhsCourseid, let lhsCoursename, let lhsDates), .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let rhsCourseid, let rhsCoursename, let rhsDates)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDates, rhs: rhsDates, with: matcher), lhsDates, rhsDates, "dates")) + return Matcher.ComparisonResult(results) + + case (.m_requestAccess, .m_requestAccess): return .match + + case (.m_courseStatus__courseID_courseID(let lhsCourseid), .m_courseStatus__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_clearAllData__removeCalendar_removeCalendar(let lhsRemovecalendar), .m_clearAllData__removeCalendar_removeCalendar(let rhsRemovecalendar)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRemovecalendar, rhs: rhsRemovecalendar, with: matcher), lhsRemovecalendar, rhsRemovecalendar, "removeCalendar")) + return Matcher.ComparisonResult(results) + + case (.m_isDatesChanged__courseID_courseIDchecksum_checksum(let lhsCourseid, let lhsChecksum), .m_isDatesChanged__courseID_courseIDchecksum_checksum(let rhsCourseid, let rhsChecksum)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsChecksum, rhs: rhsChecksum, with: matcher), lhsChecksum, rhsChecksum, "checksum")) + return Matcher.ComparisonResult(results) default: return .none } } func intValue() -> Int { switch self { - case .p_isInternetAvaliable_get: return 0 - case .p_isMobileData_get: return 0 - case .p_internetReachableSubject_get: return 0 + case .m_createCalendarIfNeeded: return 0 + case let .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(p0): return p0.intValue + case .m_removeOldCalendar: return 0 + case let .m_removeOutdatedEvents__courseID_courseID(p0): return p0.intValue + case let .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case .m_requestAccess: return 0 + case let .m_courseStatus__courseID_courseID(p0): return p0.intValue + case let .m_clearAllData__removeCalendar_removeCalendar(p0): return p0.intValue + case let .m_isDatesChanged__courseID_courseIDchecksum_checksum(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { - case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" - case .p_isMobileData_get: return "[get] .isMobileData" - case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + case .m_createCalendarIfNeeded: return ".createCalendarIfNeeded()" + case .m_filterCoursesBySelected__fetchedCourses_fetchedCourses: return ".filterCoursesBySelected(fetchedCourses:)" + case .m_removeOldCalendar: return ".removeOldCalendar()" + case .m_removeOutdatedEvents__courseID_courseID: return ".removeOutdatedEvents(courseID:)" + case .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates: return ".syncCourse(courseID:courseName:dates:)" + case .m_requestAccess: return ".requestAccess()" + case .m_courseStatus__courseID_courseID: return ".courseStatus(courseID:)" + case .m_clearAllData__removeCalendar_removeCalendar: return ".clearAllData(removeCalendar:)" + case .m_isDatesChanged__courseID_courseIDchecksum_checksum: return ".isDatesChanged(courseID:checksum:)" } } } @@ -1016,30 +1201,94 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { super.init(products) } - public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willReturn: [CourseForSync]...) -> MethodStub { + return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func requestAccess(willReturn: Bool...) -> MethodStub { + return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { - return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func courseStatus(courseID: Parameter, willReturn: SyncStatus...) -> MethodStub { + return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willProduce: (Stubber<[CourseForSync]>) -> Void) -> MethodStub { + let willReturn: [[CourseForSync]] = [] + let given: Given = { return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseForSync]).self) + willProduce(stubber) + return given + } + public static func requestAccess(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given + } + public static func courseStatus(courseID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [SyncStatus] = [] + let given: Given = { return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (SyncStatus).self) + willProduce(stubber) + return given + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given } - } public struct Verify { fileprivate var method: MethodType - public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } - public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } - public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + public static func createCalendarIfNeeded() -> Verify { return Verify(method: .m_createCalendarIfNeeded)} + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>) -> Verify { return Verify(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`))} + public static func removeOldCalendar() -> Verify { return Verify(method: .m_removeOldCalendar)} + public static func removeOutdatedEvents(courseID: Parameter) -> Verify { return Verify(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`))} + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter) -> Verify { return Verify(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`))} + public static func requestAccess() -> Verify { return Verify(method: .m_requestAccess)} + public static func courseStatus(courseID: Parameter) -> Verify { return Verify(method: .m_courseStatus__courseID_courseID(`courseID`))} + public static func clearAllData(removeCalendar: Parameter) -> Verify { return Verify(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`))} + public static func isDatesChanged(courseID: Parameter, checksum: Parameter) -> Verify { return Verify(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`))} } public struct Perform { fileprivate var method: MethodType var performs: Any + public static func createCalendarIfNeeded(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_createCalendarIfNeeded, performs: perform) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, perform: @escaping ([CourseForSync]) -> Void) -> Perform { + return Perform(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), performs: perform) + } + public static func removeOldCalendar(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeOldCalendar, performs: perform) + } + public static func removeOutdatedEvents(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`), performs: perform) + } + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter, perform: @escaping (String, String, CourseDates) -> Void) -> Perform { + return Perform(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`), performs: perform) + } + public static func requestAccess(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_requestAccess, performs: perform) + } + public static func courseStatus(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_courseStatus__courseID_courseID(`courseID`), performs: perform) + } + public static func clearAllData(removeCalendar: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`), performs: perform) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), performs: perform) + } } public func given(_ method: Given) { @@ -1115,9 +1364,9 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } -// MARK: - CoreAnalytics +// MARK: - ConfigProtocol -open class CoreAnalyticsMock: CoreAnalytics, Mock { +open class ConfigProtocolMock: ConfigProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -1155,118 +1404,1837 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var baseURL: URL { + get { invocations.append(.p_baseURL_get); return __p_baseURL ?? givenGetterValue(.p_baseURL_get, "ConfigProtocolMock - stub value for baseURL was not defined") } + } + private var __p_baseURL: (URL)? + public var baseSSOURL: URL { + get { invocations.append(.p_baseSSOURL_get); return __p_baseSSOURL ?? givenGetterValue(.p_baseSSOURL_get, "ConfigProtocolMock - stub value for baseSSOURL was not defined") } + } + private var __p_baseSSOURL: (URL)? + public var ssoFinishedURL: URL { + get { invocations.append(.p_ssoFinishedURL_get); return __p_ssoFinishedURL ?? givenGetterValue(.p_ssoFinishedURL_get, "ConfigProtocolMock - stub value for ssoFinishedURL was not defined") } + } + private var __p_ssoFinishedURL: (URL)? + public var ssoButtonTitle: [String: Any] { + get { invocations.append(.p_ssoButtonTitle_get); return __p_ssoButtonTitle ?? givenGetterValue(.p_ssoButtonTitle_get, "ConfigProtocolMock - stub value for ssoButtonTitle was not defined") } + } + private var __p_ssoButtonTitle: ([String: Any])? - open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void - perform?(`event`, `parameters`) - } + public var oAuthClientId: String { + get { invocations.append(.p_oAuthClientId_get); return __p_oAuthClientId ?? givenGetterValue(.p_oAuthClientId_get, "ConfigProtocolMock - stub value for oAuthClientId was not defined") } + } + private var __p_oAuthClientId: (String)? - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void - perform?(`event`, `biValue`, `parameters`) - } + public var tokenType: TokenType { + get { invocations.append(.p_tokenType_get); return __p_tokenType ?? givenGetterValue(.p_tokenType_get, "ConfigProtocolMock - stub value for tokenType was not defined") } + } + private var __p_tokenType: (TokenType)? - open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { - addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) - let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void - perform?(`event`, `biValue`, `action`, `rating`) - } + public var feedbackEmail: String { + get { invocations.append(.p_feedbackEmail_get); return __p_feedbackEmail ?? givenGetterValue(.p_feedbackEmail_get, "ConfigProtocolMock - stub value for feedbackEmail was not defined") } + } + private var __p_feedbackEmail: (String)? - open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { - addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) - let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void - perform?(`event`, `bivalue`, `value`, `oldValue`) - } + public var appStoreLink: String { + get { invocations.append(.p_appStoreLink_get); return __p_appStoreLink ?? givenGetterValue(.p_appStoreLink_get, "ConfigProtocolMock - stub value for appStoreLink was not defined") } + } + private var __p_appStoreLink: (String)? - open func trackEvent(_ event: AnalyticsEvent) { - addInvocation(.m_trackEvent__event(Parameter.value(`event`))) - let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void - perform?(`event`) - } + public var faq: URL? { + get { invocations.append(.p_faq_get); return __p_faq ?? optionalGivenGetterValue(.p_faq_get, "ConfigProtocolMock - stub value for faq was not defined") } + } + private var __p_faq: (URL)? - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void - perform?(`event`, `biValue`) - } + public var platformName: String { + get { invocations.append(.p_platformName_get); return __p_platformName ?? givenGetterValue(.p_platformName_get, "ConfigProtocolMock - stub value for platformName was not defined") } + } + private var __p_platformName: (String)? + public var agreement: AgreementConfig { + get { invocations.append(.p_agreement_get); return __p_agreement ?? givenGetterValue(.p_agreement_get, "ConfigProtocolMock - stub value for agreement was not defined") } + } + private var __p_agreement: (AgreementConfig)? - fileprivate enum MethodType { - case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) - case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) - case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) - case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) - case m_trackEvent__event(Parameter) - case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + public var firebase: FirebaseConfig { + get { invocations.append(.p_firebase_get); return __p_firebase ?? givenGetterValue(.p_firebase_get, "ConfigProtocolMock - stub value for firebase was not defined") } + } + private var __p_firebase: (FirebaseConfig)? - static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { - case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + public var facebook: FacebookConfig { + get { invocations.append(.p_facebook_get); return __p_facebook ?? givenGetterValue(.p_facebook_get, "ConfigProtocolMock - stub value for facebook was not defined") } + } + private var __p_facebook: (FacebookConfig)? - case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + public var microsoft: MicrosoftConfig { + get { invocations.append(.p_microsoft_get); return __p_microsoft ?? givenGetterValue(.p_microsoft_get, "ConfigProtocolMock - stub value for microsoft was not defined") } + } + private var __p_microsoft: (MicrosoftConfig)? - case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) - return Matcher.ComparisonResult(results) + public var google: GoogleConfig { + get { invocations.append(.p_google_get); return __p_google ?? givenGetterValue(.p_google_get, "ConfigProtocolMock - stub value for google was not defined") } + } + private var __p_google: (GoogleConfig)? - case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) - return Matcher.ComparisonResult(results) + public var appleSignIn: AppleSignInConfig { + get { invocations.append(.p_appleSignIn_get); return __p_appleSignIn ?? givenGetterValue(.p_appleSignIn_get, "ConfigProtocolMock - stub value for appleSignIn was not defined") } + } + private var __p_appleSignIn: (AppleSignInConfig)? + + public var features: FeaturesConfig { + get { invocations.append(.p_features_get); return __p_features ?? givenGetterValue(.p_features_get, "ConfigProtocolMock - stub value for features was not defined") } + } + private var __p_features: (FeaturesConfig)? + + public var theme: ThemeConfig { + get { invocations.append(.p_theme_get); return __p_theme ?? givenGetterValue(.p_theme_get, "ConfigProtocolMock - stub value for theme was not defined") } + } + private var __p_theme: (ThemeConfig)? + + public var uiComponents: UIComponentsConfig { + get { invocations.append(.p_uiComponents_get); return __p_uiComponents ?? givenGetterValue(.p_uiComponents_get, "ConfigProtocolMock - stub value for uiComponents was not defined") } + } + private var __p_uiComponents: (UIComponentsConfig)? + + public var discovery: DiscoveryConfig { + get { invocations.append(.p_discovery_get); return __p_discovery ?? givenGetterValue(.p_discovery_get, "ConfigProtocolMock - stub value for discovery was not defined") } + } + private var __p_discovery: (DiscoveryConfig)? + + public var dashboard: DashboardConfig { + get { invocations.append(.p_dashboard_get); return __p_dashboard ?? givenGetterValue(.p_dashboard_get, "ConfigProtocolMock - stub value for dashboard was not defined") } + } + private var __p_dashboard: (DashboardConfig)? + + public var braze: BrazeConfig { + get { invocations.append(.p_braze_get); return __p_braze ?? givenGetterValue(.p_braze_get, "ConfigProtocolMock - stub value for braze was not defined") } + } + private var __p_braze: (BrazeConfig)? + + public var branch: BranchConfig { + get { invocations.append(.p_branch_get); return __p_branch ?? givenGetterValue(.p_branch_get, "ConfigProtocolMock - stub value for branch was not defined") } + } + private var __p_branch: (BranchConfig)? + + public var program: DiscoveryConfig { + get { invocations.append(.p_program_get); return __p_program ?? givenGetterValue(.p_program_get, "ConfigProtocolMock - stub value for program was not defined") } + } + private var __p_program: (DiscoveryConfig)? + + public var URIScheme: String { + get { invocations.append(.p_URIScheme_get); return __p_URIScheme ?? givenGetterValue(.p_URIScheme_get, "ConfigProtocolMock - stub value for URIScheme was not defined") } + } + private var __p_URIScheme: (String)? + + + + + + + fileprivate enum MethodType { + case p_baseURL_get + case p_baseSSOURL_get + case p_ssoFinishedURL_get + case p_ssoButtonTitle_get + case p_oAuthClientId_get + case p_tokenType_get + case p_feedbackEmail_get + case p_appStoreLink_get + case p_faq_get + case p_platformName_get + case p_agreement_get + case p_firebase_get + case p_facebook_get + case p_microsoft_get + case p_google_get + case p_appleSignIn_get + case p_features_get + case p_theme_get + case p_uiComponents_get + case p_discovery_get + case p_dashboard_get + case p_braze_get + case p_branch_get + case p_program_get + case p_URIScheme_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_baseURL_get,.p_baseURL_get): return Matcher.ComparisonResult.match + case (.p_baseSSOURL_get,.p_baseSSOURL_get): return Matcher.ComparisonResult.match + case (.p_ssoFinishedURL_get,.p_ssoFinishedURL_get): return Matcher.ComparisonResult.match + case (.p_ssoButtonTitle_get,.p_ssoButtonTitle_get): return Matcher.ComparisonResult.match + case (.p_oAuthClientId_get,.p_oAuthClientId_get): return Matcher.ComparisonResult.match + case (.p_tokenType_get,.p_tokenType_get): return Matcher.ComparisonResult.match + case (.p_feedbackEmail_get,.p_feedbackEmail_get): return Matcher.ComparisonResult.match + case (.p_appStoreLink_get,.p_appStoreLink_get): return Matcher.ComparisonResult.match + case (.p_faq_get,.p_faq_get): return Matcher.ComparisonResult.match + case (.p_platformName_get,.p_platformName_get): return Matcher.ComparisonResult.match + case (.p_agreement_get,.p_agreement_get): return Matcher.ComparisonResult.match + case (.p_firebase_get,.p_firebase_get): return Matcher.ComparisonResult.match + case (.p_facebook_get,.p_facebook_get): return Matcher.ComparisonResult.match + case (.p_microsoft_get,.p_microsoft_get): return Matcher.ComparisonResult.match + case (.p_google_get,.p_google_get): return Matcher.ComparisonResult.match + case (.p_appleSignIn_get,.p_appleSignIn_get): return Matcher.ComparisonResult.match + case (.p_features_get,.p_features_get): return Matcher.ComparisonResult.match + case (.p_theme_get,.p_theme_get): return Matcher.ComparisonResult.match + case (.p_uiComponents_get,.p_uiComponents_get): return Matcher.ComparisonResult.match + case (.p_discovery_get,.p_discovery_get): return Matcher.ComparisonResult.match + case (.p_dashboard_get,.p_dashboard_get): return Matcher.ComparisonResult.match + case (.p_braze_get,.p_braze_get): return Matcher.ComparisonResult.match + case (.p_branch_get,.p_branch_get): return Matcher.ComparisonResult.match + case (.p_program_get,.p_program_get): return Matcher.ComparisonResult.match + case (.p_URIScheme_get,.p_URIScheme_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_baseURL_get: return 0 + case .p_baseSSOURL_get: return 0 + case .p_ssoFinishedURL_get: return 0 + case .p_ssoButtonTitle_get: return 0 + case .p_oAuthClientId_get: return 0 + case .p_tokenType_get: return 0 + case .p_feedbackEmail_get: return 0 + case .p_appStoreLink_get: return 0 + case .p_faq_get: return 0 + case .p_platformName_get: return 0 + case .p_agreement_get: return 0 + case .p_firebase_get: return 0 + case .p_facebook_get: return 0 + case .p_microsoft_get: return 0 + case .p_google_get: return 0 + case .p_appleSignIn_get: return 0 + case .p_features_get: return 0 + case .p_theme_get: return 0 + case .p_uiComponents_get: return 0 + case .p_discovery_get: return 0 + case .p_dashboard_get: return 0 + case .p_braze_get: return 0 + case .p_branch_get: return 0 + case .p_program_get: return 0 + case .p_URIScheme_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_baseURL_get: return "[get] .baseURL" + case .p_baseSSOURL_get: return "[get] .baseSSOURL" + case .p_ssoFinishedURL_get: return "[get] .ssoFinishedURL" + case .p_ssoButtonTitle_get: return "[get] .ssoButtonTitle" + case .p_oAuthClientId_get: return "[get] .oAuthClientId" + case .p_tokenType_get: return "[get] .tokenType" + case .p_feedbackEmail_get: return "[get] .feedbackEmail" + case .p_appStoreLink_get: return "[get] .appStoreLink" + case .p_faq_get: return "[get] .faq" + case .p_platformName_get: return "[get] .platformName" + case .p_agreement_get: return "[get] .agreement" + case .p_firebase_get: return "[get] .firebase" + case .p_facebook_get: return "[get] .facebook" + case .p_microsoft_get: return "[get] .microsoft" + case .p_google_get: return "[get] .google" + case .p_appleSignIn_get: return "[get] .appleSignIn" + case .p_features_get: return "[get] .features" + case .p_theme_get: return "[get] .theme" + case .p_uiComponents_get: return "[get] .uiComponents" + case .p_discovery_get: return "[get] .discovery" + case .p_dashboard_get: return "[get] .dashboard" + case .p_braze_get: return "[get] .braze" + case .p_branch_get: return "[get] .branch" + case .p_program_get: return "[get] .program" + case .p_URIScheme_get: return "[get] .URIScheme" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func baseURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func baseSSOURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseSSOURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoFinishedURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_ssoFinishedURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoButtonTitle(getter defaultValue: [String: Any]...) -> PropertyStub { + return Given(method: .p_ssoButtonTitle_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func oAuthClientId(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_oAuthClientId_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func tokenType(getter defaultValue: TokenType...) -> PropertyStub { + return Given(method: .p_tokenType_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func feedbackEmail(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_feedbackEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appStoreLink(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_appStoreLink_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func faq(getter defaultValue: URL?...) -> PropertyStub { + return Given(method: .p_faq_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func platformName(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_platformName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func agreement(getter defaultValue: AgreementConfig...) -> PropertyStub { + return Given(method: .p_agreement_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func firebase(getter defaultValue: FirebaseConfig...) -> PropertyStub { + return Given(method: .p_firebase_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func facebook(getter defaultValue: FacebookConfig...) -> PropertyStub { + return Given(method: .p_facebook_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func microsoft(getter defaultValue: MicrosoftConfig...) -> PropertyStub { + return Given(method: .p_microsoft_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func google(getter defaultValue: GoogleConfig...) -> PropertyStub { + return Given(method: .p_google_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignIn(getter defaultValue: AppleSignInConfig...) -> PropertyStub { + return Given(method: .p_appleSignIn_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func features(getter defaultValue: FeaturesConfig...) -> PropertyStub { + return Given(method: .p_features_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func theme(getter defaultValue: ThemeConfig...) -> PropertyStub { + return Given(method: .p_theme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func uiComponents(getter defaultValue: UIComponentsConfig...) -> PropertyStub { + return Given(method: .p_uiComponents_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func discovery(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_discovery_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func dashboard(getter defaultValue: DashboardConfig...) -> PropertyStub { + return Given(method: .p_dashboard_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func braze(getter defaultValue: BrazeConfig...) -> PropertyStub { + return Given(method: .p_braze_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func branch(getter defaultValue: BranchConfig...) -> PropertyStub { + return Given(method: .p_branch_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func program(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_program_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func URIScheme(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_URIScheme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var baseURL: Verify { return Verify(method: .p_baseURL_get) } + public static var baseSSOURL: Verify { return Verify(method: .p_baseSSOURL_get) } + public static var ssoFinishedURL: Verify { return Verify(method: .p_ssoFinishedURL_get) } + public static var ssoButtonTitle: Verify { return Verify(method: .p_ssoButtonTitle_get) } + public static var oAuthClientId: Verify { return Verify(method: .p_oAuthClientId_get) } + public static var tokenType: Verify { return Verify(method: .p_tokenType_get) } + public static var feedbackEmail: Verify { return Verify(method: .p_feedbackEmail_get) } + public static var appStoreLink: Verify { return Verify(method: .p_appStoreLink_get) } + public static var faq: Verify { return Verify(method: .p_faq_get) } + public static var platformName: Verify { return Verify(method: .p_platformName_get) } + public static var agreement: Verify { return Verify(method: .p_agreement_get) } + public static var firebase: Verify { return Verify(method: .p_firebase_get) } + public static var facebook: Verify { return Verify(method: .p_facebook_get) } + public static var microsoft: Verify { return Verify(method: .p_microsoft_get) } + public static var google: Verify { return Verify(method: .p_google_get) } + public static var appleSignIn: Verify { return Verify(method: .p_appleSignIn_get) } + public static var features: Verify { return Verify(method: .p_features_get) } + public static var theme: Verify { return Verify(method: .p_theme_get) } + public static var uiComponents: Verify { return Verify(method: .p_uiComponents_get) } + public static var discovery: Verify { return Verify(method: .p_discovery_get) } + public static var dashboard: Verify { return Verify(method: .p_dashboard_get) } + public static var braze: Verify { return Verify(method: .p_braze_get) } + public static var branch: Verify { return Verify(method: .p_branch_get) } + public static var program: Verify { return Verify(method: .p_program_get) } + public static var URIScheme: Verify { return Verify(method: .p_URIScheme_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - ConnectivityProtocol + +open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var isInternetAvaliable: Bool { + get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } + } + private var __p_isInternetAvaliable: (Bool)? + + public var isMobileData: Bool { + get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } + } + private var __p_isMobileData: (Bool)? + + public var internetReachableSubject: CurrentValueSubject { + get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } + } + private var __p_internetReachableSubject: (CurrentValueSubject)? + + + + + + + fileprivate enum MethodType { + case p_isInternetAvaliable_get + case p_isMobileData_get + case p_internetReachableSubject_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match + case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match + case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_isInternetAvaliable_get: return 0 + case .p_isMobileData_get: return 0 + case .p_internetReachableSubject_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" + case .p_isMobileData_get: return "[get] .isMobileData" + case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { + return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } + public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } + public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreAnalytics + +open class CoreAnalyticsMock: CoreAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { + addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) + let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void + perform?(`event`, `biValue`, `action`, `rating`) + } + + open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { + addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) + let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void + perform?(`event`, `bivalue`, `value`, `oldValue`) + } + + open func trackEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + + fileprivate enum MethodType { + case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) + case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) + case m_trackEvent__event(Parameter) + case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) + return Matcher.ComparisonResult(results) + + case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_trackEvent__event(p0): return p0.intValue + case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" + case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" + case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" + case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" + case .m_trackEvent__event: return ".trackEvent(_:)" + case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} + public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { + return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) + } + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { + return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) + } + public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackEvent__event(`event`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CorePersistenceProtocol + +open class CorePersistenceProtocolMock: CorePersistenceProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func set(userId: Int) { + addInvocation(.m_set__userId_userId(Parameter.value(`userId`))) + let perform = methodPerformValue(.m_set__userId_userId(Parameter.value(`userId`))) as? (Int) -> Void + perform?(`userId`) + } + + open func getUserID() -> Int? { + addInvocation(.m_getUserID) + let perform = methodPerformValue(.m_getUserID) as? () -> Void + perform?() + var __value: Int? = nil + do { + __value = try methodReturnValue(.m_getUserID).casted() + } catch { + // do nothing + } + return __value + } + + open func publisher() -> AnyPublisher { + addInvocation(.m_publisher) + let perform = methodPerformValue(.m_publisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_publisher).casted() + } catch { + onFatalFailure("Stub return value not specified for publisher(). Use given") + Failure("Stub return value not specified for publisher(). Use given") + } + return __value + } + + open func addToDownloadQueue(tasks: [DownloadDataTask]) { + addInvocation(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) as? ([DownloadDataTask]) -> Void + perform?(`tasks`) + } + + open func saveOfflineProgress(progress: OfflineProgress) { + addInvocation(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) + let perform = methodPerformValue(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) as? (OfflineProgress) -> Void + perform?(`progress`) + } + + open func loadProgress(for blockID: String) -> OfflineProgress? { + addInvocation(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + var __value: OfflineProgress? = nil + do { + __value = try methodReturnValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))).casted() + } catch { + // do nothing + } + return __value + } + + open func loadAllOfflineProgress() -> [OfflineProgress] { + addInvocation(.m_loadAllOfflineProgress) + let perform = methodPerformValue(.m_loadAllOfflineProgress) as? () -> Void + perform?() + var __value: [OfflineProgress] + do { + __value = try methodReturnValue(.m_loadAllOfflineProgress).casted() + } catch { + onFatalFailure("Stub return value not specified for loadAllOfflineProgress(). Use given") + Failure("Stub return value not specified for loadAllOfflineProgress(). Use given") + } + return __value + } + + open func deleteProgress(for blockID: String) { + addInvocation(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + } + + open func deleteAllProgress() { + addInvocation(.m_deleteAllProgress) + let perform = methodPerformValue(.m_deleteAllProgress) as? () -> Void + perform?() + } + + open func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) { + addInvocation(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) as? ([CourseBlock], DownloadQuality) -> Void + perform?(`blocks`, `downloadQuality`) + } + + open func nextBlockForDownloading() -> DownloadDataTask? { + addInvocation(.m_nextBlockForDownloading) + let perform = methodPerformValue(.m_nextBlockForDownloading) as? () -> Void + perform?() + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_nextBlockForDownloading).casted() + } catch { + // do nothing + } + return __value + } + + open func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) { + addInvocation(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) + let perform = methodPerformValue(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) as? (String, DownloadState, Data?) -> Void + perform?(`id`, `state`, `resumeData`) + } + + open func deleteDownloadDataTask(id: String) throws { + addInvocation(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) + let perform = methodPerformValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) as? (String) -> Void + perform?(`id`) + do { + _ = try methodReturnValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func saveDownloadDataTask(_ task: DownloadDataTask) { + addInvocation(.m_saveDownloadDataTask__task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_saveDownloadDataTask__task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) + } + + open func downloadDataTask(for blockId: String) -> DownloadDataTask? { + addInvocation(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) + let perform = methodPerformValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void + perform?(`blockId`) + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))).casted() + } catch { + // do nothing + } + return __value + } + + open func getDownloadDataTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasks) + let perform = methodPerformValue(.m_getDownloadDataTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasks(). Use given") + Failure("Stub return value not specified for getDownloadDataTasks(). Use given") + } + return __value + } + + open func getDownloadDataTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + } + return __value + } + + + fileprivate enum MethodType { + case m_set__userId_userId(Parameter) + case m_getUserID + case m_publisher + case m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>) + case m_saveOfflineProgress__progress_progress(Parameter) + case m_loadProgress__for_blockID(Parameter) + case m_loadAllOfflineProgress + case m_deleteProgress__for_blockID(Parameter) + case m_deleteAllProgress + case m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>, Parameter) + case m_nextBlockForDownloading + case m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter, Parameter, Parameter) + case m_deleteDownloadDataTask__id_id(Parameter) + case m_saveDownloadDataTask__task(Parameter) + case m_downloadDataTask__for_blockId(Parameter) + case m_getDownloadDataTasks + case m_getDownloadDataTasksForCourse__courseId(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_set__userId_userId(let lhsUserid), .m_set__userId_userId(let rhsUserid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUserid, rhs: rhsUserid, with: matcher), lhsUserid, rhsUserid, "userId")) + return Matcher.ComparisonResult(results) + + case (.m_getUserID, .m_getUserID): return .match + + case (.m_publisher, .m_publisher): return .match + + case (.m_addToDownloadQueue__tasks_tasks(let lhsTasks), .m_addToDownloadQueue__tasks_tasks(let rhsTasks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTasks, rhs: rhsTasks, with: matcher), lhsTasks, rhsTasks, "tasks")) + return Matcher.ComparisonResult(results) + + case (.m_saveOfflineProgress__progress_progress(let lhsProgress), .m_saveOfflineProgress__progress_progress(let rhsProgress)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsProgress, rhs: rhsProgress, with: matcher), lhsProgress, rhsProgress, "progress")) + return Matcher.ComparisonResult(results) + + case (.m_loadProgress__for_blockID(let lhsBlockid), .m_loadProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_loadAllOfflineProgress, .m_loadAllOfflineProgress): return .match + + case (.m_deleteProgress__for_blockID(let lhsBlockid), .m_deleteProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_deleteAllProgress, .m_deleteAllProgress): return .match + + case (.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let lhsBlocks, let lhsDownloadquality), .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let rhsBlocks, let rhsDownloadquality)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDownloadquality, rhs: rhsDownloadquality, with: matcher), lhsDownloadquality, rhsDownloadquality, "downloadQuality")) + return Matcher.ComparisonResult(results) + + case (.m_nextBlockForDownloading, .m_nextBlockForDownloading): return .match + + case (.m_updateDownloadState__id_idstate_stateresumeData_resumeData(let lhsId, let lhsState, let lhsResumedata), .m_updateDownloadState__id_idstate_stateresumeData_resumeData(let rhsId, let rhsState, let rhsResumedata)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsState, rhs: rhsState, with: matcher), lhsState, rhsState, "state")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResumedata, rhs: rhsResumedata, with: matcher), lhsResumedata, rhsResumedata, "resumeData")) + return Matcher.ComparisonResult(results) + + case (.m_deleteDownloadDataTask__id_id(let lhsId), .m_deleteDownloadDataTask__id_id(let rhsId)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + return Matcher.ComparisonResult(results) + + case (.m_saveDownloadDataTask__task(let lhsTask), .m_saveDownloadDataTask__task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "_ task")) + return Matcher.ComparisonResult(results) + + case (.m_downloadDataTask__for_blockId(let lhsBlockid), .m_downloadDataTask__for_blockId(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) + return Matcher.ComparisonResult(results) + + case (.m_getDownloadDataTasks, .m_getDownloadDataTasks): return .match + + case (.m_getDownloadDataTasksForCourse__courseId(let lhsCourseid), .m_getDownloadDataTasksForCourse__courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_set__userId_userId(p0): return p0.intValue + case .m_getUserID: return 0 + case .m_publisher: return 0 + case let .m_addToDownloadQueue__tasks_tasks(p0): return p0.intValue + case let .m_saveOfflineProgress__progress_progress(p0): return p0.intValue + case let .m_loadProgress__for_blockID(p0): return p0.intValue + case .m_loadAllOfflineProgress: return 0 + case let .m_deleteProgress__for_blockID(p0): return p0.intValue + case .m_deleteAllProgress: return 0 + case let .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(p0, p1): return p0.intValue + p1.intValue + case .m_nextBlockForDownloading: return 0 + case let .m_updateDownloadState__id_idstate_stateresumeData_resumeData(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_deleteDownloadDataTask__id_id(p0): return p0.intValue + case let .m_saveDownloadDataTask__task(p0): return p0.intValue + case let .m_downloadDataTask__for_blockId(p0): return p0.intValue + case .m_getDownloadDataTasks: return 0 + case let .m_getDownloadDataTasksForCourse__courseId(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_set__userId_userId: return ".set(userId:)" + case .m_getUserID: return ".getUserID()" + case .m_publisher: return ".publisher()" + case .m_addToDownloadQueue__tasks_tasks: return ".addToDownloadQueue(tasks:)" + case .m_saveOfflineProgress__progress_progress: return ".saveOfflineProgress(progress:)" + case .m_loadProgress__for_blockID: return ".loadProgress(for:)" + case .m_loadAllOfflineProgress: return ".loadAllOfflineProgress()" + case .m_deleteProgress__for_blockID: return ".deleteProgress(for:)" + case .m_deleteAllProgress: return ".deleteAllProgress()" + case .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality: return ".addToDownloadQueue(blocks:downloadQuality:)" + case .m_nextBlockForDownloading: return ".nextBlockForDownloading()" + case .m_updateDownloadState__id_idstate_stateresumeData_resumeData: return ".updateDownloadState(id:state:resumeData:)" + case .m_deleteDownloadDataTask__id_id: return ".deleteDownloadDataTask(id:)" + case .m_saveDownloadDataTask__task: return ".saveDownloadDataTask(_:)" + case .m_downloadDataTask__for_blockId: return ".downloadDataTask(for:)" + case .m_getDownloadDataTasks: return ".getDownloadDataTasks()" + case .m_getDownloadDataTasksForCourse__courseId: return ".getDownloadDataTasksForCourse(_:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func getUserID(willReturn: Int?...) -> MethodStub { + return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func publisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadProgress(for blockID: Parameter, willReturn: OfflineProgress?...) -> MethodStub { + return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadAllOfflineProgress(willReturn: [OfflineProgress]...) -> MethodStub { + return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func nextBlockForDownloading(willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func downloadDataTask(for blockId: Parameter, willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getUserID(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Int?] = [] + let given: Given = { return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Int?).self) + willProduce(stubber) + return given + } + public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func loadProgress(for blockID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [OfflineProgress?] = [] + let given: Given = { return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (OfflineProgress?).self) + willProduce(stubber) + return given + } + public static func loadAllOfflineProgress(willProduce: (Stubber<[OfflineProgress]>) -> Void) -> MethodStub { + let willReturn: [[OfflineProgress]] = [] + let given: Given = { return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([OfflineProgress]).self) + willProduce(stubber) + return given + } + public static func nextBlockForDownloading(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func downloadDataTask(for blockId: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func deleteDownloadDataTask(id: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func deleteDownloadDataTask(id: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func set(userId: Parameter) -> Verify { return Verify(method: .m_set__userId_userId(`userId`))} + public static func getUserID() -> Verify { return Verify(method: .m_getUserID)} + public static func publisher() -> Verify { return Verify(method: .m_publisher)} + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>) -> Verify { return Verify(method: .m_addToDownloadQueue__tasks_tasks(`tasks`))} + public static func saveOfflineProgress(progress: Parameter) -> Verify { return Verify(method: .m_saveOfflineProgress__progress_progress(`progress`))} + public static func loadProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_loadProgress__for_blockID(`blockID`))} + public static func loadAllOfflineProgress() -> Verify { return Verify(method: .m_loadAllOfflineProgress)} + public static func deleteProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_deleteProgress__for_blockID(`blockID`))} + public static func deleteAllProgress() -> Verify { return Verify(method: .m_deleteAllProgress)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`))} + public static func nextBlockForDownloading() -> Verify { return Verify(method: .m_nextBlockForDownloading)} + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter) -> Verify { return Verify(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`))} + public static func deleteDownloadDataTask(id: Parameter) -> Verify { return Verify(method: .m_deleteDownloadDataTask__id_id(`id`))} + public static func saveDownloadDataTask(_ task: Parameter) -> Verify { return Verify(method: .m_saveDownloadDataTask__task(`task`))} + public static func downloadDataTask(for blockId: Parameter) -> Verify { return Verify(method: .m_downloadDataTask__for_blockId(`blockId`))} + public static func getDownloadDataTasks() -> Verify { return Verify(method: .m_getDownloadDataTasks)} + public static func getDownloadDataTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func set(userId: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_set__userId_userId(`userId`), performs: perform) + } + public static func getUserID(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getUserID, performs: perform) + } + public static func publisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_publisher, performs: perform) + } + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>, perform: @escaping ([DownloadDataTask]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__tasks_tasks(`tasks`), performs: perform) + } + public static func saveOfflineProgress(progress: Parameter, perform: @escaping (OfflineProgress) -> Void) -> Perform { + return Perform(method: .m_saveOfflineProgress__progress_progress(`progress`), performs: perform) + } + public static func loadProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_loadProgress__for_blockID(`blockID`), performs: perform) + } + public static func loadAllOfflineProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_loadAllOfflineProgress, performs: perform) + } + public static func deleteProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteProgress__for_blockID(`blockID`), performs: perform) + } + public static func deleteAllProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteAllProgress, performs: perform) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter, perform: @escaping ([CourseBlock], DownloadQuality) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`), performs: perform) + } + public static func nextBlockForDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_nextBlockForDownloading, performs: perform) + } + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter, perform: @escaping (String, DownloadState, Data?) -> Void) -> Perform { + return Perform(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`), performs: perform) + } + public static func deleteDownloadDataTask(id: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteDownloadDataTask__id_id(`id`), performs: perform) + } + public static func saveDownloadDataTask(_ task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_saveDownloadDataTask__task(`task`), performs: perform) + } + public static func downloadDataTask(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_downloadDataTask__for_blockId(`blockId`), performs: perform) + } + public static func getDownloadDataTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasks, performs: perform) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreStorage + +open class CoreStorageMock: CoreStorage, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var accessToken: String? { + get { invocations.append(.p_accessToken_get); return __p_accessToken ?? optionalGivenGetterValue(.p_accessToken_get, "CoreStorageMock - stub value for accessToken was not defined") } + set { invocations.append(.p_accessToken_set(.value(newValue))); __p_accessToken = newValue } + } + private var __p_accessToken: (String)? + + public var refreshToken: String? { + get { invocations.append(.p_refreshToken_get); return __p_refreshToken ?? optionalGivenGetterValue(.p_refreshToken_get, "CoreStorageMock - stub value for refreshToken was not defined") } + set { invocations.append(.p_refreshToken_set(.value(newValue))); __p_refreshToken = newValue } + } + private var __p_refreshToken: (String)? + + public var pushToken: String? { + get { invocations.append(.p_pushToken_get); return __p_pushToken ?? optionalGivenGetterValue(.p_pushToken_get, "CoreStorageMock - stub value for pushToken was not defined") } + set { invocations.append(.p_pushToken_set(.value(newValue))); __p_pushToken = newValue } + } + private var __p_pushToken: (String)? + + public var appleSignFullName: String? { + get { invocations.append(.p_appleSignFullName_get); return __p_appleSignFullName ?? optionalGivenGetterValue(.p_appleSignFullName_get, "CoreStorageMock - stub value for appleSignFullName was not defined") } + set { invocations.append(.p_appleSignFullName_set(.value(newValue))); __p_appleSignFullName = newValue } + } + private var __p_appleSignFullName: (String)? + + public var appleSignEmail: String? { + get { invocations.append(.p_appleSignEmail_get); return __p_appleSignEmail ?? optionalGivenGetterValue(.p_appleSignEmail_get, "CoreStorageMock - stub value for appleSignEmail was not defined") } + set { invocations.append(.p_appleSignEmail_set(.value(newValue))); __p_appleSignEmail = newValue } + } + private var __p_appleSignEmail: (String)? + + public var cookiesDate: Date? { + get { invocations.append(.p_cookiesDate_get); return __p_cookiesDate ?? optionalGivenGetterValue(.p_cookiesDate_get, "CoreStorageMock - stub value for cookiesDate was not defined") } + set { invocations.append(.p_cookiesDate_set(.value(newValue))); __p_cookiesDate = newValue } + } + private var __p_cookiesDate: (Date)? + + public var reviewLastShownVersion: String? { + get { invocations.append(.p_reviewLastShownVersion_get); return __p_reviewLastShownVersion ?? optionalGivenGetterValue(.p_reviewLastShownVersion_get, "CoreStorageMock - stub value for reviewLastShownVersion was not defined") } + set { invocations.append(.p_reviewLastShownVersion_set(.value(newValue))); __p_reviewLastShownVersion = newValue } + } + private var __p_reviewLastShownVersion: (String)? + + public var lastReviewDate: Date? { + get { invocations.append(.p_lastReviewDate_get); return __p_lastReviewDate ?? optionalGivenGetterValue(.p_lastReviewDate_get, "CoreStorageMock - stub value for lastReviewDate was not defined") } + set { invocations.append(.p_lastReviewDate_set(.value(newValue))); __p_lastReviewDate = newValue } + } + private var __p_lastReviewDate: (Date)? + + public var user: DataLayer.User? { + get { invocations.append(.p_user_get); return __p_user ?? optionalGivenGetterValue(.p_user_get, "CoreStorageMock - stub value for user was not defined") } + set { invocations.append(.p_user_set(.value(newValue))); __p_user = newValue } + } + private var __p_user: (DataLayer.User)? + + public var userSettings: UserSettings? { + get { invocations.append(.p_userSettings_get); return __p_userSettings ?? optionalGivenGetterValue(.p_userSettings_get, "CoreStorageMock - stub value for userSettings was not defined") } + set { invocations.append(.p_userSettings_set(.value(newValue))); __p_userSettings = newValue } + } + private var __p_userSettings: (UserSettings)? + + public var resetAppSupportDirectoryUserData: Bool? { + get { invocations.append(.p_resetAppSupportDirectoryUserData_get); return __p_resetAppSupportDirectoryUserData ?? optionalGivenGetterValue(.p_resetAppSupportDirectoryUserData_get, "CoreStorageMock - stub value for resetAppSupportDirectoryUserData was not defined") } + set { invocations.append(.p_resetAppSupportDirectoryUserData_set(.value(newValue))); __p_resetAppSupportDirectoryUserData = newValue } + } + private var __p_resetAppSupportDirectoryUserData: (Bool)? + + public var useRelativeDates: Bool { + get { invocations.append(.p_useRelativeDates_get); return __p_useRelativeDates ?? givenGetterValue(.p_useRelativeDates_get, "CoreStorageMock - stub value for useRelativeDates was not defined") } + set { invocations.append(.p_useRelativeDates_set(.value(newValue))); __p_useRelativeDates = newValue } + } + private var __p_useRelativeDates: (Bool)? + + + + + + open func clear() { + addInvocation(.m_clear) + let perform = methodPerformValue(.m_clear) as? () -> Void + perform?() + } - case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - return Matcher.ComparisonResult(results) - case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - return Matcher.ComparisonResult(results) + fileprivate enum MethodType { + case m_clear + case p_accessToken_get + case p_accessToken_set(Parameter) + case p_refreshToken_get + case p_refreshToken_set(Parameter) + case p_pushToken_get + case p_pushToken_set(Parameter) + case p_appleSignFullName_get + case p_appleSignFullName_set(Parameter) + case p_appleSignEmail_get + case p_appleSignEmail_set(Parameter) + case p_cookiesDate_get + case p_cookiesDate_set(Parameter) + case p_reviewLastShownVersion_get + case p_reviewLastShownVersion_set(Parameter) + case p_lastReviewDate_get + case p_lastReviewDate_set(Parameter) + case p_user_get + case p_user_set(Parameter) + case p_userSettings_get + case p_userSettings_set(Parameter) + case p_resetAppSupportDirectoryUserData_get + case p_resetAppSupportDirectoryUserData_set(Parameter) + case p_useRelativeDates_get + case p_useRelativeDates_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_clear, .m_clear): return .match + case (.p_accessToken_get,.p_accessToken_get): return Matcher.ComparisonResult.match + case (.p_accessToken_set(let left),.p_accessToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_refreshToken_get,.p_refreshToken_get): return Matcher.ComparisonResult.match + case (.p_refreshToken_set(let left),.p_refreshToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_pushToken_get,.p_pushToken_get): return Matcher.ComparisonResult.match + case (.p_pushToken_set(let left),.p_pushToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignFullName_get,.p_appleSignFullName_get): return Matcher.ComparisonResult.match + case (.p_appleSignFullName_set(let left),.p_appleSignFullName_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignEmail_get,.p_appleSignEmail_get): return Matcher.ComparisonResult.match + case (.p_appleSignEmail_set(let left),.p_appleSignEmail_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_cookiesDate_get,.p_cookiesDate_get): return Matcher.ComparisonResult.match + case (.p_cookiesDate_set(let left),.p_cookiesDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_reviewLastShownVersion_get,.p_reviewLastShownVersion_get): return Matcher.ComparisonResult.match + case (.p_reviewLastShownVersion_set(let left),.p_reviewLastShownVersion_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_lastReviewDate_get,.p_lastReviewDate_get): return Matcher.ComparisonResult.match + case (.p_lastReviewDate_set(let left),.p_lastReviewDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_user_get,.p_user_get): return Matcher.ComparisonResult.match + case (.p_user_set(let left),.p_user_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_userSettings_get,.p_userSettings_get): return Matcher.ComparisonResult.match + case (.p_userSettings_set(let left),.p_userSettings_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_resetAppSupportDirectoryUserData_get,.p_resetAppSupportDirectoryUserData_get): return Matcher.ComparisonResult.match + case (.p_resetAppSupportDirectoryUserData_set(let left),.p_resetAppSupportDirectoryUserData_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_useRelativeDates_get,.p_useRelativeDates_get): return Matcher.ComparisonResult.match + case (.p_useRelativeDates_set(let left),.p_useRelativeDates_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) default: return .none } } func intValue() -> Int { switch self { - case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue - case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue - case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue - case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue - case let .m_trackEvent__event(p0): return p0.intValue - case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case .m_clear: return 0 + case .p_accessToken_get: return 0 + case .p_accessToken_set(let newValue): return newValue.intValue + case .p_refreshToken_get: return 0 + case .p_refreshToken_set(let newValue): return newValue.intValue + case .p_pushToken_get: return 0 + case .p_pushToken_set(let newValue): return newValue.intValue + case .p_appleSignFullName_get: return 0 + case .p_appleSignFullName_set(let newValue): return newValue.intValue + case .p_appleSignEmail_get: return 0 + case .p_appleSignEmail_set(let newValue): return newValue.intValue + case .p_cookiesDate_get: return 0 + case .p_cookiesDate_set(let newValue): return newValue.intValue + case .p_reviewLastShownVersion_get: return 0 + case .p_reviewLastShownVersion_set(let newValue): return newValue.intValue + case .p_lastReviewDate_get: return 0 + case .p_lastReviewDate_set(let newValue): return newValue.intValue + case .p_user_get: return 0 + case .p_user_set(let newValue): return newValue.intValue + case .p_userSettings_get: return 0 + case .p_userSettings_set(let newValue): return newValue.intValue + case .p_resetAppSupportDirectoryUserData_get: return 0 + case .p_resetAppSupportDirectoryUserData_set(let newValue): return newValue.intValue + case .p_useRelativeDates_get: return 0 + case .p_useRelativeDates_set(let newValue): return newValue.intValue } } func assertionName() -> String { switch self { - case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" - case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" - case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" - case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" - case .m_trackEvent__event: return ".trackEvent(_:)" - case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_clear: return ".clear()" + case .p_accessToken_get: return "[get] .accessToken" + case .p_accessToken_set: return "[set] .accessToken" + case .p_refreshToken_get: return "[get] .refreshToken" + case .p_refreshToken_set: return "[set] .refreshToken" + case .p_pushToken_get: return "[get] .pushToken" + case .p_pushToken_set: return "[set] .pushToken" + case .p_appleSignFullName_get: return "[get] .appleSignFullName" + case .p_appleSignFullName_set: return "[set] .appleSignFullName" + case .p_appleSignEmail_get: return "[get] .appleSignEmail" + case .p_appleSignEmail_set: return "[set] .appleSignEmail" + case .p_cookiesDate_get: return "[get] .cookiesDate" + case .p_cookiesDate_set: return "[set] .cookiesDate" + case .p_reviewLastShownVersion_get: return "[get] .reviewLastShownVersion" + case .p_reviewLastShownVersion_set: return "[set] .reviewLastShownVersion" + case .p_lastReviewDate_get: return "[get] .lastReviewDate" + case .p_lastReviewDate_set: return "[set] .lastReviewDate" + case .p_user_get: return "[get] .user" + case .p_user_set: return "[set] .user" + case .p_userSettings_get: return "[get] .userSettings" + case .p_userSettings_set: return "[set] .userSettings" + case .p_resetAppSupportDirectoryUserData_get: return "[get] .resetAppSupportDirectoryUserData" + case .p_resetAppSupportDirectoryUserData_set: return "[set] .resetAppSupportDirectoryUserData" + case .p_useRelativeDates_get: return "[get] .useRelativeDates" + case .p_useRelativeDates_set: return "[set] .useRelativeDates" } } } @@ -1279,41 +3247,81 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { super.init(products) } + public static func accessToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_accessToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func refreshToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_refreshToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func pushToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_pushToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignFullName(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignFullName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignEmail(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_cookiesDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func reviewLastShownVersion(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_reviewLastShownVersion_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func lastReviewDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_lastReviewDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func user(getter defaultValue: DataLayer.User?...) -> PropertyStub { + return Given(method: .p_user_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func userSettings(getter defaultValue: UserSettings?...) -> PropertyStub { + return Given(method: .p_userSettings_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func resetAppSupportDirectoryUserData(getter defaultValue: Bool?...) -> PropertyStub { + return Given(method: .p_resetAppSupportDirectoryUserData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func useRelativeDates(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_useRelativeDates_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } } public struct Verify { fileprivate var method: MethodType - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} - public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func clear() -> Verify { return Verify(method: .m_clear)} + public static var accessToken: Verify { return Verify(method: .p_accessToken_get) } + public static func accessToken(set newValue: Parameter) -> Verify { return Verify(method: .p_accessToken_set(newValue)) } + public static var refreshToken: Verify { return Verify(method: .p_refreshToken_get) } + public static func refreshToken(set newValue: Parameter) -> Verify { return Verify(method: .p_refreshToken_set(newValue)) } + public static var pushToken: Verify { return Verify(method: .p_pushToken_get) } + public static func pushToken(set newValue: Parameter) -> Verify { return Verify(method: .p_pushToken_set(newValue)) } + public static var appleSignFullName: Verify { return Verify(method: .p_appleSignFullName_get) } + public static func appleSignFullName(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignFullName_set(newValue)) } + public static var appleSignEmail: Verify { return Verify(method: .p_appleSignEmail_get) } + public static func appleSignEmail(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignEmail_set(newValue)) } + public static var cookiesDate: Verify { return Verify(method: .p_cookiesDate_get) } + public static func cookiesDate(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesDate_set(newValue)) } + public static var reviewLastShownVersion: Verify { return Verify(method: .p_reviewLastShownVersion_get) } + public static func reviewLastShownVersion(set newValue: Parameter) -> Verify { return Verify(method: .p_reviewLastShownVersion_set(newValue)) } + public static var lastReviewDate: Verify { return Verify(method: .p_lastReviewDate_get) } + public static func lastReviewDate(set newValue: Parameter) -> Verify { return Verify(method: .p_lastReviewDate_set(newValue)) } + public static var user: Verify { return Verify(method: .p_user_get) } + public static func user(set newValue: Parameter) -> Verify { return Verify(method: .p_user_set(newValue)) } + public static var userSettings: Verify { return Verify(method: .p_userSettings_get) } + public static func userSettings(set newValue: Parameter) -> Verify { return Verify(method: .p_userSettings_set(newValue)) } + public static var resetAppSupportDirectoryUserData: Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_get) } + public static func resetAppSupportDirectoryUserData(set newValue: Parameter) -> Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_set(newValue)) } + public static var useRelativeDates: Verify { return Verify(method: .p_useRelativeDates_get) } + public static func useRelativeDates(set newValue: Parameter) -> Verify { return Verify(method: .p_useRelativeDates_set(newValue)) } } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) - } - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) - } - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { - return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) - } - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { - return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) - } - public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { - return Perform(method: .m_trackEvent__event(`event`), performs: perform) - } - public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + public static func clear(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_clear, performs: perform) } } @@ -2628,6 +4636,12 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -2672,6 +4686,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showSSOWebBrowser__title_title(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -2777,6 +4792,11 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) return Matcher.ComparisonResult(results) + case (.m_showSSOWebBrowser__title_title(let lhsTitle), .m_showSSOWebBrowser__title_title(let rhsTitle)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -2836,6 +4856,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showSSOWebBrowser__title_title(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -2862,6 +4883,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showSSOWebBrowser__title_title: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -2902,6 +4924,7 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} + public static func showSSOWebBrowser(title: Parameter) -> Verify { return Verify(method: .m_showSSOWebBrowser__title_title(`title`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`))} @@ -2966,6 +4989,9 @@ open class DiscussionRouterMock: DiscussionRouter, Mock { public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showSSOWebBrowser__title_title(`title`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -3248,6 +5274,20 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func updateUnzippedFileSize(for sequentials: [CourseSequential]) -> [CourseSequential] { + addInvocation(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) + let perform = methodPerformValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) as? ([CourseSequential]) -> Void + perform?(`sequentials`) + var __value: [CourseSequential] + do { + __value = try methodReturnValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))).casted() + } catch { + onFatalFailure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + Failure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + } + return __value + } + open func resumeDownloading() throws { addInvocation(.m_resumeDownloading) let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void @@ -3275,6 +5315,12 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { return __value } + open func removeAppSupportDirectoryUnusedContent() { + addInvocation(.m_removeAppSupportDirectoryUnusedContent) + let perform = methodPerformValue(.m_removeAppSupportDirectoryUnusedContent) as? () -> Void + perform?() + } + fileprivate enum MethodType { case m_publisher @@ -3289,8 +5335,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) case m_deleteAllFiles case m_fileUrl__for_blockId(Parameter) + case m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>) case m_resumeDownloading case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_removeAppSupportDirectoryUnusedContent case p_currentDownloadTask_get static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { @@ -3341,12 +5389,19 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) return Matcher.ComparisonResult(results) + case (.m_updateUnzippedFileSize__for_sequentials(let lhsSequentials), .m_updateUnzippedFileSize__for_sequentials(let rhsSequentials)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSequentials, rhs: rhsSequentials, with: matcher), lhsSequentials, rhsSequentials, "for sequentials")) + return Matcher.ComparisonResult(results) + case (.m_resumeDownloading, .m_resumeDownloading): return .match case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) return Matcher.ComparisonResult(results) + + case (.m_removeAppSupportDirectoryUnusedContent, .m_removeAppSupportDirectoryUnusedContent): return .match case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } @@ -3366,8 +5421,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case let .m_deleteFile__blocks_blocks(p0): return p0.intValue case .m_deleteAllFiles: return 0 case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_updateUnzippedFileSize__for_sequentials(p0): return p0.intValue case .m_resumeDownloading: return 0 case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_removeAppSupportDirectoryUnusedContent: return 0 case .p_currentDownloadTask_get: return 0 } } @@ -3385,8 +5442,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" case .m_deleteAllFiles: return ".deleteAllFiles()" case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_updateUnzippedFileSize__for_sequentials: return ".updateUnzippedFileSize(for:)" case .m_resumeDownloading: return ".resumeDownloading()" case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_removeAppSupportDirectoryUnusedContent: return ".removeAppSupportDirectoryUnusedContent()" case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" } } @@ -3419,6 +5478,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willReturn: [CourseSequential]...) -> MethodStub { + return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -3457,6 +5519,13 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { willProduce(stubber) return given } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willProduce: (Stubber<[CourseSequential]>) -> Void) -> MethodStub { + let willReturn: [[CourseSequential]] = [] + let given: Given = { return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseSequential]).self) + willProduce(stubber) + return given + } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { let willReturn: [Bool] = [] let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() @@ -3541,8 +5610,10 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>) -> Verify { return Verify(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`))} public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func removeAppSupportDirectoryUnusedContent() -> Verify { return Verify(method: .m_removeAppSupportDirectoryUnusedContent)} public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } } @@ -3586,12 +5657,217 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, perform: @escaping ([CourseSequential]) -> Void) -> Perform { + return Perform(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), performs: perform) + } public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_resumeDownloading, performs: perform) } public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) } + public static func removeAppSupportDirectoryUnusedContent(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeAppSupportDirectoryUnusedContent, performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - OfflineSyncInteractorProtocol + +open class OfflineSyncInteractorProtocolMock: OfflineSyncInteractorProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func submitOfflineProgress(courseID: String, blockID: String, data: String) throws -> Bool { + addInvocation(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) + let perform = methodPerformValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) as? (String, String, String) -> Void + perform?(`courseID`, `blockID`, `data`) + var __value: Bool + do { + __value = try methodReturnValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + Failure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + } catch { + throw error + } + return __value + } + + + fileprivate enum MethodType { + case m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter, Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let lhsCourseid, let lhsBlockid, let lhsData), .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let rhsCourseid, let rhsBlockid, let rhsData)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsData, rhs: rhsData, with: matcher), lhsData, rhsData, "data")) + return Matcher.ComparisonResult(results) + } + } + + func intValue() -> Int { + switch self { + case let .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + } + } + func assertionName() -> String { + switch self { + case .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data: return ".submitOfflineProgress(courseID:blockID:data:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Bool).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter) -> Verify { return Verify(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { + return Perform(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), performs: perform) + } } public func given(_ method: Given) { diff --git a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift index b6940c034..d862278fa 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/Base/BaseResponsesViewModelTests.swift @@ -50,11 +50,27 @@ final class BaseResponsesViewModelTests: XCTestCase { abuseFlagged: false, closed: false) + var interactor: DiscussionInteractorProtocolMock! + var router: DiscussionRouterMock! + var config: ConfigMock! + var viewModel: BaseResponsesViewModel! + + override func setUp() async throws { + try await super.setUp() + + interactor = DiscussionInteractorProtocolMock() + router = DiscussionRouterMock() + config = ConfigMock() + viewModel = BaseResponsesViewModel( + interactor: interactor, + router: router, + config: config, + storage: CoreStorageMock() + ) + } + func testVoteThreadSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false viewModel.postComments = post @@ -73,10 +89,7 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteResponseSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false @@ -97,10 +110,7 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteParentThreadSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false @@ -120,10 +130,7 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteParentResponseSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false @@ -145,10 +152,7 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteNoInternetError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false @@ -167,10 +171,7 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testVoteUnknownError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) + var result = false @@ -187,10 +188,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagThreadSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -210,10 +207,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagCommentSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -233,10 +226,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagNoInternetError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -255,10 +244,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFlagUnknownError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -275,10 +260,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFollowThreadSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -298,10 +279,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFollowThreadNoInternetError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -320,10 +297,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testFollowThreadUnknownError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) var result = false @@ -340,10 +313,6 @@ final class BaseResponsesViewModelTests: XCTestCase { } func testAddNewPost() { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = BaseResponsesViewModel(interactor: interactor, router: router, config: config) viewModel.postComments = post diff --git a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift index 7da4cdf55..97ab5f718 100644 --- a/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Comment/ThreadViewModelTests.swift @@ -212,6 +212,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willProduce: {_ in})) @@ -241,6 +242,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willProduce: {_ in})) @@ -270,6 +272,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -301,6 +304,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) Given(interactor, .readBody(threadID: .any, willThrow: NSError())) @@ -328,6 +332,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) let post = Post(authorName: "", @@ -368,6 +373,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -392,6 +398,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willThrow: NSError()) ) @@ -415,6 +422,7 @@ final class ThreadViewModelTests: XCTestCase { let viewModel = ThreadViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), postStateSubject: .init(.readed(id: "1"))) viewModel.totalPages = 2 diff --git a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift index f94484fc2..d6c82f7aa 100644 --- a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionSearchTopicsViewModelTests.swift @@ -17,8 +17,10 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { func testSearchSuccess() async throws { let interactor = DiscussionInteractorProtocolMock() let router = DiscussionRouterMock() + let storage = CoreStorageMock() let viewModel = DiscussionSearchTopicsViewModel(courseID: "123", - interactor: interactor, + interactor: interactor, + storage: storage, router: router, debounce: .test) @@ -47,7 +49,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { numPages: 1) ] ) - + Given(storage, .useRelativeDates(getter: false)) Given(interactor, .searchThreads(courseID: .any, searchText: .any, pageNumber: .any, willReturn: items)) viewModel.searchText = "Test" @@ -71,6 +73,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { let router = DiscussionRouterMock() let viewModel = DiscussionSearchTopicsViewModel(courseID: "123", interactor: interactor, + storage: CoreStorageMock(), router: router, debounce: .test) @@ -100,6 +103,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { let router = DiscussionRouterMock() let viewModel = DiscussionSearchTopicsViewModel(courseID: "123", interactor: interactor, + storage: CoreStorageMock(), router: router, debounce: .test) @@ -127,6 +131,7 @@ final class DiscussionSearchTopicsViewModelTests: XCTestCase { let router = DiscussionRouterMock() let viewModel = DiscussionSearchTopicsViewModel(courseID: "123", interactor: interactor, + storage: CoreStorageMock(), router: router, debounce: .test) diff --git a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift index 32f957a85..eea0bb06f 100644 --- a/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/DiscussionTopics/DiscussionTopicsViewModelTests.swift @@ -87,9 +87,8 @@ final class DiscussionTopicsViewModelTests: XCTestCase { XCTAssertNil(viewModel.topics) XCTAssertNil(viewModel.discussionTopics) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.slowOrNoInternetConnection) XCTAssertFalse(viewModel.isShowProgress) + XCTAssertFalse(viewModel.isShowRefresh) } func testGetTopicsUnknownError() async throws { @@ -113,8 +112,7 @@ final class DiscussionTopicsViewModelTests: XCTestCase { XCTAssertNil(viewModel.topics) XCTAssertNil(viewModel.discussionTopics) - XCTAssertTrue(viewModel.showError) - XCTAssertEqual(viewModel.errorMessage, CoreLocalization.Error.unknownError) XCTAssertFalse(viewModel.isShowProgress) + XCTAssertFalse(viewModel.isShowRefresh) } } diff --git a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift index 052930f5b..e55d82227 100644 --- a/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Posts/PostViewModelTests.swift @@ -102,14 +102,32 @@ final class PostViewModelTests: XCTestCase { ]) let discussionInfo = DiscussionInfo(discussionID: "1", blackouts: []) + + var interactor: DiscussionInteractorProtocolMock! + var router: DiscussionRouterMock! + var config: ConfigMock! + var viewModel: PostsViewModel! + + override func setUp() async throws { + try await super.setUp() + + interactor = DiscussionInteractorProtocolMock() + router = DiscussionRouterMock() + config = ConfigMock() + let storage = CoreStorageMock() + Given(storage, .useRelativeDates(getter: false)) + + viewModel = PostsViewModel( + interactor: interactor, + router: router, + config: config, + storage: storage + ) + } func testGetThreadListSuccess() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() var result = false - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) viewModel.courseID = "1" viewModel.type = .allPosts @@ -145,11 +163,8 @@ final class PostViewModelTests: XCTestCase { } func testGetThreadListNoInternetError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() var result = false - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) + viewModel.isBlackedOut = false let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -170,11 +185,8 @@ final class PostViewModelTests: XCTestCase { } func testGetThreadListUnknownError() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() var result = false - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) + viewModel.isBlackedOut = false Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willThrow: NSError())) @@ -193,11 +205,7 @@ final class PostViewModelTests: XCTestCase { } func testSortingAndFilters() async throws { - let interactor = DiscussionInteractorProtocolMock() - let router = DiscussionRouterMock() - let config = ConfigMock() - let viewModel = PostsViewModel(interactor: interactor, router: router, config: config) - + Given(interactor, .getThreadsList(courseID: .any, type: .any, sort: .any, filter: .any, page: .any, willReturn: threads)) Given(interactor, .getCourseDiscussionInfo(courseID: "1", willReturn: discussionInfo)) diff --git a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift index a7c59806a..2b010617d 100644 --- a/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift +++ b/Discussion/DiscussionTests/Presentation/Responses/ResponsesViewModelTests.swift @@ -108,6 +108,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .getCommentResponses(commentID: .any, page: .any, @@ -135,6 +136,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -161,6 +163,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .getCommentResponses(commentID: .any, page: .any, willThrow: NSError())) @@ -184,6 +187,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willReturn: post)) @@ -205,6 +209,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -228,6 +233,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) Given(interactor, .addCommentTo(threadID: .any, rawBody: .any, parentID: .any, willThrow: NSError())) @@ -249,6 +255,7 @@ final class ResponsesViewModelTests: XCTestCase { let viewModel = ResponsesViewModel(interactor: interactor, router: router, config: config, + storage: CoreStorageMock(), threadStateSubject: .init(.postAdded(id: "1"))) viewModel.totalPages = 2 diff --git a/Discussion/Mockfile b/Discussion/Mockfile index dc4c39594..b7eb63e2f 100644 --- a/Discussion/Mockfile +++ b/Discussion/Mockfile @@ -14,4 +14,5 @@ unit.tests.mock: - Discussion - Foundation - SwiftUI - - Combine \ No newline at end of file + - Combine + - OEXFoundation \ No newline at end of file diff --git a/Documentation/Theming_implementation.md b/Documentation/Theming_implementation.md index 33e5b577d..e9d5b37dd 100644 --- a/Documentation/Theming_implementation.md +++ b/Documentation/Theming_implementation.md @@ -45,9 +45,11 @@ project_config: config1: # Build Configuration name in project app_bundle_id: "bundle.id.app.new1" # Bundle ID to be set product_name: "Mobile App Name1" # App Name to be set + env_config: 'prod' # env name for this configuration. possible values: prod/dev/stage (values which config_settings.yaml defines) config2: # Build Configuration name in project app_bundle_id: "bundle.id.app.new2" # Bundle ID to be set product_name: "Mobile App Name2" # App Name to be set + env_config: 'dev' # env name for this configuration. possible values: prod/dev/stage (values which config_settings.yaml defines) ``` ### Assets The config `whitelabel.yaml` can contain a few Asset items (every added Xcode project can have its own Assets). diff --git a/Gemfile.lock b/Gemfile.lock index 3945f2d28..d9bc70b14 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,25 +5,25 @@ GEM base64 nkf rexml - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.918.0) - aws-sdk-core (3.192.1) + aws-partitions (1.998.0) + aws-sdk-core (3.211.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.79.0) - aws-sdk-core (~> 3, >= 3.191.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.147.0) - aws-sdk-core (~> 3, >= 3.192.0) + aws-sdk-kms (1.95.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.169.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -38,8 +38,8 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.110.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -60,15 +60,15 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.220.0) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -84,6 +84,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -109,6 +110,8 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -126,7 +129,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -147,31 +150,31 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.2) - jwt (2.8.1) + json (2.7.5) + jwt (2.9.3) base64 - mini_magick (4.12.0) + mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.4.0) - nanaimo (0.3.0) + multipart-post (2.4.1) + nanaimo (0.4.0) naturally (2.2.1) nkf (0.2.0) optparse (0.5.0) os (1.1.4) plist (3.7.1) - public_suffix (5.0.5) + public_suffix (6.0.1) rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.3.9) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -184,6 +187,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -193,18 +197,18 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) xcode-install (2.8.1) claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.24.0) + xcodeproj (1.26.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..0d88ddcfc --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +clean_translations: + rm -rf I18N/ + python3 i18n_scripts/translation.py --clean + +translation_requirements: + pip3 install -r i18n_scripts/requirements.txt + +pull_translations: clean_translations + atlas pull $(ATLAS_OPTIONS) translations/openedx-app-ios/I18N:I18N + python3 i18n_scripts/translation.py --split --replace-underscore --add-xcode-files + +extract_translations: clean_translations + python3 i18n_scripts/translation.py --combine diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index 2c94092fc..464e642a1 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 0218196528F734FA00202564 /* Discussion.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0218196328F734FA00202564 /* Discussion.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0219C67728F4347600D64452 /* Course.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0219C67628F4347600D64452 /* Course.framework */; }; 0219C67828F4347600D64452 /* Course.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0219C67628F4347600D64452 /* Course.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 022213D22C0E08E500B917E6 /* ProfilePersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022213D12C0E08E500B917E6 /* ProfilePersistence.swift */; }; 024E69202AEFC3FB00FA0B59 /* MainScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024E691F2AEFC3FB00FA0B59 /* MainScreenViewModel.swift */; }; 025AD4AC2A6FB95C00AB8FA7 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */; }; 025DE1A428DB4DAE0053E0F4 /* Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 025DE1A328DB4DAE0053E0F4 /* Profile.framework */; }; @@ -40,29 +41,29 @@ 0770DE4C28D0A462006D8A5D /* Authorization.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0770DE4A28D0A462006D8A5D /* Authorization.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0770DE5028D0A707006D8A5D /* NetworkAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0770DE4F28D0A707006D8A5D /* NetworkAssembly.swift */; }; 0770DE6428D0BCC7006D8A5D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0770DE6628D0BCC7006D8A5D /* Localizable.strings */; }; + 0780ABE82BFCA1530093A4A6 /* NotificationsEndpoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0780ABE72BFCA1530093A4A6 /* NotificationsEndpoints.swift */; }; 07A7D78F28F5C9060000BE81 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 07A7D78E28F5C9060000BE81 /* Core.framework */; }; 07A7D79028F5C9060000BE81 /* Core.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 07A7D78E28F5C9060000BE81 /* Core.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 07D5DA3528D075AA00752FD9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */; }; 07D5DA3E28D075AB00752FD9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */; }; - 0AF04487E2B198E7A14F037F /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FEBF1DCBDEE79A04144660EB /* Pods_App_OpenEdX.framework */; }; 149FF39E2B9F1AB50034B33F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 149FF39C2B9F1AB50034B33F /* LaunchScreen.storyboard */; }; + 1924EDE8164C7AB17AD4946B /* Pods_App_OpenEdX.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC49621A0942E5EE74BDC895 /* Pods_App_OpenEdX.framework */; }; A500668B2B613ED10024680B /* PushNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500668A2B613ED10024680B /* PushNotificationsManager.swift */; }; A500668D2B6143000024680B /* FCMProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A500668C2B6143000024680B /* FCMProvider.swift */; }; A50066912B61467B0024680B /* BrazeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50066902B61467B0024680B /* BrazeProvider.swift */; }; A50066932B614DCD0024680B /* FCMListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50066922B614DCD0024680B /* FCMListener.swift */; }; A50066952B614DEF0024680B /* BrazeListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50066942B614DEF0024680B /* BrazeListener.swift */; }; - A51CDBED2B6D2BEE009B6D4E /* SegmentBraze in Frameworks */ = {isa = PBXBuildFile; productRef = A51CDBEC2B6D2BEE009B6D4E /* SegmentBraze */; }; - A51CDBEF2B6D2BEE009B6D4E /* SegmentBrazeUI in Frameworks */ = {isa = PBXBuildFile; productRef = A51CDBEE2B6D2BEE009B6D4E /* SegmentBrazeUI */; }; A5462D9C2B864AE0003B96A5 /* BranchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5462D9B2B864AE0003B96A5 /* BranchService.swift */; }; - A5462D9F2B865713003B96A5 /* Segment in Frameworks */ = {isa = PBXBuildFile; productRef = A5462D9E2B865713003B96A5 /* Segment */; }; A59568952B61630500ED4F90 /* DeepLinkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59568942B61630500ED4F90 /* DeepLinkManager.swift */; }; A59568972B61653700ED4F90 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59568962B61653700ED4F90 /* DeepLink.swift */; }; A59568992B616D9400ED4F90 /* PushLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59568982B616D9400ED4F90 /* PushLink.swift */; }; - A59702292B83C87900CA064C /* FirebaseAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59702282B83C87900CA064C /* FirebaseAnalyticsService.swift */; }; - A5BD3E302B83B0F7006A8983 /* SegmentFirebase in Frameworks */ = {isa = PBXBuildFile; productRef = A5BD3E2F2B83B0F7006A8983 /* SegmentFirebase */; }; - A5C10D8F2B861A70008E864D /* SegmentAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C10D8E2B861A70008E864D /* SegmentAnalyticsService.swift */; }; - BA3042792B1F7147009B64B7 /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = BA3042782B1F7147009B64B7 /* MSAL */; }; BA7468762B96201D00793145 /* DeepLinkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7468752B96201D00793145 /* DeepLinkRouter.swift */; }; + CE0BF0BA2CD9203A00D10289 /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = CE0BF0B92CD9203A00D10289 /* MSAL */; }; + CE3BD14E2CBEB0DA0026F4E3 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3BD14D2CBEB0DA0026F4E3 /* PluginManager.swift */; }; + CE5712792CD1099B00D4AB17 /* OEXFirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = CE5712782CD1099B00D4AB17 /* OEXFirebaseAnalytics */; }; + CE57127A2CD109A800D4AB17 /* OEXFoundation in Embed Frameworks */ = {isa = PBXBuildFile; productRef = CE9C07D72CD104E5009C44D1 /* OEXFoundation */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CE924BE72CD8FAB3000137CA /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = CE924BE62CD8FAB3000137CA /* FirebaseMessaging */; }; + CE9C07D82CD104E5009C44D1 /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CE9C07D72CD104E5009C44D1 /* OEXFoundation */; }; E0D6E6A32B1626B10089F9C9 /* Theme.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0D6E6A22B1626B10089F9C9 /* Theme.framework */; }; E0D6E6A42B1626D60089F9C9 /* Theme.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E0D6E6A22B1626B10089F9C9 /* Theme.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -78,6 +79,7 @@ 072787B228D34D83002E9142 /* Discovery.framework in Embed Frameworks */, 0218196528F734FA00202564 /* Discussion.framework in Embed Frameworks */, 07A7D79028F5C9060000BE81 /* Core.framework in Embed Frameworks */, + CE57127A2CD109A800D4AB17 /* OEXFoundation in Embed Frameworks */, 0770DE4C28D0A462006D8A5D /* Authorization.framework in Embed Frameworks */, 0219C67828F4347600D64452 /* Course.framework in Embed Frameworks */, 025DE1A528DB4DAE0053E0F4 /* Profile.framework in Embed Frameworks */, @@ -93,6 +95,7 @@ 020CA5D82AA0A25300970AAF /* AppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStorage.swift; sourceTree = ""; }; 0218196328F734FA00202564 /* Discussion.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Discussion.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0219C67628F4347600D64452 /* Course.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Course.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 022213D12C0E08E500B917E6 /* ProfilePersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePersistence.swift; sourceTree = ""; }; 02450ABD29C35FF20094E2D0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 024E691F2AEFC3FB00FA0B59 /* MainScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenViewModel.swift; sourceTree = ""; }; 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; @@ -107,7 +110,6 @@ 0293A2082A6FCDE50090A336 /* DashboardPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardPersistence.swift; sourceTree = ""; }; 0298DF2F2A4EF7230023A257 /* AnalyticsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsManager.swift; sourceTree = ""; }; 02B6B3C428E1E61400232911 /* CourseDetails.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CourseDetails.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 02ED50CA29A64AAA008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02ED50D529A6554E008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = uk; path = "uk.lproj/сountries.json"; sourceTree = ""; }; 02ED50D729A65554008341CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = Base; path = "Base.lproj/сountries.json"; sourceTree = ""; }; 02ED50D929A66007008341CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = Base; path = Base.lproj/languages.json; sourceTree = ""; }; @@ -124,13 +126,14 @@ 0770DE4A28D0A462006D8A5D /* Authorization.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Authorization.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0770DE4F28D0A707006D8A5D /* NetworkAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAssembly.swift; sourceTree = ""; }; 0770DE6528D0BCC7006D8A5D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 0780ABE72BFCA1530093A4A6 /* NotificationsEndpoints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsEndpoints.swift; sourceTree = ""; }; 07A7D78E28F5C9060000BE81 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 07D5DA3128D075AA00752FD9 /* OpenEdX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenEdX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 07D5DA3428D075AA00752FD9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 07D5DA3D28D075AB00752FD9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 0F08B37CE833845FF5CD43E0 /* Pods-App-OpenEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasestage.xcconfig"; sourceTree = ""; }; 149FF39D2B9F1AB50034B33F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 5E9E8EEB795809CB9424EBA6 /* Pods-App-OpenEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releaseprod.xcconfig"; sourceTree = ""; }; + 1BB1F1D0FABF8788646FBAF2 /* Pods-App-OpenEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugstage.xcconfig"; sourceTree = ""; }; + 37C50995093E34142FDE0ED9 /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; A500668A2B613ED10024680B /* PushNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsManager.swift; sourceTree = ""; }; A500668C2B6143000024680B /* FCMProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FCMProvider.swift; sourceTree = ""; }; A50066902B61467B0024680B /* BrazeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrazeProvider.swift; sourceTree = ""; }; @@ -140,15 +143,14 @@ A59568942B61630500ED4F90 /* DeepLinkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkManager.swift; sourceTree = ""; }; A59568962B61653700ED4F90 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; A59568982B616D9400ED4F90 /* PushLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushLink.swift; sourceTree = ""; }; - A59702282B83C87900CA064C /* FirebaseAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseAnalyticsService.swift; sourceTree = ""; }; - A5C10D8E2B861A70008E864D /* SegmentAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentAnalyticsService.swift; sourceTree = ""; }; + A681C3929FC384F83BCB6648 /* Pods-App-OpenEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugprod.xcconfig"; sourceTree = ""; }; + AA8BE99557031F3F33F8037C /* Pods-App-OpenEdX.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasestage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasestage.xcconfig"; sourceTree = ""; }; BA7468752B96201D00793145 /* DeepLinkRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkRouter.swift; sourceTree = ""; }; - BEDEC3C1F88936685DCF7AE5 /* Pods-App-OpenEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugdev.xcconfig"; sourceTree = ""; }; - C9D8705F03FC185BBE66984C /* Pods-App-OpenEdX.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugprod.xcconfig"; sourceTree = ""; }; - D19FBC727E9AD036986A0B8D /* Pods-App-OpenEdX.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releasedev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releasedev.xcconfig"; sourceTree = ""; }; - E02715409EC1643835C9EFEE /* Pods-App-OpenEdX.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugstage.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugstage.xcconfig"; sourceTree = ""; }; + CE3BD14D2CBEB0DA0026F4E3 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = ""; }; + DAD1882A21DDAF1F67E4C546 /* Pods-App-OpenEdX.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.debugdev.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.debugdev.xcconfig"; sourceTree = ""; }; E0D6E6A22B1626B10089F9C9 /* Theme.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Theme.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - FEBF1DCBDEE79A04144660EB /* Pods_App_OpenEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_OpenEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FC49621A0942E5EE74BDC895 /* Pods_App_OpenEdX.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_OpenEdX.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FC8F87F82A110A0F7A1B0725 /* Pods-App-OpenEdX.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-OpenEdX.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-OpenEdX/Pods-App-OpenEdX.releaseprod.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -157,20 +159,19 @@ buildActionMask = 2147483647; files = ( E0D6E6A32B1626B10089F9C9 /* Theme.framework in Frameworks */, + CE5712792CD1099B00D4AB17 /* OEXFirebaseAnalytics in Frameworks */, 07A7D78F28F5C9060000BE81 /* Core.framework in Frameworks */, 028A37362ADFF404008CA604 /* WhatsNew.framework in Frameworks */, + CE9C07D82CD104E5009C44D1 /* OEXFoundation in Frameworks */, + CE0BF0BA2CD9203A00D10289 /* MSAL in Frameworks */, 025DE1A428DB4DAE0053E0F4 /* Profile.framework in Frameworks */, 0770DE4B28D0A462006D8A5D /* Authorization.framework in Frameworks */, - BA3042792B1F7147009B64B7 /* MSAL in Frameworks */, - A5462D9F2B865713003B96A5 /* Segment in Frameworks */, 072787B128D34D83002E9142 /* Discovery.framework in Frameworks */, + CE924BE72CD8FAB3000137CA /* FirebaseMessaging in Frameworks */, 0218196428F734FA00202564 /* Discussion.framework in Frameworks */, 0219C67728F4347600D64452 /* Course.framework in Frameworks */, 027DB33028D8A063002B6862 /* Dashboard.framework in Frameworks */, - A5BD3E302B83B0F7006A8983 /* SegmentFirebase in Frameworks */, - A51CDBED2B6D2BEE009B6D4E /* SegmentBraze in Frameworks */, - A51CDBEF2B6D2BEE009B6D4E /* SegmentBrazeUI in Frameworks */, - 0AF04487E2B198E7A14F037F /* Pods_App_OpenEdX.framework in Frameworks */, + 1924EDE8164C7AB17AD4946B /* Pods_App_OpenEdX.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -180,11 +181,13 @@ 0293A2012A6FC9E30090A336 /* Data */ = { isa = PBXGroup; children = ( + 0780ABE62BFC9C9D0093A4A6 /* Network */, 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */, 0293A2022A6FCA590090A336 /* CorePersistence.swift */, 0293A2042A6FCD430090A336 /* CoursePersistence.swift */, 0293A2062A6FCDA30090A336 /* DiscoveryPersistence.swift */, 0293A2082A6FCDE50090A336 /* DashboardPersistence.swift */, + 022213D12C0E08E500B917E6 /* ProfilePersistence.swift */, 020CA5D82AA0A25300970AAF /* AppStorage.swift */, ); path = Data; @@ -209,6 +212,14 @@ path = DI; sourceTree = ""; }; + 0780ABE62BFC9C9D0093A4A6 /* Network */ = { + isa = PBXGroup; + children = ( + 0780ABE72BFCA1530093A4A6 /* NotificationsEndpoints.swift */, + ); + path = Network; + sourceTree = ""; + }; 07D5DA2828D075AA00752FD9 = { isa = PBXGroup; children = ( @@ -263,7 +274,7 @@ 072787B028D34D83002E9142 /* Discovery.framework */, 0770DE4A28D0A462006D8A5D /* Authorization.framework */, 0770DE1228D07845006D8A5D /* Core.framework */, - FEBF1DCBDEE79A04144660EB /* Pods_App_OpenEdX.framework */, + FC49621A0942E5EE74BDC895 /* Pods_App_OpenEdX.framework */, ); name = Frameworks; sourceTree = ""; @@ -271,12 +282,12 @@ 55A895025FB07897BA68E063 /* Pods */ = { isa = PBXGroup; children = ( - C9D8705F03FC185BBE66984C /* Pods-App-OpenEdX.debugprod.xcconfig */, - E02715409EC1643835C9EFEE /* Pods-App-OpenEdX.debugstage.xcconfig */, - BEDEC3C1F88936685DCF7AE5 /* Pods-App-OpenEdX.debugdev.xcconfig */, - 5E9E8EEB795809CB9424EBA6 /* Pods-App-OpenEdX.releaseprod.xcconfig */, - 0F08B37CE833845FF5CD43E0 /* Pods-App-OpenEdX.releasestage.xcconfig */, - D19FBC727E9AD036986A0B8D /* Pods-App-OpenEdX.releasedev.xcconfig */, + A681C3929FC384F83BCB6648 /* Pods-App-OpenEdX.debugprod.xcconfig */, + 1BB1F1D0FABF8788646FBAF2 /* Pods-App-OpenEdX.debugstage.xcconfig */, + DAD1882A21DDAF1F67E4C546 /* Pods-App-OpenEdX.debugdev.xcconfig */, + FC8F87F82A110A0F7A1B0725 /* Pods-App-OpenEdX.releaseprod.xcconfig */, + AA8BE99557031F3F33F8037C /* Pods-App-OpenEdX.releasestage.xcconfig */, + 37C50995093E34142FDE0ED9 /* Pods-App-OpenEdX.releasedev.xcconfig */, ); path = Pods; sourceTree = ""; @@ -298,8 +309,7 @@ A59568932B6162E400ED4F90 /* DeepLinkManager */, A50066872B613E4B0024680B /* PushNotificationsManager */, A50066892B613E990024680B /* AnalyticsManager */, - A59702272B83C84800CA064C /* FirebaseAnalyticsService */, - A5C10D8D2B861A56008E864D /* SegmentAnalyticsService */, + CE3BD14D2CBEB0DA0026F4E3 /* PluginManager.swift */, ); path = Managers; sourceTree = ""; @@ -351,22 +361,6 @@ path = Link; sourceTree = ""; }; - A59702272B83C84800CA064C /* FirebaseAnalyticsService */ = { - isa = PBXGroup; - children = ( - A59702282B83C87900CA064C /* FirebaseAnalyticsService.swift */, - ); - path = FirebaseAnalyticsService; - sourceTree = ""; - }; - A5C10D8D2B861A56008E864D /* SegmentAnalyticsService */ = { - isa = PBXGroup; - children = ( - A5C10D8E2B861A70008E864D /* SegmentAnalyticsService.swift */, - ); - path = SegmentAnalyticsService; - sourceTree = ""; - }; A5F46FD02B692B140003EEEF /* Services */ = { isa = PBXGroup; children = ( @@ -390,7 +384,7 @@ isa = PBXNativeTarget; buildConfigurationList = 07D5DA4528D075AB00752FD9 /* Build configuration list for PBXNativeTarget "OpenEdX" */; buildPhases = ( - CC61D82AF6FA18BD30E5E8E5 /* [CP] Check Pods Manifest.lock */, + B9442FD26CE9A85A43FC2CFA /* [CP] Check Pods Manifest.lock */, 0770DE2328D08647006D8A5D /* SwiftLint */, 07D5DA2D28D075AA00752FD9 /* Sources */, 07D5DA2E28D075AA00752FD9 /* Frameworks */, @@ -405,11 +399,10 @@ ); name = OpenEdX; packageProductDependencies = ( - BA3042782B1F7147009B64B7 /* MSAL */, - A51CDBEC2B6D2BEE009B6D4E /* SegmentBraze */, - A51CDBEE2B6D2BEE009B6D4E /* SegmentBrazeUI */, - A5BD3E2F2B83B0F7006A8983 /* SegmentFirebase */, - A5462D9E2B865713003B96A5 /* Segment */, + CE9C07D72CD104E5009C44D1 /* OEXFoundation */, + CE5712782CD1099B00D4AB17 /* OEXFirebaseAnalytics */, + CE924BE62CD8FAB3000137CA /* FirebaseMessaging */, + CE0BF0B92CD9203A00D10289 /* MSAL */, ); productName = OpenEdX; productReference = 07D5DA3128D075AA00752FD9 /* OpenEdX.app */; @@ -441,10 +434,10 @@ ); mainGroup = 07D5DA2828D075AA00752FD9; packageReferences = ( - BA3042772B1F7147009B64B7 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */, - A51CDBEB2B6D2BEE009B6D4E /* XCRemoteSwiftPackageReference "braze-segment-swift" */, - A5BD3E2E2B83B0F7006A8983 /* XCRemoteSwiftPackageReference "analytics-swift-firebase" */, - A5462D9D2B865713003B96A5 /* XCRemoteSwiftPackageReference "analytics-swift" */, + CE9C07D62CD104E5009C44D1 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */, + CE9C07D92CD10581009C44D1 /* XCRemoteSwiftPackageReference "openedx-app-firebase-analytics-ios" */, + CE924BE52CD8FAB3000137CA /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + CE0BF0B82CD9203A00D10289 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */, ); productRefGroup = 07D5DA3228D075AA00752FD9 /* Products */; projectDirPath = ""; @@ -512,7 +505,7 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; - CC61D82AF6FA18BD30E5E8E5 /* [CP] Check Pods Manifest.lock */ = { + B9442FD26CE9A85A43FC2CFA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -560,6 +553,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0780ABE82BFCA1530093A4A6 /* NotificationsEndpoints.swift in Sources */, A500668B2B613ED10024680B /* PushNotificationsManager.swift in Sources */, 0293A2052A6FCD430090A336 /* CoursePersistence.swift in Sources */, 020CA5D92AA0A25300970AAF /* AppStorage.swift in Sources */, @@ -575,7 +569,6 @@ 0770DE1E28D084E8006D8A5D /* AppAssembly.swift in Sources */, A5462D9C2B864AE0003B96A5 /* BranchService.swift in Sources */, A50066932B614DCD0024680B /* FCMListener.swift in Sources */, - A59702292B83C87900CA064C /* FirebaseAnalyticsService.swift in Sources */, A500668D2B6143000024680B /* FCMProvider.swift in Sources */, 025AD4AC2A6FB95C00AB8FA7 /* DatabaseManager.swift in Sources */, 024E69202AEFC3FB00FA0B59 /* MainScreenViewModel.swift in Sources */, @@ -583,11 +576,12 @@ 0770DE2028D0858A006D8A5D /* Router.swift in Sources */, 0293A2092A6FCDE50090A336 /* DashboardPersistence.swift in Sources */, 0770DE1728D080A1006D8A5D /* RouteController.swift in Sources */, + CE3BD14E2CBEB0DA0026F4E3 /* PluginManager.swift in Sources */, A59568952B61630500ED4F90 /* DeepLinkManager.swift in Sources */, A50066952B614DEF0024680B /* BrazeListener.swift in Sources */, 071009C928D1DB3F00344290 /* ScreenAssembly.swift in Sources */, A59568972B61653700ED4F90 /* DeepLink.swift in Sources */, - A5C10D8F2B861A70008E864D /* SegmentAnalyticsService.swift in Sources */, + 022213D22C0E08E500B917E6 /* ProfilePersistence.swift in Sources */, A59568992B616D9400ED4F90 /* PushLink.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -617,7 +611,6 @@ isa = PBXVariantGroup; children = ( 0770DE6528D0BCC7006D8A5D /* en */, - 02ED50CA29A64AAA008341CD /* uk */, ); name = Localizable.strings; sourceTree = ""; @@ -695,22 +688,26 @@ }; 02DD1C9629E80CC200F35DCE /* DebugStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E02715409EC1643835C9EFEE /* Pods-App-OpenEdX.debugstage.xcconfig */; + baseConfigurationReference = 1BB1F1D0FABF8788646FBAF2 /* Pods-App-OpenEdX.debugstage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ""; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -783,22 +780,26 @@ }; 02DD1C9829E80CCB00F35DCE /* ReleaseStage */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0F08B37CE833845FF5CD43E0 /* Pods-App-OpenEdX.releasestage.xcconfig */; + baseConfigurationReference = AA8BE99557031F3F33F8037C /* Pods-App-OpenEdX.releasestage.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ""; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -877,22 +878,26 @@ }; 0727875928D231FD002E9142 /* DebugDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BEDEC3C1F88936685DCF7AE5 /* Pods-App-OpenEdX.debugdev.xcconfig */; + baseConfigurationReference = DAD1882A21DDAF1F67E4C546 /* Pods-App-OpenEdX.debugdev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ""; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -965,22 +970,26 @@ }; 0727875B28D23204002E9142 /* ReleaseDev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D19FBC727E9AD036986A0B8D /* Pods-App-OpenEdX.releasedev.xcconfig */; + baseConfigurationReference = 37C50995093E34142FDE0ED9 /* Pods-App-OpenEdX.releasedev.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ""; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1113,22 +1122,26 @@ }; 07D5DA4628D075AB00752FD9 /* DebugProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C9D8705F03FC185BBE66984C /* Pods-App-OpenEdX.debugprod.xcconfig */; + baseConfigurationReference = A681C3929FC384F83BCB6648 /* Pods-App-OpenEdX.debugprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ""; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1147,22 +1160,26 @@ }; 07D5DA4728D075AB00752FD9 /* ReleaseProd */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5E9E8EEB795809CB9424EBA6 /* Pods-App-OpenEdX.releaseprod.xcconfig */; + baseConfigurationReference = FC8F87F82A110A0F7A1B0725 /* Pods-App-OpenEdX.releaseprod.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = OpenEdX/OpenEdX.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; + FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ""; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; + INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; + INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1211,65 +1228,60 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - A51CDBEB2B6D2BEE009B6D4E /* XCRemoteSwiftPackageReference "braze-segment-swift" */ = { + CE0BF0B82CD9203A00D10289 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/braze-inc/braze-segment-swift"; + repositoryURL = "https://github.com/AzureAD/microsoft-authentication-library-for-objc"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.2.0; + minimumVersion = 1.6.1; }; }; - A5462D9D2B865713003B96A5 /* XCRemoteSwiftPackageReference "analytics-swift" */ = { + CE924BE52CD8FAB3000137CA /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/segmentio/analytics-swift.git"; + repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.5.3; + kind = exactVersion; + version = 11.3.0; }; }; - A5BD3E2E2B83B0F7006A8983 /* XCRemoteSwiftPackageReference "analytics-swift-firebase" */ = { + CE9C07D62CD104E5009C44D1 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/segment-integrations/analytics-swift-firebase"; + repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.3.5; + kind = exactVersion; + version = 1.0.0; }; }; - BA3042772B1F7147009B64B7 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */ = { + CE9C07D92CD10581009C44D1 /* XCRemoteSwiftPackageReference "openedx-app-firebase-analytics-ios" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/AzureAD/microsoft-authentication-library-for-objc"; + repositoryURL = "https://github.com/openedx/openedx-app-firebase-analytics-ios"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.2.19; + kind = exactVersion; + version = 1.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - A51CDBEC2B6D2BEE009B6D4E /* SegmentBraze */ = { - isa = XCSwiftPackageProductDependency; - package = A51CDBEB2B6D2BEE009B6D4E /* XCRemoteSwiftPackageReference "braze-segment-swift" */; - productName = SegmentBraze; - }; - A51CDBEE2B6D2BEE009B6D4E /* SegmentBrazeUI */ = { + CE0BF0B92CD9203A00D10289 /* MSAL */ = { isa = XCSwiftPackageProductDependency; - package = A51CDBEB2B6D2BEE009B6D4E /* XCRemoteSwiftPackageReference "braze-segment-swift" */; - productName = SegmentBrazeUI; + package = CE0BF0B82CD9203A00D10289 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */; + productName = MSAL; }; - A5462D9E2B865713003B96A5 /* Segment */ = { + CE5712782CD1099B00D4AB17 /* OEXFirebaseAnalytics */ = { isa = XCSwiftPackageProductDependency; - package = A5462D9D2B865713003B96A5 /* XCRemoteSwiftPackageReference "analytics-swift" */; - productName = Segment; + package = CE9C07D92CD10581009C44D1 /* XCRemoteSwiftPackageReference "openedx-app-firebase-analytics-ios" */; + productName = OEXFirebaseAnalytics; }; - A5BD3E2F2B83B0F7006A8983 /* SegmentFirebase */ = { + CE924BE62CD8FAB3000137CA /* FirebaseMessaging */ = { isa = XCSwiftPackageProductDependency; - package = A5BD3E2E2B83B0F7006A8983 /* XCRemoteSwiftPackageReference "analytics-swift-firebase" */; - productName = SegmentFirebase; + package = CE924BE52CD8FAB3000137CA /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseMessaging; }; - BA3042782B1F7147009B64B7 /* MSAL */ = { + CE9C07D72CD104E5009C44D1 /* OEXFoundation */ = { isa = XCSwiftPackageProductDependency; - package = BA3042772B1F7147009B64B7 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */; - productName = MSAL; + package = CE9C07D62CD104E5009C44D1 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/OpenEdX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OpenEdX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..62b5e29a9 --- /dev/null +++ b/OpenEdX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,204 @@ +{ + "originHash" : "7091edbbbbea71591e476364909f2e5f04e1211b9c58ac97e2712e9546afdd90", + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "194a6706acbd25e4ef639bcaddea16e8758a3e27", + "version" : "1.2024011602.0" + } + }, + { + "identity" : "analytics-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/segmentio/analytics-swift.git", + "state" : { + "revision" : "825bac9da99ca02bacf85bdf95f707d8e9f786d1", + "version" : "1.6.2" + } + }, + { + "identity" : "analytics-swift-firebase", + "kind" : "remoteSourceControl", + "location" : "https://github.com/segment-integrations/analytics-swift-firebase", + "state" : { + "revision" : "8d955ec9554869e9f1eaf2c265d439d12947f8b3", + "version" : "1.4.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "87dd288fc792bf9751e522e171a47df5b783b0b8", + "version" : "11.1.0" + } + }, + { + "identity" : "braze-segment-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/braze-inc/braze-segment-swift", + "state" : { + "revision" : "0f1fb36c89bf4f057e89c93ec0f1a159f33d8fd5", + "version" : "4.0.0" + } + }, + { + "identity" : "braze-swift-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/braze-inc/braze-swift-sdk", + "state" : { + "revision" : "f6b0226e04d19bb79f7fa57cf9f1aa56abe465ff", + "version" : "10.3.1" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk", + "state" : { + "revision" : "f909f901bfba9e27e4e9da83242a4915d6dd64bb", + "version" : "11.3.0" + } + }, + { + "identity" : "fullstory-swift-package-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/fullstorydev/fullstory-swift-package-ios", + "state" : { + "revision" : "5ba8ef3c359f676f4a5e56c7bf8d68fa998a6362", + "version" : "1.53.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "93406fd21b85e66e2d6dbf50b472161fd75c3f1f", + "version" : "11.3.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "617af071af9aa1d6a091d59a202910ac482128f9", + "version" : "10.1.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", + "version" : "8.0.2" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "f56d8fc3162de9a498377c7b6cea43431f4f5083", + "version" : "1.65.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" + } + }, + { + "identity" : "jsonsafeencoding-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/segmentio/jsonsafeencoding-swift.git", + "state" : { + "revision" : "af6a8b360984085e36c6341b21ecb35c12f47ebd", + "version" : "2.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1", + "version" : "1.22.5" + } + }, + { + "identity" : "microsoft-authentication-library-for-objc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc", + "state" : { + "revision" : "a20fd4c4587405da35723940d6ac0ee06a7b2b17", + "version" : "1.6.0" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage.git", + "state" : { + "revision" : "8a1be70a625683bc04d6903e2935bf23f3c6d609", + "version" : "5.19.7" + } + }, + { + "identity" : "sovran-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/segmentio/sovran-swift.git", + "state" : { + "revision" : "24867f3e4ac62027db9827112135e6531b6f4051", + "version" : "1.1.2" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "ebc7251dd5b37f627c93698e4374084d98409633", + "version" : "1.28.2" + } + } + ], + "version" : 3 +} diff --git a/OpenEdX/AppDelegate.swift b/OpenEdX/AppDelegate.swift index 59138b48c..d828970d6 100644 --- a/OpenEdX/AppDelegate.swift +++ b/OpenEdX/AppDelegate.swift @@ -7,22 +7,31 @@ import UIKit import Core +import OEXFoundation import Swinject import Profile import GoogleSignIn import FacebookCore import MSAL +import UserNotifications +import OEXFirebaseAnalytics +import FirebaseCore +import FirebaseMessaging import Theme +import BackgroundTasks @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { + static let bgAppTaskId = "openEdx.offlineProgressSync" + static var shared: AppDelegate { UIApplication.shared.delegate as! AppDelegate } var window: UIWindow? + private let pluginManager = PluginManager() private var assembler: Assembler? private var lastForceLogoutTime: TimeInterval = 0 @@ -32,6 +41,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { initDI() + initPlugins() + if let config = Container.shared.resolve(ConfigProtocol.self) { Theme.Shapes.isRoundedCorners = config.theme.isRoundedCorners @@ -42,6 +53,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) } configureDeepLinkServices(launchOptions: launchOptions) + + let pushManager = Container.shared.resolve(PushNotificationsManager.self) + + if config.firebase.enabled { + FirebaseApp.configure() + if config.firebase.cloudMessagingEnabled { + Messaging.messaging().delegate = pushManager + UNUserNotificationCenter.current().delegate = pushManager + } + } + + if pushManager?.hasProviders == true { + UIApplication.shared.registerForRemoteNotifications() + } } Theme.Fonts.registerFonts() @@ -49,17 +74,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window?.rootViewController = RouteController() window?.makeKeyAndVisible() window?.tintColor = Theme.UIColors.accentColor - + NotificationCenter.default.addObserver( self, - selector: #selector(forceLogoutUser), - name: .onTokenRefreshFailed, + selector: #selector(didUserAuthorize), + name: .userAuthorized, object: nil ) - if let pushManager = Container.shared.resolve(PushNotificationsManager.self) { - pushManager.performRegistration() - } + NotificationCenter.default.addObserver( + self, + selector: #selector(didUserLogout), + name: .userLoggedOut, + object: nil + ) return true } @@ -105,6 +133,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return false } + + private func initPlugins() { + guard let config = Container.shared.resolve(ConfigProtocol.self) else { return } + if config.firebase.enabled && config.firebase.isAnalyticsSourceFirebase { + pluginManager.addPlugin(analyticsService: FirebaseAnalyticsService()) + } + + // Initialize your plugins here + } private func initDI() { let navigation = UINavigationController() @@ -112,7 +149,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { assembler = Assembler( [ - AppAssembly(navigation: navigation), + AppAssembly(navigation: navigation, pluginManager: pluginManager), NetworkAssembly(), ScreenAssembly() ], @@ -120,21 +157,32 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) } - @objc private func forceLogoutUser() { + @objc private func didUserAuthorize() { + Container.shared.resolve(PushNotificationsManager.self)?.synchronizeToken() + } + + @objc func didUserLogout(_ notification: Notification) { guard Date().timeIntervalSince1970 - lastForceLogoutTime > 5 else { return } - let analyticsManager = Container.shared.resolve(AnalyticsManager.self) - analyticsManager?.userLogout(force: true) - - lastForceLogoutTime = Date().timeIntervalSince1970 - - Container.shared.resolve(CoreStorage.self)?.clear() - Task { - await Container.shared.resolve(DownloadManagerProtocol.self)?.deleteAllFiles() + if let userInfo = notification.userInfo, + userInfo[Notification.UserInfoKey.isForced] as? Bool == true { + let analyticsManager = Container.shared.resolve(AnalyticsManager.self) + analyticsManager?.userLogout(force: true) + + lastForceLogoutTime = Date().timeIntervalSince1970 + + Container.shared.resolve(CoreStorage.self)?.clear() + Container.shared.resolve(CorePersistenceProtocol.self)?.deleteAllProgress() + Task { + await Container.shared.resolve(DownloadManagerProtocol.self)?.deleteAllFiles() + } + Container.shared.resolve(CoreDataHandlerProtocol.self)?.clear() + window?.rootViewController = RouteController() } - Container.shared.resolve(CoreDataHandlerProtocol.self)?.clear() - window?.rootViewController = RouteController() + + UNUserNotificationCenter.current().removeAllDeliveredNotifications() + Container.shared.resolve(PushNotificationsManager.self)?.refreshToken() } // Push Notifications @@ -151,8 +199,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { - guard let pushManager = Container.shared.resolve(PushNotificationsManager.self) - else { + guard let pushManager = Container.shared.resolve(PushNotificationsManager.self) else { completionHandler(.newData) return } @@ -165,4 +212,46 @@ class AppDelegate: UIResponder, UIApplicationDelegate { guard let deepLinkManager = Container.shared.resolve(DeepLinkManager.self) else { return } deepLinkManager.configureDeepLinkService(launchOptions: launchOptions) } + + // Background progress update + + func registerBackgroundTask() { + let isRegistered = BGTaskScheduler.shared.register( + forTaskWithIdentifier: Self.bgAppTaskId, + using: nil + ) { task in + debugLog("Background task is executing: \(task.identifier)") + guard let task = task as? BGAppRefreshTask else { return } + self.handleAppRefreshTask(task: task) + } + debugLog("Is the background task registered? \(isRegistered)") + } + + func handleAppRefreshTask(task: BGAppRefreshTask) { + //In real case scenario we should check internet here + reScheduleAppRefresh() + + task.expirationHandler = { + //This Block call by System + //Canel your all tak's & queues + task.setTaskCompleted(success: true) + } + + let offlineSyncManager = Container.shared.resolve(OfflineSyncManagerProtocol.self)! + Task { + await offlineSyncManager.syncOfflineProgress() + task.setTaskCompleted(success: true) + } + } + + func reScheduleAppRefresh() { + let request = BGAppRefreshTaskRequest(identifier: Self.bgAppTaskId) + request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) // App Refresh after 60 minute. + //Note :: EarliestBeginDate should not be set to too far into the future. + do { + try BGTaskScheduler.shared.submit(request) + } catch { + debugLog("Could not schedule app refresh: \(error)") + } + } } diff --git a/OpenEdX/DI/AppAssembly.swift b/OpenEdX/DI/AppAssembly.swift index 12ee2d514..ffa6fb450 100644 --- a/OpenEdX/DI/AppAssembly.swift +++ b/OpenEdX/DI/AppAssembly.swift @@ -7,6 +7,8 @@ import UIKit import Core +import OEXFoundation +import OEXFirebaseAnalytics import Swinject import KeychainSwift import Discovery @@ -21,9 +23,11 @@ import WhatsNew class AppAssembly: Assembly { private let navigation: UINavigationController + private let pluginManager: PluginManager - init(navigation: UINavigationController) { + init(navigation: UINavigationController, pluginManager: PluginManager) { self.navigation = navigation + self.pluginManager = pluginManager } func assemble(container: Container) { @@ -31,14 +35,16 @@ class AppAssembly: Assembly { self.navigation }.inObjectScope(.container) + container.register(PluginManager.self) { _ in + self.pluginManager + }.inObjectScope(.container) + container.register(Router.self) { r in Router(navigationController: r.resolve(UINavigationController.self)!, container: container) } container.register(AnalyticsManager.self) { r in - AnalyticsManager( - config: r.resolve(ConfigProtocol.self)! - ) + AnalyticsManager(services: r.resolve(PluginManager.self)!.analyticsServices) } container.register(AuthorizationAnalytics.self) { r in @@ -168,15 +174,33 @@ class AppAssembly: Assembly { r.resolve(AppStorage.self)! }.inObjectScope(.container) + container.register(SSOHelper.self){ r in + SSOHelper( + keychain: r.resolve(KeychainSwift.self)! + ) + } + container.register(Validator.self) { _ in Validator() }.inObjectScope(.container) container.register(PushNotificationsManager.self) { r in PushNotificationsManager( + deepLinkManager: r.resolve(DeepLinkManager.self)!, + storage: r.resolve(CoreStorage.self)!, + api: r.resolve(API.self)!, config: r.resolve(ConfigProtocol.self)! ) }.inObjectScope(.container) + + container.register(CalendarManagerProtocol.self) { r in + CalendarManager( + persistence: r.resolve(ProfilePersistenceProtocol.self)!, + interactor: r.resolve(ProfileInteractorProtocol.self)!, + profileStorage: r.resolve(ProfileStorage.self)! + ) + } + .inObjectScope(.container) container.register(DeepLinkManager.self) { r in DeepLinkManager( @@ -190,24 +214,17 @@ class AppAssembly: Assembly { ) }.inObjectScope(.container) - container.register(SegmentAnalyticsService.self) { r in - SegmentAnalyticsService( - config: r.resolve(ConfigProtocol.self)! - ) - }.inObjectScope(.container) - - container.register(FirebaseAnalyticsService.self) { r in - FirebaseAnalyticsService( - config: r.resolve(ConfigProtocol.self)! - ) + container.register(FirebaseAnalyticsService.self) { _ in + FirebaseAnalyticsService() }.inObjectScope(.container) container.register(PipManagerProtocol.self) { r in - PipManager( + let config = r.resolve(ConfigProtocol.self)! + return PipManager( router: r.resolve(Router.self)!, discoveryInteractor: r.resolve(DiscoveryInteractorProtocol.self)!, courseInteractor: r.resolve(CourseInteractorProtocol.self)!, - isNestedListEnabled: r.resolve(ConfigProtocol.self)?.uiComponents.courseNestedListEnabled ?? false + courseDropDownNavigationEnabled: config.uiComponents.courseDropDownNavigationEnabled ) }.inObjectScope(.container) } diff --git a/OpenEdX/DI/NetworkAssembly.swift b/OpenEdX/DI/NetworkAssembly.swift index 83537fb29..904c1ea44 100644 --- a/OpenEdX/DI/NetworkAssembly.swift +++ b/OpenEdX/DI/NetworkAssembly.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import Alamofire import Swinject @@ -38,7 +39,7 @@ class NetworkAssembly: Assembly { }.inObjectScope(.container) container.register(API.self) {r in - API(session: r.resolve(Alamofire.Session.self)!, config: r.resolve(ConfigProtocol.self)!) + API(session: r.resolve(Alamofire.Session.self)!, baseURL: r.resolve(ConfigProtocol.self)!.baseURL) }.inObjectScope(.container) } } diff --git a/OpenEdX/DI/ScreenAssembly.swift b/OpenEdX/DI/ScreenAssembly.swift index ec0ed004c..551483b91 100644 --- a/OpenEdX/DI/ScreenAssembly.swift +++ b/OpenEdX/DI/ScreenAssembly.swift @@ -8,17 +8,39 @@ import Foundation import Swinject import Core +import OEXFoundation import Authorization import Discovery import Dashboard import Profile import Course import Discussion +import Combine // swiftlint:disable function_body_length type_body_length class ScreenAssembly: Assembly { func assemble(container: Container) { + // MARK: OfflineSync + container.register(OfflineSyncRepositoryProtocol.self) { r in + OfflineSyncRepository( + api: r.resolve(API.self)! + ) + } + container.register(OfflineSyncInteractorProtocol.self) { r in + OfflineSyncInteractor( + repository: r.resolve(OfflineSyncRepositoryProtocol.self)! + ) + } + + container.register(OfflineSyncManagerProtocol.self) { r in + OfflineSyncManager( + persistence: r.resolve(CorePersistenceProtocol.self)!, + interactor: r.resolve(OfflineSyncInteractorProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)! + ) + } + // MARK: Auth container.register(AuthRepositoryProtocol.self) { r in AuthRepository( @@ -38,7 +60,12 @@ class ScreenAssembly: Assembly { MainScreenViewModel( analytics: r.resolve(MainScreenAnalytics.self)!, config: r.resolve(ConfigProtocol.self)!, + router: r.resolve(Router.self)!, + syncManager: r.resolve(OfflineSyncManagerProtocol.self)!, profileInteractor: r.resolve(ProfileInteractorProtocol.self)!, + courseInteractor: r.resolve(CourseInteractorProtocol.self)!, + appStorage: r.resolve(AppStorage.self)!, + calendarManager: r.resolve(CalendarManagerProtocol.self)!, sourceScreen: sourceScreen ) } @@ -61,6 +88,15 @@ class ScreenAssembly: Assembly { sourceScreen: sourceScreen ) } + container.register(SSOWebViewModel.self) { r in + SSOWebViewModel( + interactor: r.resolve(AuthInteractorProtocol.self)!, + router: r.resolve(AuthorizationRouter.self)!, + config: r.resolve(ConfigProtocol.self)!, + analytics: r.resolve(AuthorizationAnalytics.self)!, + ssoHelper: r.resolve(SSOHelper.self)! + ) + } container.register(SignUpViewModel.self) { r, sourceScreen in SignUpViewModel( interactor: r.resolve(AuthInteractorProtocol.self)!, @@ -139,6 +175,7 @@ class ScreenAssembly: Assembly { connectivity: r.resolve(ConnectivityProtocol.self)!, router: r.resolve(DiscoveryRouter.self)!, analytics: r.resolve(DiscoveryAnalytics.self)!, + storage: r.resolve(CoreStorage.self)!, debounce: .searchDebounce ) } @@ -161,16 +198,41 @@ class ScreenAssembly: Assembly { repository: r.resolve(DashboardRepositoryProtocol.self)! ) } - container.register(DashboardViewModel.self) { r in - DashboardViewModel( + container.register(ListDashboardViewModel.self) { r in + ListDashboardViewModel( interactor: r.resolve(DashboardInteractorProtocol.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, - analytics: r.resolve(DashboardAnalytics.self)! + analytics: r.resolve(DashboardAnalytics.self)!, + storage: r.resolve(CoreStorage.self)! + ) + } + + container.register(PrimaryCourseDashboardViewModel.self) { r in + PrimaryCourseDashboardViewModel( + interactor: r.resolve(DashboardInteractorProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)!, + analytics: r.resolve(DashboardAnalytics.self)!, + config: r.resolve(ConfigProtocol.self)!, + storage: r.resolve(CoreStorage.self)! + ) + } + + container.register(AllCoursesViewModel.self) { r in + AllCoursesViewModel( + interactor: r.resolve(DashboardInteractorProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)!, + analytics: r.resolve(DashboardAnalytics.self)!, + storage: r.resolve(CoreStorage.self)! ) } // MARK: Profile + // MARK: Course + container.register(ProfilePersistenceProtocol.self) { r in + ProfilePersistence(context: r.resolve(DatabaseManager.self)!.context) + } + container.register(ProfileRepositoryProtocol.self) { r in ProfileRepository( api: r.resolve(API.self)!, @@ -188,7 +250,6 @@ class ScreenAssembly: Assembly { container.register(ProfileViewModel.self) { r in ProfileViewModel( interactor: r.resolve(ProfileInteractorProtocol.self)!, - downloadManager: r.resolve(DownloadManagerProtocol.self)!, router: r.resolve(ProfileRouter.self)!, analytics: r.resolve(ProfileAnalytics.self)!, config: r.resolve(ConfigProtocol.self)!, @@ -208,8 +269,35 @@ class ScreenAssembly: Assembly { container.register(SettingsViewModel.self) { r in SettingsViewModel( interactor: r.resolve(ProfileInteractorProtocol.self)!, + downloadManager: r.resolve(DownloadManagerProtocol.self)!, router: r.resolve(ProfileRouter.self)!, - analytics: r.resolve(CoreAnalytics.self)! + analytics: r.resolve(ProfileAnalytics.self)!, + coreAnalytics: r.resolve(CoreAnalytics.self)!, + config: r.resolve(ConfigProtocol.self)!, + corePersistence: r.resolve(CorePersistenceProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)! + ) + } + + container.register(DatesAndCalendarViewModel.self) { r in + DatesAndCalendarViewModel( + router: r.resolve(ProfileRouter.self)!, + interactor: r.resolve(ProfileInteractorProtocol.self)!, + profileStorage: r.resolve(ProfileStorage.self)!, + persistence: r.resolve(ProfilePersistenceProtocol.self)!, + calendarManager: r.resolve(CalendarManagerProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)! + ) + } + .inObjectScope(.weak) + + container.register(ManageAccountViewModel.self) { r in + ManageAccountViewModel( + router: r.resolve(ProfileRouter.self)!, + analytics: r.resolve(ProfileAnalytics.self)!, + config: r.resolve(ConfigProtocol.self)!, + connectivity: r.resolve(ConnectivityProtocol.self)!, + interactor: r.resolve(ProfileInteractorProtocol.self)! ) } @@ -255,7 +343,7 @@ class ScreenAssembly: Assembly { // MARK: CourseScreensView container.register( CourseContainerViewModel.self - ) { r, isActive, courseStart, courseEnd, enrollmentStart, enrollmentEnd in + ) { r, isActive, courseStart, courseEnd, enrollmentStart, enrollmentEnd, selection, lastVisitedBlockID in CourseContainerViewModel( interactor: r.resolve(CourseInteractorProtocol.self)!, authInteractor: r.resolve(AuthInteractorProtocol.self)!, @@ -270,7 +358,9 @@ class ScreenAssembly: Assembly { courseEnd: courseEnd, enrollmentStart: enrollmentStart, enrollmentEnd: enrollmentEnd, - coreAnalytics: r.resolve(CoreAnalytics.self)! + lastVisitedBlockID: lastVisitedBlockID, + coreAnalytics: r.resolve(CoreAnalytics.self)!, + selection: selection ) } @@ -308,45 +398,114 @@ class ScreenAssembly: Assembly { } container.register(WebUnitViewModel.self) { r in - WebUnitViewModel(authInteractor: r.resolve(AuthInteractorProtocol.self)!, - config: r.resolve(ConfigProtocol.self)!) + WebUnitViewModel( + authInteractor: r.resolve(AuthInteractorProtocol.self)!, + config: r.resolve(ConfigProtocol.self)!, + syncManager: r.resolve(OfflineSyncManagerProtocol.self)! + ) } container.register( YouTubeVideoPlayerViewModel.self - ) { r, url, blockID, courseID, languages, playerStateSubject in - YouTubeVideoPlayerViewModel( - url: url, - blockID: blockID, - courseID: courseID, + ) { (r, url: URL?, blockID: String, courseID: String, languages: [SubtitleUrl], playerStateSubject: CurrentValueSubject) in + let router: Router = r.resolve(Router.self)! + return YouTubeVideoPlayerViewModel( languages: languages, playerStateSubject: playerStateSubject, - interactor: r.resolve(CourseInteractorProtocol.self)!, - router: r.resolve(CourseRouter.self)!, - appStorage: r.resolve(CoreStorage.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, - pipManager: r.resolve(PipManagerProtocol.self)! + playerHolder: r.resolve( + YoutubePlayerViewControllerHolder.self, + arguments: url, + blockID, + courseID, + router.currentCourseTabSelection + )! ) } - container.register( - EncodedVideoPlayerViewModel.self - ) { r, url, blockID, courseID, languages, playerStateSubject in + container.register(EncodedVideoPlayerViewModel.self) { (r, url: URL?, blockID: String, courseID: String, languages: [SubtitleUrl], playerStateSubject: CurrentValueSubject) in let router: Router = r.resolve(Router.self)! + + let holder = r.resolve( + PlayerViewControllerHolder.self, + arguments: url, + blockID, + courseID, + router.currentCourseTabSelection + )! return EncodedVideoPlayerViewModel( - url: url, - blockID: blockID, - courseID: courseID, languages: languages, playerStateSubject: playerStateSubject, - interactor: r.resolve(CourseInteractorProtocol.self)!, - router: r.resolve(CourseRouter.self)!, - appStorage: r.resolve(CoreStorage.self)!, connectivity: r.resolve(ConnectivityProtocol.self)!, + playerHolder: holder + ) + } + + container.register(PlayerDelegateProtocol.self) { _, manager in + PlayerDelegate(pipManager: manager) + } + + container.register(YoutubePlayerTracker.self) { (_, url) in + YoutubePlayerTracker(url: url) + } + + container.register(PlayerTracker.self) { (_, url) in + PlayerTracker(url: url) + } + + container.register( + YoutubePlayerViewControllerHolder.self + ) { r, url, blockID, courseID, selectedCourseTab in + YoutubePlayerViewControllerHolder( + url: url, + blockID: blockID, + courseID: courseID, + selectedCourseTab: selectedCourseTab, + videoResolution: .zero, pipManager: r.resolve(PipManagerProtocol.self)!, - selectedCourseTab: router.currentCourseTabSelection + playerTracker: r.resolve(YoutubePlayerTracker.self, argument: url)!, + playerDelegate: nil, + playerService: r.resolve(PlayerServiceProtocol.self, arguments: courseID, blockID)! ) } + + container.register( + PlayerViewControllerHolder.self + ) { (r, url: URL?, blockID: String, courseID: String, selectedCourseTab: Int) in + let pipManager = r.resolve(PipManagerProtocol.self)! + if let holder = pipManager.holder( + for: url, + blockID: blockID, + courseID: courseID, + selectedCourseTab: selectedCourseTab + ) as? PlayerViewControllerHolder { + return holder + } + + let storage = r.resolve(CoreStorage.self)! + let quality = storage.userSettings?.streamingQuality ?? .auto + let tracker = r.resolve(PlayerTracker.self, argument: url)! + let delegate = r.resolve(PlayerDelegateProtocol.self, argument: pipManager)! + let holder = PlayerViewControllerHolder( + url: url, + blockID: blockID, + courseID: courseID, + selectedCourseTab: selectedCourseTab, + videoResolution: quality.resolution, + pipManager: pipManager, + playerTracker: tracker, + playerDelegate: delegate, + playerService: r.resolve(PlayerServiceProtocol.self, arguments: courseID, blockID)! + ) + delegate.playerHolder = holder + return holder + } + + container.register(PlayerServiceProtocol.self) { r, courseID, blockID in + let interactor = r.resolve(CourseInteractorProtocol.self)! + let router = r.resolve(CourseRouter.self)! + return PlayerService(courseID: courseID, blockID: blockID, interactor: interactor, router: router) + } container.register(HandoutsViewModel.self) { r, courseID in HandoutsViewModel( @@ -368,7 +527,8 @@ class ScreenAssembly: Assembly { config: r.resolve(ConfigProtocol.self)!, courseID: courseID, courseName: courseName, - analytics: r.resolve(CourseAnalytics.self)! + analytics: r.resolve(CourseAnalytics.self)!, + calendarManager: r.resolve(CalendarManagerProtocol.self)! ) } @@ -402,6 +562,7 @@ class ScreenAssembly: Assembly { DiscussionSearchTopicsViewModel( courseID: courseID, interactor: r.resolve(DiscussionInteractorProtocol.self)!, + storage: r.resolve(CoreStorage.self)!, router: r.resolve(DiscussionRouter.self)!, debounce: .searchDebounce ) @@ -411,7 +572,8 @@ class ScreenAssembly: Assembly { PostsViewModel( interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, - config: r.resolve(ConfigProtocol.self)! + config: r.resolve(ConfigProtocol.self)!, + storage: r.resolve(CoreStorage.self)! ) } @@ -420,6 +582,7 @@ class ScreenAssembly: Assembly { interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, config: r.resolve(ConfigProtocol.self)!, + storage: r.resolve(CoreStorage.self)!, postStateSubject: subject ) } @@ -429,6 +592,7 @@ class ScreenAssembly: Assembly { interactor: r.resolve(DiscussionInteractorProtocol.self)!, router: r.resolve(DiscussionRouter.self)!, config: r.resolve(ConfigProtocol.self)!, + storage: r.resolve(CoreStorage.self)!, threadStateSubject: subject ) } diff --git a/OpenEdX/Data/AppStorage.swift b/OpenEdX/Data/AppStorage.swift index ff17e4128..d064da7aa 100644 --- a/OpenEdX/Data/AppStorage.swift +++ b/OpenEdX/Data/AppStorage.swift @@ -11,6 +11,7 @@ import Core import Profile import WhatsNew import Course +import Theme public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseStorage { @@ -34,45 +35,29 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } } } - - public var reviewLastShownVersion: String? { + + public var refreshToken: String? { get { - return userDefaults.string(forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + return keychain.get(KEY_REFRESH_TOKEN) } set(newValue) { if let newValue { - userDefaults.set(newValue, forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + keychain.set(newValue, forKey: KEY_REFRESH_TOKEN) } else { - userDefaults.removeObject(forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + keychain.delete(KEY_REFRESH_TOKEN) } } } - public var lastReviewDate: Date? { + public var pushToken: String? { get { - guard let dateString = userDefaults.string(forKey: KEY_REVIEW_LAST_REVIEW_DATE) else { - return nil - } - return Date(iso8601: dateString) + return keychain.get(KEY_PUSH_TOKEN) } set(newValue) { if let newValue { - userDefaults.set(newValue.dateToString(style: .iso8601), forKey: KEY_REVIEW_LAST_REVIEW_DATE) + keychain.set(newValue, forKey: KEY_PUSH_TOKEN) } else { - userDefaults.removeObject(forKey: KEY_REVIEW_LAST_REVIEW_DATE) - } - } - } - - public var refreshToken: String? { - get { - return keychain.get(KEY_REFRESH_TOKEN) - } - set(newValue) { - if let newValue { - keychain.set(newValue, forKey: KEY_REFRESH_TOKEN) - } else { - keychain.delete(KEY_REFRESH_TOKEN) + keychain.delete(KEY_PUSH_TOKEN) } } } @@ -103,9 +88,9 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } } - public var cookiesDate: String? { + public var cookiesDate: Date? { get { - return userDefaults.string(forKey: KEY_COOKIES_DATE) + return userDefaults.object(forKey: KEY_COOKIES_DATE) as? Date } set(newValue) { if let newValue { @@ -116,6 +101,41 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } } + public var reviewLastShownVersion: String? { + get { + return userDefaults.string(forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue, forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + } else { + userDefaults.removeObject(forKey: KEY_REVIEW_LAST_SHOWN_VERSION) + } + } + } + + public var lastReviewDate: Date? { + get { + guard let dateString = userDefaults.string(forKey: KEY_REVIEW_LAST_REVIEW_DATE) else { + return nil + } + return Date(iso8601: dateString) + } + set(newValue) { + if let newValue { + userDefaults.set( + newValue.dateToString( + style: .iso8601, + useRelativeDates: false + ), + forKey: KEY_REVIEW_LAST_REVIEW_DATE + ) + } else { + userDefaults.removeObject(forKey: KEY_REVIEW_LAST_REVIEW_DATE) + } + } + } + public var whatsNewVersion: String? { get { return userDefaults.string(forKey: KEY_WHATSNEW_VERSION) @@ -203,16 +223,134 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto } } } + + public var calendarSettings: CalendarSettings? { + get { + guard let userJson = userDefaults.data(forKey: KEY_CALENDAR_SETTINGS) else { + return nil + } + return try? JSONDecoder().decode(CalendarSettings.self, from: userJson) + } + set(newValue) { + if let settings = newValue { + let encoder = JSONEncoder() + if let encoded = try? encoder.encode(settings) { + userDefaults.set(encoded, forKey: KEY_CALENDAR_SETTINGS) + } + } else { + userDefaults.set(nil, forKey: KEY_CALENDAR_SETTINGS) + } + } + } + + public var resetAppSupportDirectoryUserData: Bool? { + get { + return userDefaults.bool(forKey: KEY_RESET_APP_SUPPORT_DIRECTORY_USER_DATA) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue, forKey: KEY_RESET_APP_SUPPORT_DIRECTORY_USER_DATA) + } else { + userDefaults.removeObject(forKey: KEY_RESET_APP_SUPPORT_DIRECTORY_USER_DATA) + } + } + } + + public var lastCalendarName: String? { + get { + return userDefaults.string(forKey: KEY_LAST_CALENDAR_NAME) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue, forKey: KEY_LAST_CALENDAR_NAME) + } else { + userDefaults.removeObject(forKey: KEY_LAST_CALENDAR_NAME) + } + } + } + + public var lastLoginUsername: String? { + get { + return userDefaults.string(forKey: KEY_LAST_LOGIN_USERNAME) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue, forKey: KEY_LAST_LOGIN_USERNAME) + } else { + userDefaults.removeObject(forKey: KEY_LAST_LOGIN_USERNAME) + } + } + } + + public var lastCalendarUpdateDate: Date? { + get { + guard let dateString = userDefaults.string(forKey: KEY_LAST_CALENDAR_UPDATE_DATE) else { + return nil + } + return Date(iso8601: dateString) + } + set(newValue) { + if let newValue { + userDefaults.set( + newValue.dateToString( + style: .iso8601, + useRelativeDates: useRelativeDates + ), + forKey: KEY_LAST_CALENDAR_UPDATE_DATE + ) + } else { + userDefaults.removeObject(forKey: KEY_LAST_CALENDAR_UPDATE_DATE) + } + } + } + + public var hideInactiveCourses: Bool? { + get { + return userDefaults.bool(forKey: KEY_HIDE_INACTIVE_COURSES) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue, forKey: KEY_HIDE_INACTIVE_COURSES) + } else { + userDefaults.removeObject(forKey: KEY_HIDE_INACTIVE_COURSES) + } + } + } + + public var firstCalendarUpdate: Bool? { + get { + return userDefaults.bool(forKey: KEY_FIRST_CALENDAR_UPDATE) + } + set(newValue) { + if let newValue { + userDefaults.set(newValue, forKey: KEY_FIRST_CALENDAR_UPDATE) + } else { + userDefaults.removeObject(forKey: KEY_FIRST_CALENDAR_UPDATE) + } + } + } + public var useRelativeDates: Bool { + get { + // We use userDefaults.object to return the default value as true + return userDefaults.object(forKey: KEY_USE_RELATIVE_DATES) as? Bool ?? true + } + set { + userDefaults.set(newValue, forKey: KEY_USE_RELATIVE_DATES) + } + } + public func clear() { accessToken = nil refreshToken = nil cookiesDate = nil user = nil + userProfile = nil } private let KEY_ACCESS_TOKEN = "accessToken" private let KEY_REFRESH_TOKEN = "refreshToken" + private let KEY_PUSH_TOKEN = "pushToken" private let KEY_COOKIES_DATE = "cookiesDate" private let KEY_USER_PROFILE = "userProfile" private let KEY_USER = "refreshToken" @@ -223,4 +361,12 @@ public class AppStorage: CoreStorage, ProfileStorage, WhatsNewStorage, CourseSto private let KEY_APPLE_SIGN_FULLNAME = "appleSignFullName" private let KEY_APPLE_SIGN_EMAIL = "appleSignEmail" private let KEY_ALLOWED_DOWNLOAD_LARGE_FILE = "allowedDownloadLargeFile" + private let KEY_CALENDAR_SETTINGS = "calendarSettings" + private let KEY_LAST_LOGIN_USERNAME = "lastLoginUsername" + private let KEY_LAST_CALENDAR_NAME = "lastCalendarName" + private let KEY_LAST_CALENDAR_UPDATE_DATE = "lastCalendarUpdateDate" + private let KEY_HIDE_INACTIVE_COURSES = "hideInactiveCourses" + private let KEY_FIRST_CALENDAR_UPDATE = "firstCalendarUpdate" + private let KEY_RESET_APP_SUPPORT_DIRECTORY_USER_DATA = "resetAppSupportDirectoryUserData" + private let KEY_USE_RELATIVE_DATES = "useRelativeDates" } diff --git a/OpenEdX/Data/CorePersistence.swift b/OpenEdX/Data/CorePersistence.swift index 8af067d07..80ffcf83c 100644 --- a/OpenEdX/Data/CorePersistence.swift +++ b/OpenEdX/Data/CorePersistence.swift @@ -6,12 +6,36 @@ // import Core +import OEXFoundation import Foundation import CoreData import Combine public class CorePersistence: CorePersistenceProtocol { - + struct CorePersistenceHelper { + static func fetchCDDownloadData( + predicate: CDPredicate? = nil, + fetchLimit: Int? = nil, + context: NSManagedObjectContext, + userId: Int32? + ) throws -> [CDDownloadData] { + let request = CDDownloadData.fetchRequest() + if let predicate = predicate { + request.predicate = predicate.predicate + } + if let fetchLimit = fetchLimit { + request.fetchLimit = fetchLimit + } + let data = try context.fetch(request).filter { + guard let userId = userId else { + return true + } + debugLog(userId, "-userId-") + return $0.userId == userId + } + return data + } + } // MARK: - Predicate enum CDPredicate { @@ -53,119 +77,170 @@ public class CorePersistence: CorePersistenceProtocol { public func addToDownloadQueue( blocks: [CourseBlock], downloadQuality: DownloadQuality - ) { + ) async { + let userId = getUserId32() ?? 0 for block in blocks { let downloadDataId = downloadDataId(from: block.id) + + await context.perform { [weak self] in + guard let self else { return } + let data = try? CorePersistenceHelper.fetchCDDownloadData( + predicate: CDPredicate.id(downloadDataId), + context: self.context, + userId: userId + ) + guard data?.first == nil else { return } + + var fileExtension: String? + var url: String? + var fileSize: Int32? + var fileName: String? + + if let html = block.offlineDownload { + let fileUrl = html.fileUrl + url = fileUrl + fileSize = Int32(html.fileSize) + fileExtension = URL(string: fileUrl)?.pathExtension + if let folderName = URL(string: fileUrl)?.lastPathComponent, + let folderUrl = URL(string: folderName)?.deletingPathExtension() { + fileName = folderUrl.absoluteString + } + saveDownloadData() + } else if let encodedVideo = block.encodedVideo, + let video = encodedVideo.video(downloadQuality: downloadQuality), + let videoUrl = video.url { + url = videoUrl + if let videoFileSize = video.fileSize { + fileSize = Int32(videoFileSize) + } + fileExtension = URL(string: videoUrl)?.pathExtension + fileName = "\(block.id).\(fileExtension ?? "")" + saveDownloadData() + } else { return } + + func saveDownloadData() { + let newDownloadData = CDDownloadData(context: context) + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + newDownloadData.id = downloadDataId + newDownloadData.blockId = block.id + newDownloadData.userId = userId + newDownloadData.courseId = block.courseId + newDownloadData.url = url + newDownloadData.fileName = fileName + newDownloadData.displayName = block.displayName + if let lastModified = block.offlineDownload?.lastModified { + newDownloadData.lastModified = lastModified + } + newDownloadData.progress = .zero + newDownloadData.resumeData = nil + newDownloadData.state = DownloadState.waiting.rawValue + newDownloadData.type = block.offlineDownload != nil + ? DownloadType.html.rawValue + : DownloadType.video.rawValue + newDownloadData.fileSize = Int32(fileSize ?? 0) + } + } + } + } - let data = try? fetchCDDownloadData( - predicate: CDPredicate.id(downloadDataId) - ) - guard data?.first == nil else { continue } - - guard let video = block.encodedVideo?.video(downloadQuality: downloadQuality), - let url = video.url, - let fileExtension = URL(string: url)?.pathExtension - else { continue } - - let fileName = "\(block.id).\(fileExtension)" + public func addToDownloadQueue(tasks: [DownloadDataTask]) { + for task in tasks { context.performAndWait { let newDownloadData = CDDownloadData(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - newDownloadData.id = downloadDataId - newDownloadData.blockId = block.id - newDownloadData.userId = getUserId32() ?? 0 - newDownloadData.courseId = block.courseId - newDownloadData.url = url - newDownloadData.fileName = fileName - newDownloadData.displayName = block.displayName + newDownloadData.id = task.id + newDownloadData.blockId = task.blockId + newDownloadData.userId = Int32(task.userId) + newDownloadData.courseId = task.courseId + newDownloadData.url = task.url + newDownloadData.fileName = task.fileName + newDownloadData.displayName = task.displayName + newDownloadData.lastModified = task.lastModified newDownloadData.progress = .zero newDownloadData.resumeData = nil newDownloadData.state = DownloadState.waiting.rawValue - newDownloadData.type = DownloadType.video.rawValue - newDownloadData.fileSize = Int32(video.fileSize ?? 0) + newDownloadData.type = task.type.rawValue + newDownloadData.fileSize = Int32(task.fileSize) } } } - public func getDownloadDataTasks(completion: @escaping ([DownloadDataTask]) -> Void) { - context.performAndWait { - guard let data = try? fetchCDDownloadData() else { - completion([]) - return + public func getDownloadDataTasks() async -> [DownloadDataTask] { + let userId = getUserId32() ?? 0 + return await context.perform {[context] in + guard let data = try? CorePersistenceHelper.fetchCDDownloadData( + context: context, + userId: userId + ) else { + return [] } let downloads = data.downloadDataTasks() - completion(downloads) + return downloads } } public func getDownloadDataTasksForCourse( - _ courseId: String, - completion: @escaping ([DownloadDataTask]) -> Void - ) { - context.performAndWait { - guard let data = try? fetchCDDownloadData( - predicate: .courseId(courseId) + _ courseId: String + ) async -> [DownloadDataTask] { + let uID = userId + let int32Id = getUserId32() + return await context.perform {[context] in + guard let data = try? CorePersistenceHelper.fetchCDDownloadData( + predicate: .courseId(courseId), + context: context, + userId: int32Id ) else { - completion([]) - return + return [] } if data.isEmpty { - completion([]) - return + return [] } let downloads = data .downloadDataTasks() - .filter(userId: userId) + .filter(userId: uID) - completion(downloads) + return downloads } } - public func downloadDataTask( - for blockId: String, - completion: @escaping (DownloadDataTask?) -> Void - ) { - context.performAndWait { - let data = try? fetchCDDownloadData( - predicate: .id(downloadDataId(from: blockId)) + public func downloadDataTask(for blockId: String) -> DownloadDataTask? { + let dataId = downloadDataId(from: blockId) + let userId = getUserId32() + return context.performAndWait {[context] in + let data = try? CorePersistenceHelper.fetchCDDownloadData( + predicate: .id(dataId), + context: context, + userId: userId ) guard let downloadData = data?.first else { - completion(nil) - return + return nil } - let downloadDataTask = DownloadDataTask(sourse: downloadData) - - completion(downloadDataTask) + return DownloadDataTask(sourse: downloadData) } } - public func downloadDataTask(for blockId: String) -> DownloadDataTask? { - let data = try? fetchCDDownloadData( - predicate: .id(downloadDataId(from: blockId)) - ) - - guard let downloadData = data?.first else { return nil } - - return DownloadDataTask(sourse: downloadData) - } - - public func nextBlockForDownloading() -> DownloadDataTask? { - let data = try? fetchCDDownloadData( - predicate: .state(DownloadState.finished.rawValue), - fetchLimit: 1 - ) - - guard let downloadData = data?.first else { - return nil + public func nextBlockForDownloading() async -> DownloadDataTask? { + let userId = getUserId32() + return await context.perform {[context] in + let data = try? CorePersistenceHelper.fetchCDDownloadData( + predicate: .state(DownloadState.finished.rawValue), + fetchLimit: 1, + context: context, + userId: userId + ) + + guard let downloadData = data?.first else { + return nil + } + + return DownloadDataTask(sourse: downloadData) } - - return DownloadDataTask(sourse: downloadData) } public func updateDownloadState( @@ -173,9 +248,13 @@ public class CorePersistence: CorePersistenceProtocol { state: DownloadState, resumeData: Data? ) { - context.performAndWait { - guard let data = try? fetchCDDownloadData( - predicate: .id(downloadDataId(from: id)) + let dataId = downloadDataId(from: id) + let userId = getUserId32() + context.perform {[context] in + guard let data = try? CorePersistenceHelper.fetchCDDownloadData( + predicate: .id(dataId), + context: context, + userId: userId ) else { return } @@ -194,11 +273,15 @@ public class CorePersistence: CorePersistenceProtocol { } } - public func deleteDownloadDataTask(id: String) throws { - context.performAndWait { + public func deleteDownloadDataTask(id: String) async throws { + let dataId = downloadDataId(from: id) + let userId = getUserId32() + return await context.perform {[context] in do { - let records = try fetchCDDownloadData( - predicate: .id(downloadDataId(from: id)) + let records = try CorePersistenceHelper.fetchCDDownloadData( + predicate: .id(dataId), + context: context, + userId: userId ) for record in records { @@ -214,7 +297,7 @@ public class CorePersistence: CorePersistenceProtocol { } public func saveDownloadDataTask(_ task: DownloadDataTask) { - context.performAndWait { + context.perform {[context] in let newDownloadData = CDDownloadData(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump newDownloadData.id = task.id @@ -260,6 +343,85 @@ public class CorePersistence: CorePersistenceProtocol { }) .eraseToAnyPublisher() } + + // MARK: - Offline Progress + public func saveOfflineProgress(progress: OfflineProgress) { + context.performAndWait { + let progressForSaving = CDOfflineProgress(context: context) + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + progressForSaving.blockID = progress.blockID + progressForSaving.progressJson = progress.progressJson + + do { + try context.save() + } catch { + debugLog("⛔️⛔️⛔️⛔️⛔️", error) + } + } + } + + public func loadProgress(for blockID: String) -> OfflineProgress? { + context.performAndWait { + let request = CDOfflineProgress.fetchRequest() + request.predicate = NSPredicate(format: "blockID = %@", blockID) + guard let progress = try? context.fetch(request).first, + let savedBlockID = progress.blockID, + let progressJson = progress.progressJson, + blockID == savedBlockID else { return nil } + + return OfflineProgress( + progressJson: progressJson + ) + } + } + + public func loadAllOfflineProgress() -> [OfflineProgress] { + context.performAndWait { + let result = try? context.fetch(CDOfflineProgress.fetchRequest()) + .map { + OfflineProgress( + progressJson: $0.progressJson ?? "" + )} + if let result, !result.isEmpty { + return result + } else { + return [] + } + } + } + + public func deleteProgress(for blockID: String) { + context.performAndWait { + let request = CDOfflineProgress.fetchRequest() + request.predicate = NSPredicate(format: "blockID = %@", blockID) + guard let progress = try? context.fetch(request).first else { return } + + do { + context.delete(progress) + try context.save() + debugLog("File erased successfully") + } catch { + debugLog("Error deleteing progress: \(error.localizedDescription)") + } + } + } + + public func deleteAllProgress() { + context.performAndWait { + let request = CDOfflineProgress.fetchRequest() + guard let allProgress = try? context.fetch(request) else { return } + + do { + for progress in allProgress { + context.delete(progress) + try context.save() + debugLog("File erased successfully") + } + } catch { + debugLog("Error deleteing progress: \(error.localizedDescription)") + } + } + } // MARK: - Private Intents @@ -268,20 +430,28 @@ public class CorePersistence: CorePersistenceProtocol { fetchLimit: Int? = nil ) throws -> [CDDownloadData] { let request = CDDownloadData.fetchRequest() + + var predicates = [NSPredicate]() + if let predicate = predicate { - request.predicate = predicate.predicate + predicates.append(predicate.predicate) + } + + if let userId = getUserId32() { + let userIdNumber = NSNumber(value: userId) + let userIdPredicate = NSPredicate(format: "userId == %@", userIdNumber) + predicates.append(userIdPredicate) } + + if !predicates.isEmpty { + request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) + } + if let fetchLimit = fetchLimit { request.fetchLimit = fetchLimit } - let data = try context.fetch(request).filter { - guard let userId = getUserId32() else { - return true - } - debugLog(userId, "-userId-") - return $0.userId == userId - } - return data + + return try context.fetch(request) } private func getUserId32() -> Int32? { diff --git a/OpenEdX/Data/CoursePersistence.swift b/OpenEdX/Data/CoursePersistence.swift index e2fc37e54..4115de9f2 100644 --- a/OpenEdX/Data/CoursePersistence.swift +++ b/OpenEdX/Data/CoursePersistence.swift @@ -18,39 +18,43 @@ public class CoursePersistence: CoursePersistenceProtocol { self.context = context } - public func loadEnrollments() throws -> [CourseItem] { - let result = try? context.fetch(CDCourseItem.fetchRequest()) - .map { - CourseItem(name: $0.name ?? "", - org: $0.org ?? "", - shortDescription: $0.desc ?? "", - imageURL: $0.imageURL ?? "", - isActive: $0.isActive, - courseStart: $0.courseStart, - courseEnd: $0.courseEnd, - enrollmentStart: $0.enrollmentStart, - enrollmentEnd: $0.enrollmentEnd, - courseID: $0.courseID ?? "", - numPages: Int($0.numPages), - coursesCount: Int($0.courseCount))} - if let result, !result.isEmpty { - return result - } else { - throw NoCachedDataError() + public func loadEnrollments() async throws -> [CourseItem] { + try await context.perform { [context] in + let result = try? context.fetch(CDCourseItem.fetchRequest()) + .map { + CourseItem(name: $0.name ?? "", + org: $0.org ?? "", + shortDescription: $0.desc ?? "", + imageURL: $0.imageURL ?? "", + hasAccess: $0.hasAccess, + courseStart: $0.courseStart, + courseEnd: $0.courseEnd, + enrollmentStart: $0.enrollmentStart, + enrollmentEnd: $0.enrollmentEnd, + courseID: $0.courseID ?? "", + numPages: Int($0.numPages), + coursesCount: Int($0.courseCount), + courseRawImage: $0.courseRawImage, + progressEarned: 0, + progressPossible: 0) + } + if let result, !result.isEmpty { + return result + } else { + throw NoCachedDataError() + } } } public func saveEnrollments(items: [CourseItem]) { - context.performAndWait { + context.perform {[context] in for item in items { let newItem = CDCourseItem(context: context) newItem.name = item.name newItem.org = item.org newItem.desc = item.shortDescription newItem.imageURL = item.imageURL - if let isActive = item.isActive { - newItem.isActive = isActive - } + newItem.hasAccess = item.hasAccess newItem.courseStart = item.courseStart newItem.courseEnd = item.courseEnd newItem.enrollmentStart = item.enrollmentStart @@ -68,84 +72,103 @@ public class CoursePersistence: CoursePersistenceProtocol { } } - public func loadCourseStructure(courseID: String) throws -> DataLayer.CourseStructure { - let request = CDCourseStructure.fetchRequest() - request.predicate = NSPredicate(format: "id = %@", courseID) - guard let structure = try? context.fetch(request).first else { throw NoCachedDataError() } - - let requestBlocks = CDCourseBlock.fetchRequest() - requestBlocks.predicate = NSPredicate(format: "courseID = %@", courseID) - - let blocks = try? context.fetch(requestBlocks).map { - let userViewData = DataLayer.CourseDetailUserViewData( - transcripts: $0.transcripts?.jsonStringToDictionary() as? [String: String], - encodedVideo: DataLayer.CourseDetailEncodedVideoData( - youTube: DataLayer.EncodedVideoData( - url: $0.youTube?.url, - fileSize: Int($0.youTube?.fileSize ?? 0) - ), - fallback: DataLayer.EncodedVideoData( - url: $0.fallback?.url, - fileSize: Int($0.fallback?.fileSize ?? 0) - ), - desktopMP4: DataLayer.EncodedVideoData( - url: $0.desktopMP4?.url, - fileSize: Int($0.desktopMP4?.fileSize ?? 0) - ), - mobileHigh: DataLayer.EncodedVideoData( - url: $0.mobileHigh?.url, - fileSize: Int($0.mobileHigh?.fileSize ?? 0) + public func loadCourseStructure(courseID: String) async throws -> DataLayer.CourseStructure { + try await context.perform {[context] in + let request = CDCourseStructure.fetchRequest() + request.predicate = NSPredicate(format: "id = %@", courseID) + guard let structure = try? context.fetch(request).first else { throw NoCachedDataError() } + + let requestBlocks = CDCourseBlock.fetchRequest() + requestBlocks.predicate = NSPredicate(format: "courseID = %@", courseID) + + let blocks = try? context.fetch(requestBlocks).map { + let userViewData = DataLayer.CourseDetailUserViewData( + transcripts: $0.transcripts?.jsonStringToDictionary() as? [String: String], + encodedVideo: DataLayer.CourseDetailEncodedVideoData( + youTube: DataLayer.EncodedVideoData( + url: $0.youTube?.url, + fileSize: $0.youTube?.fileSize == nil ? nil : Int($0.youTube!.fileSize) + ), + fallback: DataLayer.EncodedVideoData( + url: $0.fallback?.url, + fileSize: $0.fallback?.fileSize == nil ? nil : Int($0.fallback!.fileSize) + ), + desktopMP4: DataLayer.EncodedVideoData( + url: $0.desktopMP4?.url, + fileSize: $0.desktopMP4?.fileSize == nil ? nil : Int($0.desktopMP4!.fileSize) + ), + mobileHigh: DataLayer.EncodedVideoData( + url: $0.mobileHigh?.url, + fileSize: $0.mobileHigh?.fileSize == nil ? nil : Int($0.mobileHigh!.fileSize) + ), + mobileLow: DataLayer.EncodedVideoData( + url: $0.mobileLow?.url, + fileSize: $0.mobileLow?.fileSize == nil ? nil : Int($0.mobileLow!.fileSize) + ), + hls: DataLayer.EncodedVideoData( + url: $0.hls?.url, + fileSize: $0.hls?.fileSize == nil ? nil : Int($0.hls!.fileSize) + ) ), - mobileLow: DataLayer.EncodedVideoData( - url: $0.mobileLow?.url, - fileSize: Int($0.mobileLow?.fileSize ?? 0) + topicID: "" + ) + return DataLayer.CourseBlock( + blockId: $0.blockId ?? "", + id: $0.id ?? "", + graded: $0.graded, + due: $0.due, + completion: $0.completion, + studentUrl: $0.studentUrl ?? "", + webUrl: $0.webUrl ?? "", + type: $0.type ?? "", + displayName: $0.displayName ?? "", + descendants: $0.descendants, + allSources: $0.allSources, + userViewData: userViewData, + multiDevice: $0.multiDevice, + assignmentProgress: DataLayer.AssignmentProgress( + assignmentType: $0.assignmentType, + numPointsEarned: $0.numPointsEarned, + numPointsPossible: $0.numPointsPossible ), - hls: DataLayer.EncodedVideoData( - url: $0.hls?.url, - fileSize: Int($0.hls?.fileSize ?? 0) + offlineDownload: DataLayer.OfflineDownload( + fileUrl: $0.fileUrl, + lastModified: $0.lastModified, + fileSize: Int($0.fileSize) + ) + ) + } + + let dictionary = blocks?.reduce(into: [:]) { result, block in + result[block.id] = block + } ?? [:] + + return DataLayer.CourseStructure( + rootItem: structure.rootItem ?? "", + dict: dictionary, + id: structure.id ?? "", + media: DataLayer.CourseMedia( + image: DataLayer.Image( + raw: structure.mediaRaw ?? "", + small: structure.mediaSmall ?? "", + large: structure.mediaLarge ?? "" ) ), - topicID: "" - ) - return DataLayer.CourseBlock( - blockId: $0.blockId ?? "", - id: $0.id ?? "", - graded: $0.graded, - completion: $0.completion, - studentUrl: $0.studentUrl ?? "", - webUrl: $0.webUrl ?? "", - type: $0.type ?? "", - displayName: $0.displayName ?? "", - descendants: $0.descendants, - allSources: $0.allSources, - userViewData: userViewData, - multiDevice: $0.multiDevice + certificate: DataLayer.Certificate(url: structure.certificate), + org: structure.org ?? "", + isSelfPaced: structure.isSelfPaced, + courseProgress: DataLayer.CourseProgress( + assignmentsCompleted: Int(structure.assignmentsCompleted), + totalAssignmentsCount: Int(structure.totalAssignmentsCount) + ) ) } - let dictionary = blocks?.reduce(into: [:]) { result, block in - result[block.id] = block - } ?? [:] - - return DataLayer.CourseStructure( - rootItem: structure.rootItem ?? "", - dict: dictionary, - id: structure.id ?? "", - media: DataLayer.CourseMedia( - image: DataLayer.Image( - raw: structure.mediaRaw ?? "", - small: structure.mediaSmall ?? "", - large: structure.mediaLarge ?? "" - ) - ), - certificate: DataLayer.Certificate(url: structure.certificate), - org: structure.org ?? "", - isSelfPaced: structure.isSelfPaced - ) } public func saveCourseStructure(structure: DataLayer.CourseStructure) { - context.performAndWait { + context.perform {[context] in + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let newStructure = CDCourseStructure(context: self.context) newStructure.certificate = structure.certificate?.url newStructure.mediaSmall = structure.media.image.small @@ -154,6 +177,8 @@ public class CoursePersistence: CoursePersistenceProtocol { newStructure.id = structure.id newStructure.rootItem = structure.rootItem newStructure.isSelfPaced = structure.isSelfPaced + newStructure.totalAssignmentsCount = Int32(structure.courseProgress?.totalAssignmentsCount ?? 0) + newStructure.assignmentsCompleted = Int32(structure.courseProgress?.assignmentsCompleted ?? 0) for block in Array(structure.dict.values) { let courseDetail = CDCourseBlock(context: self.context) @@ -168,6 +193,27 @@ public class CoursePersistence: CoursePersistenceProtocol { courseDetail.type = block.type courseDetail.completion = block.completion ?? 0 courseDetail.multiDevice = block.multiDevice ?? false + if let numPointsEarned = block.assignmentProgress?.numPointsEarned { + courseDetail.numPointsEarned = numPointsEarned + } + if let numPointsPossible = block.assignmentProgress?.numPointsPossible { + courseDetail.numPointsPossible = numPointsPossible + } + if let assignmentType = block.assignmentProgress?.assignmentType { + courseDetail.assignmentType = assignmentType + } + if let due = block.due { + courseDetail.due = due + } + + if let offlineDownload = block.offlineDownload, + let fileSize = offlineDownload.fileSize, + let fileUrl = offlineDownload.fileUrl, + let lastModified = offlineDownload.lastModified { + courseDetail.fileSize = Int64(fileSize) + courseDetail.fileUrl = fileUrl + courseDetail.lastModified = lastModified + } if block.userViewData?.encodedVideo?.youTube != nil { let youTube = CDCourseBlockVideo(context: self.context) @@ -231,7 +277,7 @@ public class CoursePersistence: CoursePersistenceProtocol { } public func saveSubtitles(url: String, subtitlesString: String) { - context.performAndWait { + context.perform {[context] in let newSubtitle = CDSubtitle(context: context) newSubtitle.url = url newSubtitle.subtitle = subtitlesString @@ -245,16 +291,18 @@ public class CoursePersistence: CoursePersistenceProtocol { } } - public func loadSubtitles(url: String) -> String? { - let request = CDSubtitle.fetchRequest() - request.predicate = NSPredicate(format: "url = %@", url) - - guard let subtitle = try? context.fetch(request).first, - let loaded = subtitle.uploadedAt else { return nil } - if Date().timeIntervalSince1970 - loaded.timeIntervalSince1970 < 5 * 3600 { - return subtitle.subtitle ?? "" + public func loadSubtitles(url: String) async -> String? { + await context.perform {[context] in + let request = CDSubtitle.fetchRequest() + request.predicate = NSPredicate(format: "url = %@", url) + + guard let subtitle = try? context.fetch(request).first, + let loaded = subtitle.uploadedAt else { return nil } + if Date().timeIntervalSince1970 - loaded.timeIntervalSince1970 < 5 * 3600 { + return subtitle.subtitle ?? "" + } + return nil } - return nil } public func saveCourseDates(courseID: String, courseDates: CourseDates) { diff --git a/OpenEdX/Data/DashboardPersistence.swift b/OpenEdX/Data/DashboardPersistence.swift index 06241c00f..9cac9921b 100644 --- a/OpenEdX/Data/DashboardPersistence.swift +++ b/OpenEdX/Data/DashboardPersistence.swift @@ -18,42 +18,49 @@ public class DashboardPersistence: DashboardPersistenceProtocol { self.context = context } - public func loadMyCourses() throws -> [CourseItem] { - let result = try? context.fetch(CDDashboardCourse.fetchRequest()) - .map { CourseItem(name: $0.name ?? "", - org: $0.org ?? "", - shortDescription: $0.desc ?? "", - imageURL: $0.imageURL ?? "", - isActive: nil, - courseStart: $0.courseStart, - courseEnd: $0.courseEnd, - enrollmentStart: $0.enrollmentStart, - enrollmentEnd: $0.enrollmentEnd, - courseID: $0.courseID ?? "", - numPages: Int($0.numPages), - coursesCount: Int($0.courseCount))} - if let result, !result.isEmpty { - return result - } else { - throw NoCachedDataError() + public func loadEnrollments() async throws -> [CourseItem] { + try await context.perform {[context] in + let result = try? context.fetch(CDDashboardCourse.fetchRequest()) + .map { CourseItem(name: $0.name ?? "", + org: $0.org ?? "", + shortDescription: $0.desc ?? "", + imageURL: $0.imageURL ?? "", + hasAccess: $0.hasAccess, + courseStart: $0.courseStart, + courseEnd: $0.courseEnd, + enrollmentStart: $0.enrollmentStart, + enrollmentEnd: $0.enrollmentEnd, + courseID: $0.courseID ?? "", + numPages: Int($0.numPages), + coursesCount: Int($0.courseCount), + courseRawImage: $0.courseRawImage, + progressEarned: 0, + progressPossible: 0)} + if let result, !result.isEmpty { + return result + } else { + throw NoCachedDataError() + } } } - public func saveMyCourses(items: [CourseItem]) { + public func saveEnrollments(items: [CourseItem]) { for item in items { - context.performAndWait { - let newItem = CDDashboardCourse(context: context) + context.perform {[context] in + let newItem = CDDashboardCourse(context: self.context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump newItem.name = item.name newItem.org = item.org newItem.desc = item.shortDescription newItem.imageURL = item.imageURL + newItem.hasAccess = item.hasAccess newItem.courseStart = item.courseStart newItem.courseEnd = item.courseEnd newItem.enrollmentStart = item.enrollmentStart newItem.enrollmentEnd = item.enrollmentEnd newItem.numPages = Int32(item.numPages) newItem.courseID = item.courseID + newItem.courseRawImage = item.courseRawImage do { try context.save() @@ -63,4 +70,183 @@ public class DashboardPersistence: DashboardPersistenceProtocol { } } } + + public func loadPrimaryEnrollment() async throws -> PrimaryEnrollment { + try await context.perform {[context] in + let request = CDMyEnrollments.fetchRequest() + if let result = try context.fetch(request).first { + let primaryCourse = result.primaryCourse.flatMap { cdPrimaryCourse -> PrimaryCourse? in + + let futureAssignments = (cdPrimaryCourse.futureAssignments as? Set ?? []) + .map { future in + return Assignment( + type: future.type ?? "", + title: future.title ?? "", + description: future.descript ?? "", + date: future.date ?? Date(), + complete: future.complete, + firstComponentBlockId: future.firstComponentBlockId + ) + } + + let pastAssignments = (cdPrimaryCourse.pastAssignments as? Set ?? []) + .map { past in + return Assignment( + type: past.type ?? "", + title: past.title ?? "", + description: past.descript ?? "", + date: past.date ?? Date(), + complete: past.complete, + firstComponentBlockId: past.firstComponentBlockId + ) + } + + return PrimaryCourse( + name: cdPrimaryCourse.name ?? "", + org: cdPrimaryCourse.org ?? "", + courseID: cdPrimaryCourse.courseID ?? "", + hasAccess: cdPrimaryCourse.hasAccess, + courseStart: cdPrimaryCourse.courseStart, + courseEnd: cdPrimaryCourse.courseEnd, + courseBanner: cdPrimaryCourse.courseBanner ?? "", + futureAssignments: futureAssignments, + pastAssignments: pastAssignments, + progressEarned: Int(cdPrimaryCourse.progressEarned), + progressPossible: Int(cdPrimaryCourse.progressPossible), + lastVisitedBlockID: cdPrimaryCourse.lastVisitedBlockID ?? "", + resumeTitle: cdPrimaryCourse.resumeTitle + ) + } + + let courses = (result.courses as? Set ?? []) + .map { cdCourse in + return CourseItem( + name: cdCourse.name ?? "", + org: cdCourse.org ?? "", + shortDescription: cdCourse.desc ?? "", + imageURL: cdCourse.imageURL ?? "", + hasAccess: cdCourse.hasAccess, + courseStart: cdCourse.courseStart, + courseEnd: cdCourse.courseEnd, + enrollmentStart: cdCourse.enrollmentStart, + enrollmentEnd: cdCourse.enrollmentEnd, + courseID: cdCourse.courseID ?? "", + numPages: Int(cdCourse.numPages), + coursesCount: Int(cdCourse.courseCount), + courseRawImage: cdCourse.courseRawImage, + progressEarned: Int(cdCourse.progressEarned), + progressPossible: Int(cdCourse.progressPossible) + ) + } + + return PrimaryEnrollment( + primaryCourse: primaryCourse, + courses: courses, + totalPages: Int(result.totalPages), + count: Int(result.count) + ) + } else { + throw NoCachedDataError() + } + } + } + + // swiftlint:disable function_body_length + public func savePrimaryEnrollment(enrollments: PrimaryEnrollment) { + // Deleting all old data before saving new ones + clearOldEnrollmentsData() + context.perform {[context] in + let newEnrollment = CDMyEnrollments(context: context) + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + + // Saving new courses + newEnrollment.courses = NSSet(array: enrollments.courses.map { course in + let cdCourse = CDDashboardCourse(context: self.context) + cdCourse.name = course.name + cdCourse.org = course.org + cdCourse.desc = course.shortDescription + cdCourse.imageURL = course.imageURL + cdCourse.courseStart = course.courseStart + cdCourse.courseEnd = course.courseEnd + cdCourse.enrollmentStart = course.enrollmentStart + cdCourse.enrollmentEnd = course.enrollmentEnd + cdCourse.courseID = course.courseID + cdCourse.numPages = Int32(course.numPages) + cdCourse.hasAccess = course.hasAccess + cdCourse.progressEarned = Int32(course.progressEarned) + cdCourse.progressPossible = Int32(course.progressPossible) + return cdCourse + }) + + // Saving PrimaryCourse + if let primaryCourse = enrollments.primaryCourse { + let cdPrimaryCourse = CDPrimaryCourse(context: context) + + let futureAssignments = primaryCourse.futureAssignments.map { assignment in + let cdAssignment = CDAssignment(context: self.context) + cdAssignment.type = assignment.type + cdAssignment.title = assignment.title + cdAssignment.descript = assignment.description + cdAssignment.date = assignment.date + cdAssignment.complete = assignment.complete + cdAssignment.firstComponentBlockId = assignment.firstComponentBlockId + return cdAssignment + } + cdPrimaryCourse.futureAssignments = NSSet(array: futureAssignments) + + let pastAssignments = primaryCourse.pastAssignments.map { assignment in + let cdAssignment = CDAssignment(context: self.context) + cdAssignment.type = assignment.type + cdAssignment.title = assignment.title + cdAssignment.descript = assignment.description + cdAssignment.date = assignment.date + cdAssignment.complete = assignment.complete + cdAssignment.firstComponentBlockId = assignment.firstComponentBlockId + return cdAssignment + } + cdPrimaryCourse.pastAssignments = NSSet(array: pastAssignments) + + cdPrimaryCourse.name = primaryCourse.name + cdPrimaryCourse.org = primaryCourse.org + cdPrimaryCourse.courseID = primaryCourse.courseID + cdPrimaryCourse.hasAccess = primaryCourse.hasAccess + cdPrimaryCourse.courseStart = primaryCourse.courseStart + cdPrimaryCourse.courseEnd = primaryCourse.courseEnd + cdPrimaryCourse.courseBanner = primaryCourse.courseBanner + cdPrimaryCourse.progressEarned = Int32(primaryCourse.progressEarned) + cdPrimaryCourse.progressPossible = Int32(primaryCourse.progressPossible) + cdPrimaryCourse.lastVisitedBlockID = primaryCourse.lastVisitedBlockID + cdPrimaryCourse.resumeTitle = primaryCourse.resumeTitle + + newEnrollment.primaryCourse = cdPrimaryCourse + } + + newEnrollment.totalPages = Int32(enrollments.totalPages) + newEnrollment.count = Int32(enrollments.count) + + do { + try context.save() + } catch { + print("Error when saving MyEnrollments:", error) + } + } + } + // swiftlint:enable function_body_length + + func clearOldEnrollmentsData() { + context.performAndWait {[context] in + let fetchRequest1: NSFetchRequest = CDDashboardCourse.fetchRequest() + let batchDeleteRequest1 = NSBatchDeleteRequest(fetchRequest: fetchRequest1) + + let fetchRequest2: NSFetchRequest = CDMyEnrollments.fetchRequest() + let batchDeleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2) + + do { + try context.execute(batchDeleteRequest1) + try context.execute(batchDeleteRequest2) + } catch { + print("Error when deleting old data:", error) + } + } + } } diff --git a/OpenEdX/Data/DatabaseManager.swift b/OpenEdX/Data/DatabaseManager.swift index b8f71b346..57cd98d45 100644 --- a/OpenEdX/Data/DatabaseManager.swift +++ b/OpenEdX/Data/DatabaseManager.swift @@ -11,6 +11,7 @@ import Core import Discovery import Dashboard import Course +import Profile class DatabaseManager: CoreDataHandlerProtocol { @@ -20,7 +21,8 @@ class DatabaseManager: CoreDataHandlerProtocol { Bundle(for: CoreBundle.self), Bundle(for: DiscoveryBundle.self), Bundle(for: DashboardBundle.self), - Bundle(for: CourseBundle.self) + Bundle(for: CourseBundle.self), + Bundle(for: ProfileBundle.self) ] private lazy var persistentContainer: NSPersistentContainer = { diff --git a/OpenEdX/Data/DiscoveryPersistence.swift b/OpenEdX/Data/DiscoveryPersistence.swift index 189264f41..282b107ee 100644 --- a/OpenEdX/Data/DiscoveryPersistence.swift +++ b/OpenEdX/Data/DiscoveryPersistence.swift @@ -18,45 +18,49 @@ public class DiscoveryPersistence: DiscoveryPersistenceProtocol { self.context = context } - public func loadDiscovery() throws -> [CourseItem] { - let result = try? context.fetch(CDDiscoveryCourse.fetchRequest()) - .map { CourseItem(name: $0.name ?? "", - org: $0.org ?? "", - shortDescription: $0.desc ?? "", - imageURL: $0.imageURL ?? "", - isActive: $0.isActive, - courseStart: $0.courseStart, - courseEnd: $0.courseEnd, - enrollmentStart: $0.enrollmentStart, - enrollmentEnd: $0.enrollmentEnd, - courseID: $0.courseID ?? "", - numPages: Int($0.numPages), - coursesCount: Int($0.courseCount))} - if let result, !result.isEmpty { - return result - } else { - throw NoCachedDataError() + public func loadDiscovery() async throws -> [CourseItem] { + try await context.perform {[context] in + let result = try? context.fetch(CDDiscoveryCourse.fetchRequest()) + .map { CourseItem(name: $0.name ?? "", + org: $0.org ?? "", + shortDescription: $0.desc ?? "", + imageURL: $0.imageURL ?? "", + hasAccess: $0.hasAccess, + courseStart: $0.courseStart, + courseEnd: $0.courseEnd, + enrollmentStart: $0.enrollmentStart, + enrollmentEnd: $0.enrollmentEnd, + courseID: $0.courseID ?? "", + numPages: Int($0.numPages), + coursesCount: Int($0.courseCount), + courseRawImage: $0.courseRawImage, + progressEarned: 0, + progressPossible: 0)} + if let result, !result.isEmpty { + return result + } else { + throw NoCachedDataError() + } } } public func saveDiscovery(items: [CourseItem]) { for item in items { - context.performAndWait { + context.perform {[context] in let newItem = CDDiscoveryCourse(context: context) context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump newItem.name = item.name newItem.org = item.org newItem.desc = item.shortDescription newItem.imageURL = item.imageURL - if let isActive = item.isActive { - newItem.isActive = isActive - } + newItem.hasAccess = item.hasAccess newItem.courseStart = item.courseStart newItem.courseEnd = item.courseEnd newItem.enrollmentStart = item.enrollmentStart newItem.enrollmentEnd = item.enrollmentEnd newItem.numPages = Int32(item.numPages) newItem.courseID = item.courseID + newItem.courseRawImage = item.courseRawImage do { try context.save() @@ -67,28 +71,31 @@ public class DiscoveryPersistence: DiscoveryPersistenceProtocol { } } - public func loadCourseDetails(courseID: String) throws -> CourseDetails { - let request = CDCourseDetails.fetchRequest() - request.predicate = NSPredicate(format: "courseID = %@", courseID) - guard let courseDetails = try? context.fetch(request).first else { throw NoCachedDataError() } - return CourseDetails( - courseID: courseDetails.courseID ?? "", - org: courseDetails.org ?? "", - courseTitle: courseDetails.courseTitle ?? "", - courseDescription: courseDetails.courseDescription ?? "", - courseStart: courseDetails.courseStart, - courseEnd: courseDetails.courseEnd, - enrollmentStart: courseDetails.enrollmentStart, - enrollmentEnd: courseDetails.enrollmentEnd, - isEnrolled: courseDetails.isEnrolled, - overviewHTML: courseDetails.overviewHTML ?? "", - courseBannerURL: courseDetails.courseBannerURL ?? "", - courseVideoURL: nil - ) + public func loadCourseDetails(courseID: String) async throws -> CourseDetails { + try await context.perform {[context] in + let request = CDCourseDetails.fetchRequest() + request.predicate = NSPredicate(format: "courseID = %@", courseID) + guard let courseDetails = try? context.fetch(request).first else { throw NoCachedDataError() } + return CourseDetails( + courseID: courseDetails.courseID ?? "", + org: courseDetails.org ?? "", + courseTitle: courseDetails.courseTitle ?? "", + courseDescription: courseDetails.courseDescription ?? "", + courseStart: courseDetails.courseStart, + courseEnd: courseDetails.courseEnd, + enrollmentStart: courseDetails.enrollmentStart, + enrollmentEnd: courseDetails.enrollmentEnd, + isEnrolled: courseDetails.isEnrolled, + overviewHTML: courseDetails.overviewHTML ?? "", + courseBannerURL: courseDetails.courseBannerURL ?? "", + courseVideoURL: nil, + courseRawImage: courseDetails.courseRawImage + ) + } } public func saveCourseDetails(course: CourseDetails) { - context.performAndWait { + context.perform {[context] in let newCourseDetails = CDCourseDetails(context: self.context) newCourseDetails.courseID = course.courseID newCourseDetails.org = course.org @@ -101,6 +108,7 @@ public class DiscoveryPersistence: DiscoveryPersistenceProtocol { newCourseDetails.isEnrolled = course.isEnrolled newCourseDetails.overviewHTML = course.overviewHTML newCourseDetails.courseBannerURL = course.courseBannerURL + newCourseDetails.courseRawImage = course.courseRawImage do { try context.save() diff --git a/OpenEdX/Data/Network/NotificationsEndpoints.swift b/OpenEdX/Data/Network/NotificationsEndpoints.swift new file mode 100644 index 000000000..4af73fb69 --- /dev/null +++ b/OpenEdX/Data/Network/NotificationsEndpoints.swift @@ -0,0 +1,45 @@ +// +// NotificationsEndpoints.swift +// OpenEdX +// +// Created by Volodymyr Chekyrta on 21.05.24. +// + +import Foundation +import Core +import OEXFoundation +import Alamofire + +enum NotificationsEndpoints: EndPointType { + + case syncFirebaseToken(token: String) + + var path: String { + switch self { + case .syncFirebaseToken: + return "/api/mobile/v4/notifications/create-token/" + } + } + + var httpMethod: HTTPMethod { + switch self { + case .syncFirebaseToken: + return .post + } + } + + var headers: HTTPHeaders? { + nil + } + + var task: HTTPTask { + switch self { + case let .syncFirebaseToken(token): + let params: [String: Encodable] = [ + "registration_id": token, + "active": true + ] + return .requestParameters(parameters: params, encoding: URLEncoding.httpBody) + } + } +} diff --git a/OpenEdX/Data/ProfilePersistence.swift b/OpenEdX/Data/ProfilePersistence.swift new file mode 100644 index 000000000..afc4f28ec --- /dev/null +++ b/OpenEdX/Data/ProfilePersistence.swift @@ -0,0 +1,169 @@ +// +// ProfilePersistence.swift +// OpenEdX +// +// Created by  Stepanok Ivan on 03.06.2024. +// + +import Profile +import Core +import OEXFoundation +import Foundation +import CoreData + +public class ProfilePersistence: ProfilePersistenceProtocol { + + private var context: NSManagedObjectContext + + public init(context: NSManagedObjectContext) { + self.context = context + } + + public func getCourseState(courseID: String) -> CourseCalendarState? { + context.performAndWait { + let fetchRequest: NSFetchRequest = CDCourseCalendarState.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "courseID == %@", courseID) + do { + if let result = try context.fetch(fetchRequest).first, + let courseID = result.courseID, + let checksum = result.checksum { + return CourseCalendarState(courseID: courseID, checksum: checksum) + } + } catch { + debugLog("⛔️ Error fetching CourseCalendarEvents: \(error)") + } + return nil + } + } + + public func getAllCourseStates() -> [CourseCalendarState] { + var states: [CourseCalendarState] = [] + context.performAndWait { + let fetchRequest: NSFetchRequest = CDCourseCalendarState.fetchRequest() + do { + let results = try context.fetch(fetchRequest) + states = results.compactMap { result in + if let courseID = result.courseID, let checksum = result.checksum { + return CourseCalendarState(courseID: courseID, checksum: checksum) + } + return nil + } + } catch { + debugLog("⛔️ Error fetching CourseCalendarEvents: \(error)") + } + } + return states + } + + public func saveCourseState(state: CourseCalendarState) { + context.performAndWait { + let newState = CDCourseCalendarState(context: context) + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + newState.courseID = state.courseID + newState.checksum = state.checksum + do { + try context.save() + } catch { + debugLog("⛔️ Error saving CourseCalendarEvent: \(error)") + } + } + } + + public func removeCourseState(courseID: String) { + context.performAndWait { + let fetchRequest: NSFetchRequest = CDCourseCalendarState.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "courseID == %@", courseID) + do { + if let result = try context.fetch(fetchRequest).first { + if let object = result as? NSManagedObject { + context.delete(object) + try context.save() + } + } + } catch { + debugLog("⛔️ Error removing CDCourseCalendarState: \(error)") + } + } + } + + public func deleteAllCourseStatesAndEvents() { + let fetchRequestCalendarStates: NSFetchRequest = CDCourseCalendarState.fetchRequest() + let deleteRequestCalendarStates = NSBatchDeleteRequest(fetchRequest: fetchRequestCalendarStates) + let fetchRequestCalendarEvents: NSFetchRequest = CDCourseCalendarEvent.fetchRequest() + let deleteRequestCalendarEvents = NSBatchDeleteRequest(fetchRequest: fetchRequestCalendarEvents) + + do { + try context.execute(deleteRequestCalendarStates) + try context.execute(deleteRequestCalendarEvents) + try context.save() + } catch { + debugLog("⛔️⛔️⛔️⛔️⛔️", error) + } + } + + public func saveCourseCalendarEvent(_ event: CourseCalendarEvent) { + context.performAndWait { + let newEvent = CDCourseCalendarEvent(context: context) + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + newEvent.courseID = event.courseID + newEvent.eventIdentifier = event.eventIdentifier + do { + try context.save() + } catch { + debugLog("⛔️ Error saving CourseCalendarEvent: \(error)") + } + } + } + + public func removeCourseCalendarEvents(for courseId: String) { + context.performAndWait { + let fetchRequest: NSFetchRequest = CDCourseCalendarEvent.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "courseID == %@", courseId) + do { + let results = try context.fetch(fetchRequest) + results.forEach { result in + if let object = result as? NSManagedObject { + context.delete(object) + } + } + try context.save() + } catch { + debugLog("⛔️ Error removing CourseCalendarEvents: \(error)") + } + } + } + + public func removeAllCourseCalendarEvents() { + context.performAndWait { + let fetchRequest: NSFetchRequest = CDCourseCalendarEvent.fetchRequest() + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + do { + try context.execute(deleteRequest) + try context.save() + } catch { + debugLog("⛔️ Error removing CourseCalendarEvents: \(error)") + } + } + } + + public func getCourseCalendarEvents(for courseId: String) -> [CourseCalendarEvent] { + var events: [CourseCalendarEvent] = [] + context.performAndWait { + let fetchRequest: NSFetchRequest = CDCourseCalendarEvent.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "courseID == %@", courseId) + do { + let results = try context.fetch(fetchRequest) + events = results.compactMap { result in + if let courseID = result.courseID, let eventIdentifier = result.eventIdentifier { + return CourseCalendarEvent(courseID: courseID, eventIdentifier: eventIdentifier) + } + return nil + } + } catch { + debugLog("⛔️ Error fetching CourseCalendarEvents: \(error)") + } + } + return events + } + +} diff --git a/OpenEdX/Info.plist b/OpenEdX/Info.plist index 2b4cb0751..e9bd32e58 100644 --- a/OpenEdX/Info.plist +++ b/OpenEdX/Info.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + openEdx.offlineProgressSync + Configuration $(CONFIGURATION) FirebaseAppDelegateProxyEnabled @@ -26,18 +30,20 @@ NSAllowsArbitraryLoads + NSAllowsArbitraryLoadsInWebContent + + NSCalendarsFullAccessUsageDescription + We would like to utilize your calendar list to subscribe you to your personalized calendar for this course. UIAppFonts UIBackgroundModes audio + fetch + processing UIViewControllerBasedStatusBarAppearance - NSCalendarsUsageDescription - We would like to utilize your calendar list to subscribe you to your personalized calendar for this course. - NSCalendarsFullAccessUsageDescription - We would like to utilize your calendar list to subscribe you to your personalized calendar for this course. diff --git a/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift b/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift index aca6aff6b..f7bfa05b8 100644 --- a/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift +++ b/OpenEdX/Managers/AnalyticsManager/AnalyticsManager.swift @@ -15,12 +15,10 @@ import Course import Discussion import WhatsNew import Swinject +import OEXFoundation +import OEXFirebaseAnalytics -protocol AnalyticsService { - func identify(id: String, username: String?, email: String?) - func logEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) -} - +// swiftlint:disable type_body_length file_length class AnalyticsManager: AuthorizationAnalytics, MainScreenAnalytics, DiscoveryAnalytics, @@ -30,27 +28,12 @@ class AnalyticsManager: AuthorizationAnalytics, DiscussionAnalytics, CoreAnalytics, WhatsNewAnalytics { - private var services: [AnalyticsService] = [] + + private var services: [AnalyticsService] // Init Analytics Manager - public init(config: ConfigProtocol) { - services = servicesFor(config: config) - } - - private func servicesFor(config: ConfigProtocol) -> [AnalyticsService] { - var analyticsServices: [AnalyticsService] = [] - // add Firebase Analytics Service - if config.firebase.enabled && config.firebase.isAnalyticsSourceFirebase, - let firebaseService = Container.shared.resolve(FirebaseAnalyticsService.self) { - analyticsServices.append(firebaseService) - } - - // add Segment Analytics Service - if config.segment.enabled, - let segmentService = Container.shared.resolve(SegmentAnalyticsService.self) { - analyticsServices.append(segmentService) - } - return analyticsServices + public init(services: [AnalyticsService]) { + self.services = services } public func identify(id: String, username: String, email: String) { @@ -61,7 +44,13 @@ class AnalyticsManager: AuthorizationAnalytics, private func logEvent(_ event: AnalyticsEvent, parameters: [String: Any]? = nil) { for service in services { - service.logEvent(event, parameters: parameters) + service.logEvent(event.rawValue, parameters: parameters) + } + } + + private func logScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]? = nil) { + for service in services { + service.logScreenEvent(event.rawValue, parameters: parameters) } } @@ -84,6 +73,24 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(event, parameters: [EventParamKey.name: biValue.rawValue]) } + public func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]? = nil) { + logScreenEvent(event, parameters: parameters) + } + + public func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + var eventParams: [String: Any] = [EventParamKey.name: biValue.rawValue] + + if let parameters { + eventParams.merge(parameters, uniquingKeysWith: { (first, _) in first }) + } + + logScreenEvent(event, parameters: eventParams) + } + + private func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + logScreenEvent(event, parameters: [EventParamKey.name: biValue.rawValue]) + } + // MARK: Pre Login public func userLogin(method: AuthMethod) { @@ -130,10 +137,14 @@ class AnalyticsManager: AuthorizationAnalytics, ) } + public func authTrackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + trackScreenEvent(event, biValue: biValue) + } + // MARK: MainScreenAnalytics public func mainDiscoveryTabClicked() { - trackEvent(.mainDiscoveryTabClicked, biValue: .mainDiscoveryTabClicked) + trackScreenEvent(.mainDiscoveryTabClicked, biValue: .mainDiscoveryTabClicked) } public func mainDashboardTabClicked() { @@ -141,11 +152,11 @@ class AnalyticsManager: AuthorizationAnalytics, } public func mainProgramsTabClicked() { - trackEvent(.mainProgramsTabClicked, biValue: .mainProgramsTabClicked) + trackScreenEvent(.mainProgramsTabClicked, biValue: .mainProgramsTabClicked) } public func mainProfileTabClicked() { - trackEvent(.mainProfileTabClicked, biValue: .mainProfileTabClicked) + trackScreenEvent(.mainProfileTabClicked, biValue: .mainProfileTabClicked) } // MARK: Discovery @@ -178,7 +189,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.dashboardCourseClicked.rawValue ] - logEvent(.dashboardCourseClicked, parameters: parameters) + logScreenEvent(.dashboardCourseClicked, parameters: parameters) } // MARK: Profile @@ -223,7 +234,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.name: EventBIValue.profileDeleteAccountClicked.rawValue, EventParamKey.category: EventCategory.profile ] - logEvent(.profileDeleteAccountClicked) + logEvent(.profileDeleteAccountClicked, parameters: parameters) } public func profileVideoSettingsClicked() { @@ -269,7 +280,7 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(event, parameters: parameters) } - public func profileEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + public func profileTrackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { let parameters = [ EventParamKey.category: EventCategory.profile, EventParamKey.name: biValue.rawValue @@ -278,6 +289,15 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(event, parameters: parameters) } + public func profileScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + let parameters = [ + EventParamKey.category: EventCategory.profile, + EventParamKey.name: biValue.rawValue + ] + + logScreenEvent(event, parameters: parameters) + } + public func privacyPolicyClicked() { let parameters = [ EventParamKey.name: EventBIValue.privacyPolicyClicked.rawValue, @@ -329,9 +349,10 @@ class AnalyticsManager: AuthorizationAnalytics, public func userLogout(force: Bool) { let parameters = [ EventParamKey.name: EventBIValue.userLogout.rawValue, - EventParamKey.category: EventCategory.profile + EventParamKey.category: EventCategory.profile, + EventParamKey.force: "\(force)" ] - logEvent(.userLogout, parameters: [EventParamKey.force: force]) + logEvent(.userLogout, parameters: parameters) } // MARK: Course @@ -377,13 +398,13 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(.externalLinkOpenAlertAction, parameters: parameters) } - public func discoveryEvent(event: AnalyticsEvent, biValue: EventBIValue) { + public func discoveryScreenEvent(event: AnalyticsEvent, biValue: EventBIValue) { let parameters = [ EventParamKey.category: EventCategory.discovery, EventParamKey.name: biValue.rawValue ] - logEvent(event, parameters: parameters) + logScreenEvent(event, parameters: parameters) } public func viewCourseClicked(courseId: String, courseName: String) { @@ -392,7 +413,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.category: EventCategory.discovery ] - logEvent(.viewCourseClicked, parameters: parameters) + logScreenEvent(.viewCourseClicked, parameters: parameters) } public func resumeCourseClicked(courseId: String, courseName: String, blockId: String) { @@ -490,7 +511,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.courseOutlineCourseTabClicked.rawValue ] - logEvent(.courseOutlineCourseTabClicked, parameters: parameters) + logScreenEvent(.courseOutlineCourseTabClicked, parameters: parameters) } public func courseOutlineVideosTabClicked(courseId: String, courseName: String) { @@ -499,7 +520,16 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.courseOutlineVideosTabClicked.rawValue ] - logEvent(.courseOutlineVideosTabClicked, parameters: parameters) + logScreenEvent(.courseOutlineVideosTabClicked, parameters: parameters) + } + + func courseOutlineOfflineTabClicked(courseId: String, courseName: String) { + let parameters = [ + EventParamKey.courseID: courseId, + EventParamKey.courseName: courseName, + EventParamKey.name: EventBIValue.courseOutlineOfflineTabClicked.rawValue + ] + logEvent(.courseOutlineOfflineTabClicked, parameters: parameters) } public func courseOutlineDatesTabClicked(courseId: String, courseName: String) { @@ -508,7 +538,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.courseOutlineDatesTabClicked.rawValue ] - logEvent(.courseOutlineDatesTabClicked, parameters: parameters) + logScreenEvent(.courseOutlineDatesTabClicked, parameters: parameters) } public func courseOutlineDiscussionTabClicked(courseId: String, courseName: String) { @@ -517,7 +547,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.courseOutlineDiscussionTabClicked.rawValue ] - logEvent(.courseOutlineDiscussionTabClicked, parameters: parameters) + logScreenEvent(.courseOutlineDiscussionTabClicked, parameters: parameters) } public func courseOutlineHandoutsTabClicked(courseId: String, courseName: String) { @@ -526,7 +556,7 @@ class AnalyticsManager: AuthorizationAnalytics, EventParamKey.courseName: courseName, EventParamKey.name: EventBIValue.courseOutlineHandoutsTabClicked.rawValue ] - logEvent(.courseOutlineHandoutsTabClicked, parameters: parameters) + logScreenEvent(.courseOutlineHandoutsTabClicked, parameters: parameters) } func datesComponentTapped( @@ -613,6 +643,16 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(event, parameters: parameters) } + public func trackCourseScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, courseID: String) { + let parameters = [ + EventParamKey.courseID: courseID, + EventParamKey.category: EventCategory.course, + EventParamKey.name: biValue.rawValue + ] + + logScreenEvent(event, parameters: parameters) + } + public func plsEvent( _ event: AnalyticsEvent, bivalue: EventBIValue, @@ -779,3 +819,4 @@ class AnalyticsManager: AuthorizationAnalytics, logEvent(.whatnewClose, parameters: parameters) } } +// swiftlint:enable type_body_length file_length diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift index be43a71c6..20262a0ef 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkManager.swift @@ -38,11 +38,11 @@ public class DeepLinkManager { private let discussionInteractor: DiscussionInteractorProtocol private let courseInteractor: CourseInteractorProtocol private let profileInteractor: ProfileInteractorProtocol - + var userloggedIn: Bool { - return !(storage.user?.username?.isEmpty ?? true) - } - + return !(storage.user?.username?.isEmpty ?? true) + } + public init( config: ConfigProtocol, router: DeepLinkRouter, @@ -59,7 +59,7 @@ public class DeepLinkManager { self.discussionInteractor = discussionInteractor self.courseInteractor = courseInteractor self.profileInteractor = profileInteractor - + services = servicesFor(config: config) } @@ -102,9 +102,9 @@ public class DeepLinkManager { guard link.type != .none else { return } - + let isAppActive = UIApplication.shared.applicationState == .active - + Task { if isAppActive { await showNotificationAlert(link) @@ -124,11 +124,11 @@ public class DeepLinkManager { } } } - + @MainActor private func showNotificationAlert(_ link: PushLink) { router.dismissPresentedViewController() - + router.presentAlert( alertTitle: link.title ?? "", alertMessage: link.body ?? "", @@ -148,17 +148,19 @@ public class DeepLinkManager { type: .deepLink ) } - + private func isDiscovery(type: DeepLinkType) -> Bool { type == .discovery || type == .discoveryCourseDetail || type == .discoveryProgramDetail } - + private func isDiscussionThreads(type: DeepLinkType) -> Bool { type == .discussionPost || type == .discussionTopic || - type == .discussionComment + type == .discussionComment || + type == .forumResponse || + type == .forumComment } private func isHandout(type: DeepLinkType) -> Bool { @@ -195,9 +197,14 @@ public class DeepLinkManager { .courseAnnouncement, .discussionTopic, .discussionPost, + .forumResponse, + .forumComment, .discussionComment, .courseComponent: await showCourseScreen(with: type, link: link) + case .enroll, .addBetaTester: + await showCourseScreen(with: type, link: link) + NotificationCenter.default.post(name: .refreshEnrollments, object: nil) case .program, .programDetail: guard config.program.enabled else { return } if let pathID = link.pathID, !pathID.isEmpty { @@ -209,6 +216,9 @@ public class DeepLinkManager { router.showTabScreen(tab: .profile) case .userProfile: await showEditProfile() + case .unenroll, .removeBetaTester: + router.showTabScreen(tab: .dashboard) + NotificationCenter.default.post(name: .refreshEnrollments, object: nil) default: break } @@ -248,9 +258,8 @@ public class DeepLinkManager { link: link, courseDetails: courseDetails ) { [weak self] in - guard let self else { - return - } + guard let self else { return } + guard courseDetails.isEnrolled else { return } if self.isHandout(type: type) { self.router.showProgress() @@ -344,7 +353,6 @@ public class DeepLinkManager { isBlackedOut: isBlackedOut ) case .discussionPost: - if let topicID = link.topicID, !topicID.isEmpty, let topics = try? await discussionInteractor.getTopic( @@ -367,8 +375,7 @@ public class DeepLinkManager { isBlackedOut: isBlackedOut ) } - - case .discussionComment: + case .discussionComment, .forumResponse: if let topicID = link.topicID, !topicID.isEmpty, let topics = try? await discussionInteractor.getTopic( @@ -404,6 +411,42 @@ public class DeepLinkManager { isBlackedOut: isBlackedOut ) } + case .forumComment: + if let topicID = link.topicID, + !topicID.isEmpty, + let topics = try? await discussionInteractor.getTopic( + courseID: courseDetails.courseID, + topicID: topicID + ) { + router.showThreads( + topicID: topicID, + courseDetails: courseDetails, + topics: topics, + isBlackedOut: isBlackedOut + ) + } + + if let threadID = link.threadID, + !threadID.isEmpty, + let userThread = try? await discussionInteractor.getThread(threadID: threadID) { + router.showThread( + userThread: userThread, + isBlackedOut: isBlackedOut + ) + } + + if let parentID = link.parentID, + !parentID.isEmpty, + let comment = try? await self.discussionInteractor.getResponse(responseID: parentID), + let commentParentID = comment.parentID, + !commentParentID.isEmpty, + let parentComment = try? await self.discussionInteractor.getResponse(responseID: commentParentID) { + router.showComment( + comment: comment, + parentComment: parentComment.post, + isBlackedOut: isBlackedOut + ) + } default: break } diff --git a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift index ae33a9d64..c77b2d4db 100644 --- a/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift +++ b/OpenEdX/Managers/DeepLinkManager/DeepLinkRouter/DeepLinkRouter.swift @@ -107,12 +107,15 @@ extension Router: DeepLinkRouter { if courseDetails.isEnrolled { showCourseScreens( courseID: courseDetails.courseID, - isActive: nil, + hasAccess: nil, courseStart: courseDetails.courseStart, courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: courseDetails.courseTitle + title: courseDetails.courseTitle, + courseRawImage: courseDetails.courseRawImage, + showDates: false, + lastVisitedBlockID: nil ) } else { showCourseDetais( @@ -129,7 +132,9 @@ extension Router: DeepLinkRouter { .courseHandout, .courseAnnouncement, .courseDashboard, - .courseComponent: + .courseComponent, + .enroll, + .addBetaTester: popToCourseContainerView(animated: false) default: break @@ -167,7 +172,8 @@ extension Router: DeepLinkRouter { handouts: nil, announcements: updates, router: self, - cssInjector: cssInjector + cssInjector: cssInjector, + type: .announcements ) } @@ -183,7 +189,8 @@ extension Router: DeepLinkRouter { handouts: handouts, announcements: nil, router: self, - cssInjector: cssInjector + cssInjector: cssInjector, + type: .handouts ) } diff --git a/OpenEdX/Managers/DeepLinkManager/Link/DeepLink.swift b/OpenEdX/Managers/DeepLinkManager/Link/DeepLink.swift index 1da67d716..a1c1a7ae7 100644 --- a/OpenEdX/Managers/DeepLinkManager/Link/DeepLink.swift +++ b/OpenEdX/Managers/DeepLinkManager/Link/DeepLink.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation enum DeepLinkType: String { case courseDashboard = "course_dashboard" @@ -26,6 +27,12 @@ enum DeepLinkType: String { case programDetail = "program_detail" case userProfile = "user_profile" case profile = "profile" + case forumResponse = "forum_response" + case forumComment = "forum_comment" + case enroll = "enroll" + case unenroll = "unenroll" + case addBetaTester = "add_beta_tester" + case removeBetaTester = "remove_beta_tester" case none } @@ -33,30 +40,38 @@ private enum DeepLinkKeys: String, RawStringExtractable { case courseID = "course_id" case pathID = "path_id" case screenName = "screen_name" + case notificationType = "notification_type" case topicID = "topic_id" case threadID = "thread_id" case commentID = "comment_id" + case parentID = "parent_id" case componentID = "component_id" } public class DeepLink { let courseID: String? let screenName: String? + let notificationType: String? let pathID: String? let topicID: String? let threadID: String? let commentID: String? + let parentID: String? let componentID: String? var type: DeepLinkType init(dictionary: [AnyHashable: Any]) { courseID = dictionary[DeepLinkKeys.courseID.rawValue] as? String screenName = dictionary[DeepLinkKeys.screenName.rawValue] as? String + notificationType = dictionary[DeepLinkKeys.notificationType.rawValue] as? String pathID = dictionary[DeepLinkKeys.pathID.rawValue] as? String topicID = dictionary[DeepLinkKeys.topicID.rawValue] as? String threadID = dictionary[DeepLinkKeys.threadID.rawValue] as? String commentID = dictionary[DeepLinkKeys.commentID.rawValue] as? String componentID = dictionary[DeepLinkKeys.componentID.rawValue] as? String - type = DeepLinkType(rawValue: screenName ?? DeepLinkType.none.rawValue) ?? .none + parentID = dictionary[DeepLinkKeys.parentID.rawValue] as? String + type = DeepLinkType( + rawValue: screenName ?? notificationType ?? DeepLinkType.none.rawValue + ) ?? .none } } diff --git a/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift b/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift deleted file mode 100644 index 12bd225e8..000000000 --- a/OpenEdX/Managers/FirebaseAnalyticsService/FirebaseAnalyticsService.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// FirebaseAnalyticsService.swift -// OpenEdX -// -// Created by Anton Yarmolenka on 19/02/2024. -// - -import Foundation -import Firebase -import Core - -private let MaxParameterValueCharacters = 100 -private let MaxNameValueCharacters = 40 - -class FirebaseAnalyticsService: AnalyticsService { - // Init manager - public init(config: ConfigProtocol) { - guard config.firebase.enabled && config.firebase.isAnalyticsSourceFirebase else { return } - - FirebaseApp.configure() - } - - func identify(id: String, username: String?, email: String?) { - Analytics.setUserID(id) - } - - func logEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { - guard let name = try? formatFirebaseName(event.rawValue) else { - debugLog("Firebase: event name is not supported: \(event.rawValue)") - return - } - - Analytics.logEvent(name, parameters: formatParamaters(params: parameters)) - } -} - -extension FirebaseAnalyticsService { - private func formatParamaters(params: [String: Any]?) -> [String: Any] { - // Firebase only supports String or Number as value for event parameters - var formattedParams: [String: Any] = [:] - - for (key, value) in params ?? [:] { - if let key = try? formatFirebaseName(key) { - formattedParams[key] = formatParamValue(value: value) - } - } - - return formattedParams - } - - private func formatFirebaseName(_ eventName: String) throws -> String { - let trimmed = eventName.trimmingCharacters(in: .whitespaces) - do { - let regex = try NSRegularExpression(pattern: "([^a-zA-Z0-9_])", options: .caseInsensitive) - let formattedString = regex.stringByReplacingMatches( - in: trimmed, - options: .reportProgress, - range: NSRange(location: 0, length: trimmed.count), - withTemplate: "_" - ) - - // Resize the string to maximum 40 characters if needed - let range = NSRange(location: 0, length: min(formattedString.count, MaxNameValueCharacters)) - var formattedName = NSString(string: formattedString).substring(with: range) - - while formattedName.contains("__") { - formattedName = formattedName.replace(string: "__", replacement: "_") - } - - return formattedName - - } catch { - debugLog("Could not parse event name for Firebase.") - throw(error) - } - } - - private func formatParamValue(value: Any?) -> Any? { - - guard var formattedValue = value as? String else { return value} - - // Firebase only supports 100 characters for parameter value - if formattedValue.count > MaxParameterValueCharacters { - let index = formattedValue.index(formattedValue.startIndex, offsetBy: MaxParameterValueCharacters) - formattedValue = String(formattedValue[.. String { - return replacingOccurrences(of: string, with: replacement, options: NSString.CompareOptions.literal, range: nil) - } -} diff --git a/OpenEdX/Managers/PipManager.swift b/OpenEdX/Managers/PipManager.swift index 8720ae03f..6938ae9a2 100644 --- a/OpenEdX/Managers/PipManager.swift +++ b/OpenEdX/Managers/PipManager.swift @@ -6,33 +6,34 @@ // import Course +import Core import Combine import Discovery import SwiftUI public class PipManager: PipManagerProtocol { - var controllerHolder: PlayerViewControllerHolder? + var controllerHolder: PlayerViewControllerHolderProtocol? let discoveryInteractor: DiscoveryInteractorProtocol let courseInteractor: CourseInteractorProtocol let router: Router - let isNestedListEnabled: Bool + let courseDropDownNavigationEnabled: Bool public var isPipActive: Bool { controllerHolder != nil } - - private var ratePublisher: PassthroughSubject? - private var cancellations: [AnyCancellable] = [] - + public var isPipPlaying: Bool { + controllerHolder?.isPlaying ?? false + } + public init( router: Router, discoveryInteractor: DiscoveryInteractorProtocol, courseInteractor: CourseInteractorProtocol, - isNestedListEnabled: Bool + courseDropDownNavigationEnabled: Bool ) { self.discoveryInteractor = discoveryInteractor self.courseInteractor = courseInteractor self.router = router - self.isNestedListEnabled = isNestedListEnabled + self.courseDropDownNavigationEnabled = courseDropDownNavigationEnabled } public func holder( @@ -40,7 +41,7 @@ public class PipManager: PipManagerProtocol { blockID: String, courseID: String, selectedCourseTab: Int - ) -> PlayerViewControllerHolder? { + ) -> PlayerViewControllerHolderProtocol? { if controllerHolder?.blockID == blockID, controllerHolder?.courseID == courseID, controllerHolder?.selectedCourseTab == selectedCourseTab { @@ -50,32 +51,29 @@ public class PipManager: PipManagerProtocol { return nil } - public func set(holder: PlayerViewControllerHolder) { + public func set(holder: PlayerViewControllerHolderProtocol) { controllerHolder = holder - ratePublisher = PassthroughSubject() - cancellations.removeAll() - holder.playerController.player?.publisher(for: \.rate) - .sink { [weak self] rate in - self?.ratePublisher?.send(rate) - } - .store(in: &cancellations) } - public func remove(holder: PlayerViewControllerHolder) { - if controllerHolder == holder { + public func remove(holder: PlayerViewControllerHolderProtocol) { + if isCurrentHolderEqualTo(holder) { controllerHolder = nil - cancellations.removeAll() - ratePublisher = nil } } + + private func isCurrentHolderEqualTo(_ holder: PlayerViewControllerHolderProtocol) -> Bool { + controllerHolder?.blockID == holder.blockID && + controllerHolder?.courseID == holder.courseID && + controllerHolder?.url == holder.url && + controllerHolder?.selectedCourseTab == holder.selectedCourseTab + } public func pipRatePublisher() -> AnyPublisher? { - ratePublisher? - .eraseToAnyPublisher() + controllerHolder?.getRatePublisher() } @MainActor - public func restore(holder: PlayerViewControllerHolder) async throws { + public func restore(holder: PlayerViewControllerHolderProtocol) async throws { let courseID = holder.courseID // if we are on CourseUnitView, and tab is same with holder @@ -94,11 +92,11 @@ public class PipManager: PipManagerProtocol { public func pauseCurrentPipVideo() { guard let holder = controllerHolder else { return } - holder.playerController.player?.pause() + holder.playerController?.pause() } @MainActor - private func navigate(to holder: PlayerViewControllerHolder) async throws { + private func navigate(to holder: PlayerViewControllerHolderProtocol) async throws { let currentControllers = router.getNavigationController().viewControllers guard let mainController = currentControllers.first as? UIHostingController else { return @@ -116,7 +114,7 @@ public class PipManager: PipManagerProtocol { viewControllers.append(try await containerController(for: holder)) } - if !isNestedListEnabled && holder.selectedCourseTab != CourseTab.dates.rawValue { + if !courseDropDownNavigationEnabled && holder.selectedCourseTab != CourseTab.dates.rawValue { viewControllers.append(try await courseVerticalController(for: holder)) } @@ -127,7 +125,7 @@ public class PipManager: PipManagerProtocol { @MainActor private func courseVerticalController( - for holder: PlayerViewControllerHolder + for holder: PlayerViewControllerHolderProtocol ) async throws -> UIHostingController { var courseStructure = try await courseInteractor.getLoadedCourseBlocks(courseID: holder.courseID) if holder.selectedCourseTab == CourseTab.videos.rawValue { @@ -150,7 +148,7 @@ public class PipManager: PipManagerProtocol { @MainActor private func courseUnitController( - for holder: PlayerViewControllerHolder + for holder: PlayerViewControllerHolderProtocol ) async throws -> UIHostingController { var courseStructure = try await courseInteractor.getLoadedCourseBlocks(courseID: holder.courseID) @@ -178,24 +176,27 @@ public class PipManager: PipManagerProtocol { @MainActor private func containerController( - for holder: PlayerViewControllerHolder + for holder: PlayerViewControllerHolderProtocol ) async throws -> UIHostingController { let courseDetails = try await getCourseDetails(for: holder) - let isActive: Bool? = nil + let hasAccess: Bool? = nil let controller = router.getCourseScreensController( courseID: courseDetails.courseID, - isActive: isActive, + hasAccess: hasAccess, courseStart: courseDetails.courseStart, courseEnd: courseDetails.courseEnd, enrollmentStart: courseDetails.enrollmentStart, enrollmentEnd: courseDetails.enrollmentEnd, - title: courseDetails.courseTitle + title: courseDetails.courseTitle, + courseRawImage: courseDetails.courseRawImage, + showDates: false, + lastVisitedBlockID: nil ) controller.rootView.viewModel.selection = holder.selectedCourseTab return controller } - private func getCourseDetails(for holder: PlayerViewControllerHolder) async throws -> CourseDetails { + private func getCourseDetails(for holder: PlayerViewControllerHolderProtocol) async throws -> CourseDetails { if let value = try? await discoveryInteractor.getLoadedCourseDetails( courseID: holder.courseID ) { diff --git a/OpenEdX/Managers/PluginManager.swift b/OpenEdX/Managers/PluginManager.swift new file mode 100644 index 000000000..f0661aa6d --- /dev/null +++ b/OpenEdX/Managers/PluginManager.swift @@ -0,0 +1,20 @@ +// +// PluginManager.swift +// OpenEdX +// +// Created by Ivan Stepanok on 15.10.2024. +// + +import Foundation +import OEXFoundation + +public class PluginManager { + + private(set) var analyticsServices: [AnalyticsService] = [] + + public init() {} + + func addPlugin(analyticsService: AnalyticsService) { + analyticsServices.append(analyticsService) + } +} diff --git a/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift b/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift index 7c2bde74a..1e4d37220 100644 --- a/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift +++ b/OpenEdX/Managers/PushNotificationsManager/Listeners/BrazeListener.swift @@ -6,12 +6,35 @@ // import Foundation +import Swinject class BrazeListener: PushNotificationsListener { + + private let deepLinkManager: DeepLinkManager + + init(deepLinkManager: DeepLinkManager) { + self.deepLinkManager = deepLinkManager + } + func shouldListenNotification(userinfo: [AnyHashable: Any]) -> Bool { //A push notification sent from the braze has a key ab in it like ab = {c = "c_value";}; - guard let _ = userinfo["ab"] as? [String : Any], userinfo.count > 0 - else { return false } - return true + let data = userinfo["ab"] as? [String: Any] + return userinfo.count > 0 && data != nil + } + + func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) { + guard let dictionary = userInfo as? [String: AnyHashable], + shouldListenNotification(userinfo: userInfo) else { return } + + // Removed as part of the move to a plugin architecture, this code should be called from the plugin. + +// if let segmentService = Container.shared.resolve(SegmentAnalyticsService.self) { +// segmentService.analytics?.receivedRemoteNotification(userInfo: userInfo) +// } + + + + let link = PushLink(dictionary: dictionary) + deepLinkManager.processLinkFromNotification(link) } } diff --git a/OpenEdX/Managers/PushNotificationsManager/Listeners/FCMListener.swift b/OpenEdX/Managers/PushNotificationsManager/Listeners/FCMListener.swift index b0ed7f5f8..5d951e1a8 100644 --- a/OpenEdX/Managers/PushNotificationsManager/Listeners/FCMListener.swift +++ b/OpenEdX/Managers/PushNotificationsManager/Listeners/FCMListener.swift @@ -6,8 +6,29 @@ // import Foundation +import FirebaseMessaging class FCMListener: PushNotificationsListener { + + private let deepLinkManager: DeepLinkManager + + init(deepLinkManager: DeepLinkManager) { + self.deepLinkManager = deepLinkManager + } + // check if userinfo contains data for this Listener - func shouldListenNotification(userinfo: [AnyHashable: Any]) -> Bool { false } + func shouldListenNotification(userinfo: [AnyHashable: Any]) -> Bool { + let data = userinfo["gcm.message_id"] + return userinfo.count > 0 && data != nil + } + + func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) { + guard let dictionary = userInfo as? [String: AnyHashable], + shouldListenNotification(userinfo: userInfo) else { return } + // With swizzling disabled you must let Messaging know about the message, for Analytics + Messaging.messaging().appDidReceiveMessage(userInfo) + + let link = PushLink(dictionary: dictionary) + deepLinkManager.processLinkFromNotification(link) + } } diff --git a/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift b/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift index 9e45059e3..3ef708cb1 100644 --- a/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift +++ b/OpenEdX/Managers/PushNotificationsManager/Providers/BrazeProvider.swift @@ -6,23 +6,34 @@ // import Foundation -import SegmentBrazeUI import Swinject +import OEXFoundation class BrazeProvider: PushNotificationsProvider { + func didRegisterWithDeviceToken(deviceToken: Data) { - guard let segmentService = Container.shared.resolve(SegmentAnalyticsService.self) else { return } - segmentService.analytics?.add( - plugin: BrazeDestination( - additionalConfiguration: { configuration in - configuration.logger.level = .debug - }, additionalSetup: { braze in - braze.notifications.register(deviceToken: deviceToken) - } - ) - ) + // Removed as part of the move to a plugin architecture, this code should be called from the plugin. + +// guard let segmentService = Container.shared.resolve(SegmentAnalyticsService.self) else { return } +// segmentService.analytics?.add( +// plugin: BrazeDestination( +// additionalConfiguration: { configuration in +// configuration.logger.level = .info +// }, additionalSetup: { braze in +// braze.notifications.register(deviceToken: deviceToken) +// } +// ) +// ) +// +// segmentService.analytics?.registeredForRemoteNotifications(deviceToken: deviceToken) } func didFailToRegisterForRemoteNotificationsWithError(error: Error) { } + + func synchronizeToken() { + } + + func refreshToken() { + } } diff --git a/OpenEdX/Managers/PushNotificationsManager/Providers/FCMProvider.swift b/OpenEdX/Managers/PushNotificationsManager/Providers/FCMProvider.swift index 4e66a2a30..71b0c82c3 100644 --- a/OpenEdX/Managers/PushNotificationsManager/Providers/FCMProvider.swift +++ b/OpenEdX/Managers/PushNotificationsManager/Providers/FCMProvider.swift @@ -6,13 +6,59 @@ // import Foundation +import Core +import OEXFoundation +import FirebaseCore +import FirebaseMessaging -class FCMProvider: PushNotificationsProvider { +class FCMProvider: NSObject, PushNotificationsProvider, MessagingDelegate { + + private var storage: CoreStorage + private let api: API + + init(storage: CoreStorage, api: API) { + self.storage = storage + self.api = api + } + func didRegisterWithDeviceToken(deviceToken: Data) { - + Messaging.messaging().apnsToken = deviceToken } func didFailToRegisterForRemoteNotificationsWithError(error: Error) { + } + + func synchronizeToken() { + guard let fcmToken = storage.pushToken, storage.user != nil else { return } + sendFCMToken(fcmToken) + } + + func refreshToken() { + Messaging.messaging().deleteToken { error in + if let error = error { + debugLog("Error deleting FCM token: \(error.localizedDescription)") + } else { + Messaging.messaging().token { token, error in + if let error = error { + debugLog("Error fetching FCM token: \(error.localizedDescription)") + } else if let token = token { + debugLog("FCM token fetched: \(token)") + } + } + } + } + } + + func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + storage.pushToken = fcmToken + guard let fcmToken, storage.user != nil else { return } + sendFCMToken(fcmToken) + } + + private func sendFCMToken(_ token: String) { + Task { + try? await api.request(NotificationsEndpoints.syncFirebaseToken(token: token)) + } } } diff --git a/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift b/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift index be832bcbb..2f9c3c1b4 100644 --- a/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift +++ b/OpenEdX/Managers/PushNotificationsManager/PushNotificationsManager.swift @@ -7,12 +7,17 @@ import Foundation import Core +import OEXFoundation import UIKit -import Swinject +import UserNotifications +import FirebaseCore +import FirebaseMessaging public protocol PushNotificationsProvider { func didRegisterWithDeviceToken(deviceToken: Data) func didFailToRegisterForRemoteNotificationsWithError(error: Error) + func synchronizeToken() + func refreshToken() } protocol PushNotificationsListener { @@ -20,45 +25,53 @@ protocol PushNotificationsListener { func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) } -extension PushNotificationsListener { - func didReceiveRemoteNotification(userInfo: [AnyHashable: Any]) { - guard let dictionary = userInfo as? [String: AnyHashable], - shouldListenNotification(userinfo: userInfo), - let deepLinkManager = Container.shared.resolve(DeepLinkManager.self) - else { return } - let link = PushLink(dictionary: dictionary) - deepLinkManager.processLinkFromNotification(link) - } -} - -class PushNotificationsManager { +class PushNotificationsManager: NSObject { + + private let deepLinkManager: DeepLinkManager + private let storage: CoreStorage + private let api: API + private var providers: [PushNotificationsProvider] = [] private var listeners: [PushNotificationsListener] = [] + public var hasProviders: Bool { + providers.count > 0 + } + // Init manager - public init(config: ConfigProtocol) { + public init( + deepLinkManager: DeepLinkManager, + storage: CoreStorage, + api: API, + config: ConfigProtocol + ) { + self.deepLinkManager = deepLinkManager + self.storage = storage + self.api = api + + super.init() providers = providersFor(config: config) listeners = listenersFor(config: config) } private func providersFor(config: ConfigProtocol) -> [PushNotificationsProvider] { var pushProviders: [PushNotificationsProvider] = [] - if config.firebase.cloudMessagingEnabled { - pushProviders.append(FCMProvider()) - } if config.braze.pushNotificationsEnabled { pushProviders.append(BrazeProvider()) } + if config.firebase.cloudMessagingEnabled { + pushProviders.append(FCMProvider(storage: storage, api: api)) + } return pushProviders } private func listenersFor(config: ConfigProtocol) -> [PushNotificationsListener] { var pushListeners: [PushNotificationsListener] = [] - if config.firebase.cloudMessagingEnabled { - pushListeners.append(FCMListener()) - } if config.braze.pushNotificationsEnabled { - pushListeners.append(BrazeListener()) + pushListeners.append(BrazeListener(deepLinkManager: deepLinkManager)) + } + if config.firebase.cloudMessagingEnabled { + pushListeners.append(FCMListener(deepLinkManager: deepLinkManager)) } return pushListeners } @@ -67,9 +80,7 @@ class PushNotificationsManager { public func performRegistration() { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in if granted { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } + debugLog("Permission for push notifications granted.") } else if let error = error { debugLog("Push notifications permission error: \(error.localizedDescription)") } else { @@ -94,4 +105,50 @@ class PushNotificationsManager { listener.didReceiveRemoteNotification(userInfo: userInfo) } } + + func synchronizeToken() { + for provider in providers { + provider.synchronizeToken() + } + } + + func refreshToken() { + for provider in providers { + provider.refreshToken() + } + } +} + +// MARK: - MessagingDelegate +extension PushNotificationsManager: MessagingDelegate { + func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + for provider in providers where provider is MessagingDelegate { + (provider as? MessagingDelegate)?.messaging?(messaging, didReceiveRegistrationToken: fcmToken) + } + } +} + +// MARK: - UNUserNotificationCenterDelegate +extension PushNotificationsManager: UNUserNotificationCenterDelegate { + @MainActor + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification + ) async -> UNNotificationPresentationOptions { + if UIApplication.shared.applicationState == .active { + didReceiveRemoteNotification(userInfo: notification.request.content.userInfo) + return [] + } + + return [[.list, .banner, .sound]] + } + + @MainActor + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse + ) async { + let userInfo = response.notification.request.content.userInfo + didReceiveRemoteNotification(userInfo: userInfo) + } } diff --git a/OpenEdX/Managers/SegmentAnalyticsService/SegmentAnalyticsService.swift b/OpenEdX/Managers/SegmentAnalyticsService/SegmentAnalyticsService.swift deleted file mode 100644 index b8c2cd422..000000000 --- a/OpenEdX/Managers/SegmentAnalyticsService/SegmentAnalyticsService.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// SegmentAnalyticsService.swift -// OpenEdX -// -// Created by Anton Yarmolenka on 21/02/2024. -// - -import Foundation -import Core -import Segment -import SegmentFirebase - -class SegmentAnalyticsService: AnalyticsService { - var analytics: Analytics? - - // Init manager - public init(config: ConfigProtocol) { - guard config.segment.enabled else { return } - - let configuration = Configuration(writeKey: config.segment.writeKey) - .trackApplicationLifecycleEvents(true) - .flushInterval(10) - analytics = Analytics(configuration: configuration) - if config.firebase.enabled && config.firebase.isAnalyticsSourceSegment { - analytics?.add(plugin: FirebaseDestination()) - } - } - - func identify(id: String, username: String?, email: String?) { - guard let email = email, let username = username else { return } - let traits: [String: String] = [ - "email": email, - "username": username - ] - analytics?.identify(userId: id, traits: traits) - } - - func logEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { - analytics?.track( - name: event.rawValue, - properties: parameters - ) - } -} diff --git a/OpenEdX/RouteController.swift b/OpenEdX/RouteController.swift index ac53c3d36..6df0b22c0 100644 --- a/OpenEdX/RouteController.swift +++ b/OpenEdX/RouteController.swift @@ -44,6 +44,7 @@ class RouteController: UIViewController { } } + resetAppSupportDirectoryUserData() coreAnalytics.trackEvent(.launch, biValue: .launch) } @@ -99,4 +100,23 @@ class RouteController: UIViewController { } present(navigation, animated: false) } + + /** + This code will delete any old application’s downloaded user data, such as video files, + from the Application Support directory to optimize storage. This activity will be performed + only once during the upgrade from the old Open edX application to the new one or during + fresh installation. We can consider removing this code once we are confident that most or + all users have transitioned to the new application. + */ + private func resetAppSupportDirectoryUserData() { + guard var upgradationValue = Container.shared.resolve(CoreStorage.self), + let downloadManager = Container.shared.resolve(DownloadManagerProtocol.self), + upgradationValue.resetAppSupportDirectoryUserData == false + else { return } + + Task { + downloadManager.removeAppSupportDirectoryUnusedContent() + upgradationValue.resetAppSupportDirectoryUserData = true + } + } } diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index 6c96146b3..13f99ed02 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -359,62 +359,95 @@ public class Router: AuthorizationRouter, public func showCourseScreens( courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String + title: String, + courseRawImage: String?, + showDates: Bool, + lastVisitedBlockID: String? ) { let controller = getCourseScreensController( courseID: courseID, - isActive: isActive, + hasAccess: hasAccess, courseStart: courseStart, courseEnd: courseEnd, enrollmentStart: enrollmentStart, enrollmentEnd: enrollmentEnd, - title: title + title: title, + courseRawImage: courseRawImage, + showDates: showDates, + lastVisitedBlockID: lastVisitedBlockID ) navigationController.pushViewController(controller, animated: true) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + Container.shared.resolve(PushNotificationsManager.self)?.performRegistration() + } } public func getCourseScreensController( courseID: String, - isActive: Bool?, + hasAccess: Bool?, courseStart: Date?, courseEnd: Date?, enrollmentStart: Date?, enrollmentEnd: Date?, - title: String + title: String, + courseRawImage: String?, + showDates: Bool, + lastVisitedBlockID: String? ) -> UIHostingController { let vm = Container.shared.resolve( CourseContainerViewModel.self, - arguments: isActive, + arguments: hasAccess, courseStart, courseEnd, enrollmentStart, - enrollmentEnd + enrollmentEnd, + showDates ? CourseTab.dates : CourseTab.course, + lastVisitedBlockID + )! + + let datesVm = Container.shared.resolve( + CourseDatesViewModel.self, + arguments: courseID, + title )! + let screensView = CourseContainerView( viewModel: vm, + courseDatesViewModel: datesVm, courseID: courseID, - title: title + title: title, + courseRawImage: courseRawImage ) return UIHostingController(rootView: screensView) } + public func showAllCourses(courses: [CourseItem]) { + let vm = Container.shared.resolve(AllCoursesViewModel.self)! + let view = AllCoursesView(viewModel: vm, router: self) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + public func showHandoutsUpdatesView( handouts: String?, announcements: [CourseUpdate]?, router: Course.CourseRouter, - cssInjector: CSSInjector + cssInjector: CSSInjector, + type: HandoutsItemType ) { let view = HandoutsUpdatesDetailView( handouts: handouts, announcements: announcements, router: router, - cssInjector: cssInjector + cssInjector: cssInjector, + type: type ) let controller = UIHostingController(rootView: view) navigationController.pushViewController(controller, animated: true) @@ -462,7 +495,7 @@ public class Router: AuthorizationRouter, )! let config = Container.shared.resolve(ConfigProtocol.self) - let isDropdownActive = config?.uiComponents.courseNestedListEnabled ?? false + let isDropdownActive = config?.uiComponents.courseDropDownNavigationEnabled ?? false let view = CourseUnitView(viewModel: viewModel, isDropdownActive: isDropdownActive) return UIHostingController(rootView: view) @@ -515,6 +548,13 @@ public class Router: AuthorizationRouter, } } + public func showGatedContentError(url: String) { + let view = NotAvailableOnMobileView(url: url) + + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + private func openBlockInBrowser(blockURL: URL) { presentAlert( alertTitle: "", @@ -537,7 +577,12 @@ public class Router: AuthorizationRouter, downloads: [DownloadDataTask], manager: DownloadManagerProtocol ) { - let downloadsView = DownloadsView(isSheet: false, downloads: downloads, manager: manager) + let downloadsView = DownloadsView( + isSheet: false, + router: Container.shared.resolve(CourseRouter.self)!, + downloads: downloads, + manager: manager + ) let controller = UIHostingController(rootView: downloadsView) navigationController.pushViewController(controller, animated: true) } @@ -562,13 +607,12 @@ public class Router: AuthorizationRouter, chapterIndex: chapterIndex, sequentialIndex: sequentialIndex ) - - let config = Container.shared.resolve(ConfigProtocol.self) - let isCourseNestedListEnabled = config?.uiComponents.courseNestedListEnabled ?? false var controllers = navigationController.viewControllers + let config = Container.shared.resolve(ConfigProtocol.self)! + let courseDropDownNavigationEnabled = config.uiComponents.courseDropDownNavigationEnabled - if isCourseNestedListEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { + if courseDropDownNavigationEnabled || currentCourseTabSelection == CourseTab.dates.rawValue { controllers.removeLast(1) controllers.append(contentsOf: [controllerUnit]) } else { @@ -694,6 +738,49 @@ public class Router: AuthorizationRouter, navigationController.pushViewController(controller, animated: true) } + public func showVideoSettings() { + let viewModel = Container.shared.resolve(SettingsViewModel.self)! + let view = VideoSettingsView(viewModel: viewModel) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + + public func showDatesAndCalendar() { + let viewModel = Container.shared.resolve(DatesAndCalendarViewModel.self)! + let storage = Container.shared.resolve(ProfileStorage.self) + + let view: AnyView + if storage?.calendarSettings == nil { + view = AnyView(DatesAndCalendarView(viewModel: viewModel)) + } else { + view = AnyView(SyncCalendarOptionsView(viewModel: viewModel)) + } + + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + + public func showSyncCalendarOptions() { + let viewModel = Container.shared.resolve(DatesAndCalendarViewModel.self)! + let view = SyncCalendarOptionsView(viewModel: viewModel) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + + public func showCoursesToSync() { + let viewModel = Container.shared.resolve(DatesAndCalendarViewModel.self)! + let view = CoursesToSyncView(viewModel: viewModel) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + + public func showManageAccount() { + let viewModel = Container.shared.resolve(ManageAccountViewModel.self)! + let view = ManageAccountView(viewModel: viewModel) + let controller = UIHostingController(rootView: view) + navigationController.pushViewController(controller, animated: true) + } + public func showVideoQualityView(viewModel: SettingsViewModel) { let view = VideoQualityView(viewModel: viewModel) let controller = UIHostingController(rootView: view) @@ -708,7 +795,8 @@ public class Router: AuthorizationRouter, let view = VideoDownloadQualityView( downloadQuality: downloadQuality, didSelect: didSelect, - analytics: analytics + analytics: analytics, + router: self ) let controller = UIHostingController(rootView: view) navigationController.pushViewController(controller, animated: true) @@ -765,7 +853,18 @@ public class Router: AuthorizationRouter, let webBrowser = WebBrowser( url: url.absoluteString, pageTitle: title, - showProgress: true + showProgress: true, + connectivity: Container.shared.resolve(ConnectivityProtocol.self)! + ) + let controller = UIHostingController(rootView: webBrowser) + navigationController.pushViewController(controller, animated: true) + } + + public func showSSOWebBrowser(title: String) { + let config = Container.shared.resolve(ConfigProtocol.self)! + let webBrowser = ContainerWebView( + config.baseSSOURL.absoluteString, + title: title ) let controller = UIHostingController(rootView: webBrowser) navigationController.pushViewController(controller, animated: true) diff --git a/OpenEdX/View/MainScreenView.swift b/OpenEdX/View/MainScreenView.swift index f51ebc476..f430a4662 100644 --- a/OpenEdX/View/MainScreenView.swift +++ b/OpenEdX/View/MainScreenView.swift @@ -17,12 +17,11 @@ import Theme struct MainScreenView: View { - @State private var settingsTapped: Bool = false @State private var disableAllTabs: Bool = false - @State private var updateAvaliable: Bool = false + @State private var updateAvailable: Bool = false @ObservedObject private(set) var viewModel: MainScreenViewModel - + init(viewModel: MainScreenViewModel) { self.viewModel = viewModel UITabBar.appearance().isTranslucent = false @@ -35,19 +34,80 @@ struct MainScreenView: View { for: .normal ) } - + var body: some View { TabView(selection: $viewModel.selection) { - let config = Container.shared.resolve(ConfigProtocol.self) - if config?.discovery.enabled ?? false { + switch viewModel.config.dashboard.type { + case .list: + ZStack { + ListDashboardView( + viewModel: Container.shared.resolve(ListDashboardViewModel.self)!, + router: Container.shared.resolve(DashboardRouter.self)! + ) + + if updateAvailable { + UpdateNotificationView(config: viewModel.config) + } + } + .tabItem { + CoreAssets.dashboard.swiftUIImage.renderingMode(.template) + Text(CoreLocalization.Mainscreen.dashboard) + } + .tag(MainTab.dashboard) + .accessibilityIdentifier("dashboard_tabitem") + if viewModel.config.program.enabled { + ZStack { + if viewModel.config.program.type == .webview { + ProgramWebviewView( + viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, + router: Container.shared.resolve(DiscoveryRouter.self)! + ) + } else if viewModel.config.program.type == .native { + Text(CoreLocalization.Mainscreen.inDeveloping) + } + + if updateAvailable { + UpdateNotificationView(config: viewModel.config) + } + } + .tabItem { + CoreAssets.programs.swiftUIImage.renderingMode(.template) + Text(CoreLocalization.Mainscreen.programs) + } + .tag(MainTab.programs) + } + case .gallery: + ZStack { + PrimaryCourseDashboardView( + viewModel: Container.shared.resolve(PrimaryCourseDashboardViewModel.self)!, + router: Container.shared.resolve(DashboardRouter.self)!, + programView: ProgramWebviewView( + viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, + router: Container.shared.resolve(DiscoveryRouter.self)! + ), + openDiscoveryPage: { viewModel.selection = .discovery } + ) + if updateAvailable { + UpdateNotificationView(config: viewModel.config) + } + } + .tabItem { + CoreAssets.learn.swiftUIImage.renderingMode(.template) + Text(CoreLocalization.Mainscreen.learn) + } + .tag(MainTab.dashboard) + .accessibilityIdentifier("dashboard_tabitem") + } + + if viewModel.config.discovery.enabled { ZStack { - if config?.discovery.type == .native { + if viewModel.config.discovery.type == .native { DiscoveryView( viewModel: Container.shared.resolve(DiscoveryViewModel.self)!, router: Container.shared.resolve(DiscoveryRouter.self)!, sourceScreen: viewModel.sourceScreen ) - } else if config?.discovery.type == .webview { + } else if viewModel.config.discovery.type == .webview { DiscoveryWebview( viewModel: Container.shared.resolve( DiscoveryWebviewViewModel.self, @@ -56,7 +116,7 @@ struct MainScreenView: View { ) } - if updateAvaliable { + if updateAvailable { UpdateNotificationView(config: viewModel.config) } } @@ -68,49 +128,9 @@ struct MainScreenView: View { .accessibilityIdentifier("discovery_tabitem") } - ZStack { - DashboardView( - viewModel: Container.shared.resolve(DashboardViewModel.self)!, - router: Container.shared.resolve(DashboardRouter.self)! - ) - if updateAvaliable { - UpdateNotificationView(config: viewModel.config) - } - } - .tabItem { - CoreAssets.dashboard.swiftUIImage.renderingMode(.template) - Text(CoreLocalization.Mainscreen.dashboard) - } - .tag(MainTab.dashboard) - .accessibilityIdentifier("dashboard_tabitem") - - if config?.program.enabled ?? false { - ZStack { - if config?.program.type == .webview { - ProgramWebviewView( - viewModel: Container.shared.resolve(ProgramWebviewViewModel.self)!, - router: Container.shared.resolve(DiscoveryRouter.self)! - ) - } else if config?.program.type == .native { - Text(CoreLocalization.Mainscreen.inDeveloping) - .accessibilityIdentifier("indevelopment_program_text") - } - - if updateAvaliable { - UpdateNotificationView(config: viewModel.config) - } - } - .tabItem { - CoreAssets.programs.swiftUIImage.renderingMode(.template) - Text(CoreLocalization.Mainscreen.programs) - } - .tag(MainTab.programs) - .accessibilityIdentifier("programs_tabitem") - } - VStack { ProfileView( - viewModel: Container.shared.resolve(ProfileViewModel.self)!, settingsTapped: $settingsTapped + viewModel: Container.shared.resolve(ProfileViewModel.self)! ) } .tabItem { @@ -120,22 +140,19 @@ struct MainScreenView: View { .tag(MainTab.profile) .accessibilityIdentifier("profile_tabitem") } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) + .navigationBarHidden(viewModel.selection == .dashboard) + .navigationBarBackButtonHidden(viewModel.selection == .dashboard) .navigationTitle(titleBar()) .toolbar { ToolbarItem(placement: .navigationBarTrailing, content: { - if viewModel.selection == .profile { - Button(action: { - settingsTapped.toggle() - }, label: { - CoreAssets.edit.swiftUIImage.renderingMode(.template) - .foregroundColor(Theme.Colors.navigationBarTintColor) - }) - .accessibilityIdentifier("edit_profile_button") - } else { - VStack {} - } + Button(action: { + let router = Container.shared.resolve(ProfileRouter.self)! + router.showSettings() + }, label: { + CoreAssets.settings.swiftUIImage.renderingMode(.template) + .foregroundColor(Theme.Colors.accentColor) + }) + .accessibilityIdentifier("edit_profile_button") }) } .onReceive(NotificationCenter.default.publisher(for: .onAppUpgradeAccountSettingsTapped)) { _ in @@ -143,7 +160,14 @@ struct MainScreenView: View { disableAllTabs = true } .onReceive(NotificationCenter.default.publisher(for: .onNewVersionAvaliable)) { _ in - updateAvaliable = true + updateAvailable = true + } + .onReceive(NotificationCenter.default.publisher(for: .showDownloadFailed)) { downloads in + if let downloads = downloads.object as? [DownloadDataTask] { + Task { + await viewModel.showDownloadFailed(downloads: downloads) + } + } } .onChange(of: viewModel.selection) { _ in if disableAllTabs { @@ -165,6 +189,7 @@ struct MainScreenView: View { .onFirstAppear { Task { await viewModel.prefetchDataForOffline() + await viewModel.loadCalendar() } } .accentColor(Theme.Colors.accentXColor) @@ -175,7 +200,9 @@ struct MainScreenView: View { case .discovery: return DiscoveryLocalization.title case .dashboard: - return DashboardLocalization.title + return viewModel.config.dashboard.type == .list + ? DashboardLocalization.title + : DashboardLocalization.Learn.title case .programs: return CoreLocalization.Mainscreen.programs case .profile: diff --git a/OpenEdX/View/MainScreenViewModel.swift b/OpenEdX/View/MainScreenViewModel.swift index d3409b191..7c2d17f17 100644 --- a/OpenEdX/View/MainScreenViewModel.swift +++ b/OpenEdX/View/MainScreenViewModel.swift @@ -7,7 +7,11 @@ import Foundation import Core +import OEXFoundation import Profile +import Course +import Swinject +import Combine public enum MainTab { case discovery @@ -17,29 +21,54 @@ public enum MainTab { } final class MainScreenViewModel: ObservableObject { - + private let analytics: MainScreenAnalytics let config: ConfigProtocol + let router: BaseRouter + let syncManager: OfflineSyncManagerProtocol let profileInteractor: ProfileInteractorProtocol + let courseInteractor: CourseInteractorProtocol var sourceScreen: LogistrationSourceScreen - + private var appStorage: CoreStorage & ProfileStorage + private let calendarManager: CalendarManagerProtocol + private var cancellables = Set() + @Published var selection: MainTab = .dashboard - + init(analytics: MainScreenAnalytics, config: ConfigProtocol, + router: BaseRouter, + syncManager: OfflineSyncManagerProtocol, profileInteractor: ProfileInteractorProtocol, + courseInteractor: CourseInteractorProtocol, + appStorage: CoreStorage & ProfileStorage, + calendarManager: CalendarManagerProtocol, sourceScreen: LogistrationSourceScreen = .default ) { self.analytics = analytics self.config = config + self.router = router + self.syncManager = syncManager self.profileInteractor = profileInteractor + self.courseInteractor = courseInteractor + self.appStorage = appStorage + self.calendarManager = calendarManager self.sourceScreen = sourceScreen + + NotificationCenter.default.publisher(for: .shiftCourseDates, object: nil) + .sink { notification in + guard let (courseID, courseName) = notification.object as? (String, String) else { return } + Task { + await self.updateCourseDates(courseID: courseID, courseName: courseName) + } + } + .store(in: &cancellables) } - + public func select(tab: MainTab) { selection = tab } - + func trackMainDiscoveryTabClicked() { analytics.mainDiscoveryTabClicked() } @@ -52,6 +81,37 @@ final class MainScreenViewModel: ObservableObject { func trackMainProfileTabClicked() { analytics.mainProfileTabClicked() } + + @MainActor + func showDownloadFailed(downloads: [DownloadDataTask]) async { + if let sequentials = try? await courseInteractor.getSequentialsContainsBlocks( + blockIds: downloads.map { + $0.blockId + }, + courseID: downloads.first?.courseId ?? "" + ) { + router.presentView( + transitionStyle: .coverVertical, + view: DownloadErrorAlertView( + errorType: .downloadFailed, + sequentials: sequentials, + tryAgain: { [weak self] in + guard let self else { return } + NotificationCenter.default.post( + name: .tryDownloadAgain, + object: downloads + ) + self.router.dismiss(animated: true) + }, + close: { [weak self] in + guard let self else { return } + self.router.dismiss(animated: true) + } + ), + completion: {} + ) + } + } @MainActor func prefetchDataForOffline() async { @@ -60,4 +120,69 @@ final class MainScreenViewModel: ObservableObject { } } + func loadCalendar() async { + if let username = appStorage.user?.username { + await updateCalendarIfNeeded(for: username) + } + } +} + +extension MainScreenViewModel { + + // MARK: Update calendar on startup + private func updateCalendarIfNeeded(for username: String) async { + + if username == appStorage.lastLoginUsername { + let today = Date() + let calendar = Calendar.current + + if let lastUpdate = appStorage.lastCalendarUpdateDate { + if calendar.isDateInToday(lastUpdate) { + return + } + } + appStorage.lastCalendarUpdateDate = today + + guard appStorage.calendarSettings?.calendarName != "", + appStorage.calendarSettings?.courseCalendarSync ?? true + else { + debugLog("No calendar for user: \(username)") + return + } + + do { + var coursesForSync = try await profileInteractor.enrollmentsStatus().filter { $0.recentlyActive } + + let selectedCourses = await calendarManager.filterCoursesBySelected(fetchedCourses: coursesForSync) + + for course in selectedCourses { + if let courseDates = try? await profileInteractor.getCourseDates(courseID: course.courseID), + calendarManager.isDatesChanged(courseID: course.courseID, checksum: courseDates.checksum) { + debugLog("Calendar needs update for courseID: \(course.courseID)") + await calendarManager.removeOutdatedEvents(courseID: course.courseID) + await calendarManager.syncCourse( + courseID: course.courseID, + courseName: course.name, + dates: courseDates + ) + } + } + debugLog("No calendar update needed for username: \(username)") + } catch { + debugLog("Error updating calendar: \(error.localizedDescription)") + } + } else { + appStorage.lastLoginUsername = username + calendarManager.clearAllData(removeCalendar: false) + } + } + + private func updateCourseDates(courseID: String, courseName: String) async { + if let courseDates = try? await profileInteractor.getCourseDates(courseID: courseID), + calendarManager.isDatesChanged(courseID: courseID, checksum: courseDates.checksum) { + debugLog("Calendar update needed for courseID: \(courseID)") + await calendarManager.removeOutdatedEvents(courseID: courseID) + await calendarManager.syncCourse(courseID: courseID, courseName: courseName, dates: courseDates) + } + } } diff --git a/OpenEdX/uk.lproj/Localizable.strings b/OpenEdX/uk.lproj/Localizable.strings deleted file mode 100644 index 8e7d62729..000000000 --- a/OpenEdX/uk.lproj/Localizable.strings +++ /dev/null @@ -1,7 +0,0 @@ -/* - Localizable.strings - OpenEdX - - Created by Vladimir Chekyrta on 13.09.2022. - -*/ diff --git a/Podfile b/Podfile index b644e0124..a207a2f84 100644 --- a/Podfile +++ b/Podfile @@ -1,10 +1,10 @@ -platform :ios, '14.0' +platform :ios, '16.0' use_frameworks! :linkage => :static abstract_target "App" do #Code style - pod 'SwiftLint', '~> 0.5' + pod 'SwiftLint', '~> 0.57.0' #CodeGen for resources pod 'SwiftGen', '~> 6.6' @@ -16,15 +16,12 @@ abstract_target "App" do target "Core" do project './Core/Core.xcodeproj' workspace './Core/Core.xcodeproj' - #Networking - pod 'Alamofire', '~> 5.7' - #Keychain - pod 'KeychainSwift', '~> 20.0' - #SwiftUI backward UIKit access - #pod 'Introspect', '~> 0.6' - pod 'SwiftUIIntrospect', '~> 0.8' - pod 'Kingfisher', '~> 7.8' - pod 'Swinject', '2.8.3' + #Keychain + pod 'KeychainSwift', '~> 24.0' + + target 'CoreTests' do + pod 'SwiftyMocky', :git => 'https://github.com/MakeAWishFoundation/SwiftyMocky.git', :tag => '4.2.0' + end end target "Authorization" do diff --git a/Podfile.lock b/Podfile.lock index afb31e61a..32a960dfc 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,37 +1,25 @@ PODS: - - Alamofire (5.8.0) - - KeychainSwift (20.0.0) - - Kingfisher (7.9.1) + - KeychainSwift (24.0.0) - Sourcery (1.8.0): - Sourcery/CLI-Only (= 1.8.0) - Sourcery/CLI-Only (1.8.0) - - SwiftGen (6.6.2) - - SwiftLint (0.53.0) - - SwiftUIIntrospect (0.12.0) + - SwiftGen (6.6.3) + - SwiftLint (0.57.0) - SwiftyMocky (4.2.0): - Sourcery (= 1.8.0) - - Swinject (2.8.3) DEPENDENCIES: - - Alamofire (~> 5.7) - - KeychainSwift (~> 20.0) - - Kingfisher (~> 7.8) + - KeychainSwift (~> 24.0) - SwiftGen (~> 6.6) - - SwiftLint (~> 0.5) - - SwiftUIIntrospect (~> 0.8) + - SwiftLint (~> 0.57.0) - SwiftyMocky (from `https://github.com/MakeAWishFoundation/SwiftyMocky.git`, tag `4.2.0`) - - Swinject (= 2.8.3) SPEC REPOS: trunk: - - Alamofire - KeychainSwift - - Kingfisher - Sourcery - SwiftGen - SwiftLint - - SwiftUIIntrospect - - Swinject EXTERNAL SOURCES: SwiftyMocky: @@ -44,16 +32,12 @@ CHECKOUT OPTIONS: :tag: 4.2.0 SPEC CHECKSUMS: - Alamofire: 0e92e751b3e9e66d7982db43919d01f313b8eb91 - KeychainSwift: 0ce6a4d13f7228054d1a71bb1b500448fb2ab837 - Kingfisher: 1d14e9f59cbe19389f591c929000332bf70efd32 + KeychainSwift: 007c4647486e4563adca839cf02cef00deb3b670 Sourcery: 6f5fe49b82b7e02e8c65560cbd52e1be67a1af2e - SwiftGen: 1366a7f71aeef49954ca5a63ba4bef6b0f24138c - SwiftLint: 5ce4d6a8ff83f1b5fd5ad5dbf30965d35af65e44 - SwiftUIIntrospect: 89f443402f701a9197e9e54e3c2ed00b10c32e6d + SwiftGen: 4993cbf71cbc4886f775e26f8d5c3a1188ec9f99 + SwiftLint: eb47480d47c982481592c195c221d11013a679cc SwiftyMocky: c5e96e4ff76ec6dbf5a5941aeb039b5a546954a0 - Swinject: 893c9a543000ac2f10ee4cbaf0933c6992c935d5 -PODFILE CHECKSUM: 881176d00eabfe8f78d6022c56c277cf61aad22b +PODFILE CHECKSUM: fe79196bcbd67eb66f3dd20e3a90c1210980722d COCOAPODS: 1.15.2 diff --git a/Profile/Data/ProfileStorage.swift b/Profile/Data/ProfileStorage.swift deleted file mode 100644 index 2770f6060..000000000 --- a/Profile/Data/ProfileStorage.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ProfileStorage.swift -// Profile -// -// Created by  Stepanok Ivan on 30.08.2023. -// - -import Foundation -import Core - -public protocol ProfileStorage { - var userProfile: DataLayer.UserProfile? {get set} -} - -#if DEBUG -public class ProfileStorageMock: ProfileStorage { - - public var userProfile: DataLayer.UserProfile? - - public init() {} -} -#endif diff --git a/Profile/Mockfile b/Profile/Mockfile index 408c90399..9d3f7e354 100644 --- a/Profile/Mockfile +++ b/Profile/Mockfile @@ -14,4 +14,5 @@ unit.tests.mock: - Profile - Foundation - SwiftUI - - Combine \ No newline at end of file + - Combine + - OEXFoundation \ No newline at end of file diff --git a/Profile/Profile.xcodeproj/project.pbxproj b/Profile/Profile.xcodeproj/project.pbxproj index f4ebc7e78..2fc6aba89 100644 --- a/Profile/Profile.xcodeproj/project.pbxproj +++ b/Profile/Profile.xcodeproj/project.pbxproj @@ -10,6 +10,9 @@ 020102D129784B3100BBF80C /* EditProfileViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020102D029784B3100BBF80C /* EditProfileViewModelTests.swift */; }; 020306C82932B13F000949EA /* EditProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020306C72932B13F000949EA /* EditProfileView.swift */; }; 020306CA2932B14D000949EA /* EditProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020306C92932B14D000949EA /* EditProfileViewModel.swift */; }; + 020CBD8D2BC53E1B003D6B4E /* VideoSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020CBD8C2BC53E1B003D6B4E /* VideoSettingsView.swift */; }; + 021C90D52BC986B3004876AF /* DatesAndCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021C90D42BC986B3004876AF /* DatesAndCalendarView.swift */; }; + 021C90D72BC98734004876AF /* DatesAndCalendarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021C90D62BC98734004876AF /* DatesAndCalendarViewModel.swift */; }; 021D924628DC634300ACC565 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924528DC634300ACC565 /* ProfileView.swift */; }; 021D924C28DC884A00ACC565 /* ProfileEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924B28DC884A00ACC565 /* ProfileEndpoint.swift */; }; 021D924E28DC88BB00ACC565 /* ProfileRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D924D28DC88BB00ACC565 /* ProfileRepository.swift */; }; @@ -17,14 +20,27 @@ 021D925528DC92F800ACC565 /* ProfileInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021D925428DC92F800ACC565 /* ProfileInteractor.swift */; }; 021D925C28DDADBD00ACC565 /* swiftgen.yml in Resources */ = {isa = PBXBuildFile; fileRef = 021D925B28DDADBD00ACC565 /* swiftgen.yml */; }; 021D925F28DDADE600ACC565 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 021D926128DDADE600ACC565 /* Localizable.strings */; }; + 022213CD2C0E050B00B917E6 /* CalendarSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022213CC2C0E050B00B917E6 /* CalendarSettings.swift */; }; + 022213D02C0E072400B917E6 /* ProfilePersistenceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022213CF2C0E072400B917E6 /* ProfilePersistenceProtocol.swift */; }; + 022213D72C0E18A300B917E6 /* CourseCalendarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022213D62C0E18A200B917E6 /* CourseCalendarState.swift */; }; + 022301E42BF4B7610028A287 /* SyncCalendarOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022301E32BF4B7610028A287 /* SyncCalendarOptionsView.swift */; }; + 022301E62BF4B7A20028A287 /* AssignmentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022301E52BF4B7A20028A287 /* AssignmentStatusView.swift */; }; + 022301E82BF4C4E70028A287 /* ToggleWithDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022301E72BF4C4E70028A287 /* ToggleWithDescriptionView.swift */; }; 0248F9B128DDB09D0041327E /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0248F9B028DDB09D0041327E /* Strings.swift */; }; + 0250C1AD2C231E2500B9E554 /* ProfileCoreModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 022213D32C0E092300B917E6 /* ProfileCoreModel.xcdatamodeld */; }; 0259104429C39C9E004B5A55 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0259104329C39C9E004B5A55 /* SettingsView.swift */; }; 0259104629C39CCF004B5A55 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0259104529C39CCF004B5A55 /* SettingsViewModel.swift */; }; 0259104829C3A5F0004B5A55 /* VideoQualityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0259104729C3A5F0004B5A55 /* VideoQualityView.swift */; }; 025DE1A028DB4D9D0053E0F4 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 025DE19F28DB4D9D0053E0F4 /* Core.framework */; }; 0262149229AE57A1008BD75A /* DeleteAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0262149129AE57A1008BD75A /* DeleteAccountView.swift */; }; 0262149429AE57B1008BD75A /* DeleteAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0262149329AE57B1008BD75A /* DeleteAccountViewModel.swift */; }; + 027FEF372C1710040037807E /* CourseCalendarEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 027FEF362C1710040037807E /* CourseCalendarEvent.swift */; }; + 0281D1532BEA9A40006DAD7A /* NewCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0281D1522BEA9A40006DAD7A /* NewCalendarView.swift */; }; + 0281D1552BEBA8D9006DAD7A /* DropDownPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0281D1542BEBA8D9006DAD7A /* DropDownPicker.swift */; }; + 0288C4EA2BC6AE2D009158B9 /* ManageAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0288C4E92BC6AE2D009158B9 /* ManageAccountView.swift */; }; + 0288C4EC2BC6AE82009158B9 /* ManageAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0288C4EB2BC6AE82009158B9 /* ManageAccountViewModel.swift */; }; 029301DA2938948500E99AB8 /* ProfileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029301D92938948500E99AB8 /* ProfileType.swift */; }; + 0294987A2C4E4332008FD0E7 /* RelativeDatesToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029498792C4E4332008FD0E7 /* RelativeDatesToggleView.swift */; }; 02A4833329B7710A00D33F33 /* DeleteAccountViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A4833229B7710A00D33F33 /* DeleteAccountViewModelTests.swift */; }; 02A9A91D2978194A00B55797 /* ProfileViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9A91C2978194A00B55797 /* ProfileViewModelTests.swift */; }; 02A9A91E2978194A00B55797 /* Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 020F834A28DB4CCD0062FA70 /* Profile.framework */; platformFilter = ios; }; @@ -32,11 +48,21 @@ 02B089432A9F832200754BD4 /* ProfileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B089422A9F832200754BD4 /* ProfileStorage.swift */; }; 02D0FD092AD698380020D752 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D0FD082AD698380020D752 /* UserProfileView.swift */; }; 02D0FD0B2AD6984D0020D752 /* UserProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D0FD0A2AD6984D0020D752 /* UserProfileViewModel.swift */; }; + 02EBC7532C19CD1800BE182C /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EBC7522C19CD1700BE182C /* CalendarManager.swift */; }; 02F175352A4DAD030019CD70 /* ProfileAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F175342A4DAD030019CD70 /* ProfileAnalytics.swift */; }; 02F3BFE7292539850051930C /* ProfileRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F3BFE6292539850051930C /* ProfileRouter.swift */; }; + 02F81DDF2BF4D83E002D3604 /* CalendarDialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F81DDE2BF4D83E002D3604 /* CalendarDialogView.swift */; }; + 02F81DE12BF4F009002D3604 /* CoursesToSyncView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F81DE02BF4F009002D3604 /* CoursesToSyncView.swift */; }; + 02F81DE32BF502B9002D3604 /* SyncSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F81DE22BF502B9002D3604 /* SyncSelector.swift */; }; + 02FE9A802BC707D500B3C206 /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02FE9A7F2BC707D400B3C206 /* SettingsViewModelTests.swift */; }; 0796C8C929B7905300444B05 /* ProfileBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0796C8C829B7905300444B05 /* ProfileBottomSheet.swift */; }; 25B36FF48C1307888A3890DA /* Pods_App_Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEA369C38362C1A91A012F70 /* Pods_App_Profile.framework */; }; BAD9CA3F2B29BF5C00DE790A /* ProfileSupportInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD9CA3E2B29BF5C00DE790A /* ProfileSupportInfoView.swift */; }; + CE1735042CD23D7A00F9606A /* DatesAndCalendarViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1735032CD23D7A00F9606A /* DatesAndCalendarViewModelTests.swift */; }; + CE961F032CD163FD00799B9F /* CalendarManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE961F022CD163FD00799B9F /* CalendarManagerTests.swift */; }; + CE7CAF3D2CC1562C00E0AC9D /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CE7CAF3C2CC1562C00E0AC9D /* OEXFoundation */; }; + CEB1E2702CC14EB000921517 /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CEB1E26F2CC14EB000921517 /* OEXFoundation */; }; + CEBCA4312CC13CB900076589 /* BranchSDK in Frameworks */ = {isa = PBXBuildFile; productRef = CEBCA4302CC13CB900076589 /* BranchSDK */; }; E8264C634DD8AD314ECE8905 /* Pods_App_Profile_ProfileTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C85ADF87135E03275A980E07 /* Pods_App_Profile_ProfileTests.framework */; }; /* End PBXBuildFile section */ @@ -50,11 +76,27 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + CE7CAF3F2CC1562C00E0AC9D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 020102D029784B3100BBF80C /* EditProfileViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileViewModelTests.swift; sourceTree = ""; }; 020306C72932B13F000949EA /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = ""; }; 020306C92932B14D000949EA /* EditProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileViewModel.swift; sourceTree = ""; }; + 020CBD8C2BC53E1B003D6B4E /* VideoSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoSettingsView.swift; sourceTree = ""; }; 020F834A28DB4CCD0062FA70 /* Profile.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Profile.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 021C90D42BC986B3004876AF /* DatesAndCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatesAndCalendarView.swift; sourceTree = ""; }; + 021C90D62BC98734004876AF /* DatesAndCalendarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatesAndCalendarViewModel.swift; sourceTree = ""; }; 021D924528DC634300ACC565 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 021D924B28DC884A00ACC565 /* ProfileEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEndpoint.swift; sourceTree = ""; }; 021D924D28DC88BB00ACC565 /* ProfileRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRepository.swift; sourceTree = ""; }; @@ -62,6 +104,13 @@ 021D925428DC92F800ACC565 /* ProfileInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileInteractor.swift; sourceTree = ""; }; 021D925B28DDADBD00ACC565 /* swiftgen.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = swiftgen.yml; sourceTree = ""; }; 021D926028DDADE600ACC565 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 022213CC2C0E050B00B917E6 /* CalendarSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarSettings.swift; sourceTree = ""; }; + 022213CF2C0E072400B917E6 /* ProfilePersistenceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePersistenceProtocol.swift; sourceTree = ""; }; + 022213D42C0E092300B917E6 /* ProfileCoreModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ProfileCoreModel.xcdatamodel; sourceTree = ""; }; + 022213D62C0E18A200B917E6 /* CourseCalendarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseCalendarState.swift; sourceTree = ""; }; + 022301E32BF4B7610028A287 /* SyncCalendarOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncCalendarOptionsView.swift; sourceTree = ""; }; + 022301E52BF4B7A20028A287 /* AssignmentStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssignmentStatusView.swift; sourceTree = ""; }; + 022301E72BF4C4E70028A287 /* ToggleWithDescriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleWithDescriptionView.swift; sourceTree = ""; }; 0248F9B028DDB09D0041327E /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 0259104329C39C9E004B5A55 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 0259104529C39CCF004B5A55 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; @@ -69,7 +118,13 @@ 025DE19F28DB4D9D0053E0F4 /* Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Core.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0262149129AE57A1008BD75A /* DeleteAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountView.swift; sourceTree = ""; }; 0262149329AE57B1008BD75A /* DeleteAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountViewModel.swift; sourceTree = ""; }; + 027FEF362C1710040037807E /* CourseCalendarEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseCalendarEvent.swift; sourceTree = ""; }; + 0281D1522BEA9A40006DAD7A /* NewCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCalendarView.swift; sourceTree = ""; }; + 0281D1542BEBA8D9006DAD7A /* DropDownPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropDownPicker.swift; sourceTree = ""; }; + 0288C4E92BC6AE2D009158B9 /* ManageAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageAccountView.swift; sourceTree = ""; }; + 0288C4EB2BC6AE82009158B9 /* ManageAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageAccountViewModel.swift; sourceTree = ""; }; 029301D92938948500E99AB8 /* ProfileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileType.swift; sourceTree = ""; }; + 029498792C4E4332008FD0E7 /* RelativeDatesToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelativeDatesToggleView.swift; sourceTree = ""; }; 02A4833229B7710A00D33F33 /* DeleteAccountViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DeleteAccountViewModelTests.swift; path = ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift; sourceTree = SOURCE_ROOT; }; 02A9A91A2978194A00B55797 /* ProfileTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ProfileTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 02A9A91C2978194A00B55797 /* ProfileViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModelTests.swift; sourceTree = ""; }; @@ -77,9 +132,13 @@ 02B089422A9F832200754BD4 /* ProfileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStorage.swift; sourceTree = ""; }; 02D0FD082AD698380020D752 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = ""; }; 02D0FD0A2AD6984D0020D752 /* UserProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileViewModel.swift; sourceTree = ""; }; - 02ED50CE29A64BAD008341CD /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + 02EBC7522C19CD1700BE182C /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = ""; }; 02F175342A4DAD030019CD70 /* ProfileAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileAnalytics.swift; sourceTree = ""; }; 02F3BFE6292539850051930C /* ProfileRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRouter.swift; sourceTree = ""; }; + 02F81DDE2BF4D83E002D3604 /* CalendarDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarDialogView.swift; sourceTree = ""; }; + 02F81DE02BF4F009002D3604 /* CoursesToSyncView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoursesToSyncView.swift; sourceTree = ""; }; + 02F81DE22BF502B9002D3604 /* SyncSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSelector.swift; sourceTree = ""; }; + 02FE9A7F2BC707D400B3C206 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SettingsViewModelTests.swift; path = ProfileTests/Presentation/Settings/SettingsViewModelTests.swift; sourceTree = SOURCE_ROOT; }; 0796C8C829B7905300444B05 /* ProfileBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileBottomSheet.swift; sourceTree = ""; }; 0E5054C44435557666B6D885 /* Pods-App-Profile.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.debugstage.xcconfig"; sourceTree = ""; }; 3674C51E1BE41D834B5C4E99 /* Pods-App-Profile.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.debugdev.xcconfig"; sourceTree = ""; }; @@ -98,6 +157,8 @@ BAD9CA3E2B29BF5C00DE790A /* ProfileSupportInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSupportInfoView.swift; sourceTree = ""; }; BEA369C38362C1A91A012F70 /* Pods_App_Profile.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Profile.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C85ADF87135E03275A980E07 /* Pods_App_Profile_ProfileTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Profile_ProfileTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CE1735032CD23D7A00F9606A /* DatesAndCalendarViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatesAndCalendarViewModelTests.swift; sourceTree = ""; }; + CE961F022CD163FD00799B9F /* CalendarManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManagerTests.swift; sourceTree = ""; }; F52EFE7DC07BE68B9A302DAF /* Pods-App-Profile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile.debug.xcconfig"; path = "Target Support Files/Pods-App-Profile/Pods-App-Profile.debug.xcconfig"; sourceTree = ""; }; FB33709D5DBACDEA33BD016F /* Pods-App-Profile-ProfileTests.debugstage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Profile-ProfileTests.debugstage.xcconfig"; path = "Target Support Files/Pods-App-Profile-ProfileTests/Pods-App-Profile-ProfileTests.debugstage.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -107,6 +168,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CEB1E2702CC14EB000921517 /* OEXFoundation in Frameworks */, + CEBCA4312CC13CB900076589 /* BranchSDK in Frameworks */, 025DE1A028DB4D9D0053E0F4 /* Core.framework in Frameworks */, 25B36FF48C1307888A3890DA /* Pods_App_Profile.framework in Frameworks */, ); @@ -117,6 +180,7 @@ buildActionMask = 2147483647; files = ( 02A9A91E2978194A00B55797 /* Profile.framework in Frameworks */, + CE7CAF3D2CC1562C00E0AC9D /* OEXFoundation in Frameworks */, E8264C634DD8AD314ECE8905 /* Pods_App_Profile_ProfileTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -148,7 +212,6 @@ 020F834028DB4CCD0062FA70 = { isa = PBXGroup; children = ( - 02B089412A9F830D00754BD4 /* Data */, 021D925B28DDADBD00ACC565 /* swiftgen.yml */, 020F834C28DB4CCD0062FA70 /* Profile */, 02A9A91B2978194A00B55797 /* ProfileTests */, @@ -180,9 +243,24 @@ path = Profile; sourceTree = ""; }; + 021C90D32BC986A4004876AF /* DatesAndCalendar */ = { + isa = PBXGroup; + children = ( + 022213CB2C0E04FB00B917E6 /* Models */, + 0281D1512BEA9A2D006DAD7A /* Elements */, + 021C90D42BC986B3004876AF /* DatesAndCalendarView.swift */, + 022301E32BF4B7610028A287 /* SyncCalendarOptionsView.swift */, + 02F81DE02BF4F009002D3604 /* CoursesToSyncView.swift */, + 021C90D62BC98734004876AF /* DatesAndCalendarViewModel.swift */, + 02EBC7522C19CD1700BE182C /* CalendarManager.swift */, + ); + path = DatesAndCalendar; + sourceTree = ""; + }; 021D924428DC631800ACC565 /* Presentation */ = { isa = PBXGroup; children = ( + 021C90D32BC986A4004876AF /* DatesAndCalendar */, 0259104229C39C84004B5A55 /* Settings */, 0203DC3D29AE79F80017BD05 /* Profile */, 0203DC3C29AE79EB0017BD05 /* EditProfile */, @@ -196,6 +274,8 @@ 021D924928DC882B00ACC565 /* Data */ = { isa = PBXGroup; children = ( + 02B089422A9F832200754BD4 /* ProfileStorage.swift */, + 022213CE2C0E070F00B917E6 /* Persistence */, 021D924A28DC883000ACC565 /* Network */, 021D924D28DC88BB00ACC565 /* ProfileRepository.swift */, ); @@ -227,6 +307,25 @@ path = SwiftGen; sourceTree = ""; }; + 022213CB2C0E04FB00B917E6 /* Models */ = { + isa = PBXGroup; + children = ( + 022213CC2C0E050B00B917E6 /* CalendarSettings.swift */, + 022213D62C0E18A200B917E6 /* CourseCalendarState.swift */, + 027FEF362C1710040037807E /* CourseCalendarEvent.swift */, + ); + path = Models; + sourceTree = ""; + }; + 022213CE2C0E070F00B917E6 /* Persistence */ = { + isa = PBXGroup; + children = ( + 022213CF2C0E072400B917E6 /* ProfilePersistenceProtocol.swift */, + 022213D32C0E092300B917E6 /* ProfileCoreModel.xcdatamodeld */, + ); + path = Persistence; + sourceTree = ""; + }; 02362C8329350C0C00134A5B /* Model */ = { isa = PBXGroup; children = ( @@ -239,6 +338,9 @@ isa = PBXGroup; children = ( 0259104329C39C9E004B5A55 /* SettingsView.swift */, + 0288C4E92BC6AE2D009158B9 /* ManageAccountView.swift */, + 0288C4EB2BC6AE82009158B9 /* ManageAccountViewModel.swift */, + 020CBD8C2BC53E1B003D6B4E /* VideoSettingsView.swift */, 0259104729C3A5F0004B5A55 /* VideoQualityView.swift */, 0259104529C39CCF004B5A55 /* SettingsViewModel.swift */, ); @@ -254,9 +356,24 @@ path = DeleteAccount; sourceTree = ""; }; + 0281D1512BEA9A2D006DAD7A /* Elements */ = { + isa = PBXGroup; + children = ( + 0281D1522BEA9A40006DAD7A /* NewCalendarView.swift */, + 0281D1542BEBA8D9006DAD7A /* DropDownPicker.swift */, + 022301E52BF4B7A20028A287 /* AssignmentStatusView.swift */, + 022301E72BF4C4E70028A287 /* ToggleWithDescriptionView.swift */, + 02F81DDE2BF4D83E002D3604 /* CalendarDialogView.swift */, + 02F81DE22BF502B9002D3604 /* SyncSelector.swift */, + 029498792C4E4332008FD0E7 /* RelativeDatesToggleView.swift */, + ); + path = Elements; + sourceTree = ""; + }; 02A4832F29B770B600D33F33 /* Profile */ = { isa = PBXGroup; children = ( + 02FE9A7F2BC707D400B3C206 /* SettingsViewModelTests.swift */, 02A9A91C2978194A00B55797 /* ProfileViewModelTests.swift */, ); path = Profile; @@ -281,20 +398,14 @@ 02A9A91B2978194A00B55797 /* ProfileTests */ = { isa = PBXGroup; children = ( + CE961F022CD163FD00799B9F /* CalendarManagerTests.swift */, + CE1735032CD23D7A00F9606A /* DatesAndCalendarViewModelTests.swift */, 0766DFD3299AD9D800EBEF6A /* Presentation */, 02A9A92A29781A6300B55797 /* ProfileMock.generated.swift */, ); path = ProfileTests; sourceTree = ""; }; - 02B089412A9F830D00754BD4 /* Data */ = { - isa = PBXGroup; - children = ( - 02B089422A9F832200754BD4 /* ProfileStorage.swift */, - ); - path = Data; - sourceTree = ""; - }; 02D0FD072AD695E10020D752 /* UserProfile */ = { isa = PBXGroup; children = ( @@ -405,6 +516,7 @@ 02A9A9172978194A00B55797 /* Frameworks */, 02A9A9182978194A00B55797 /* Resources */, E4D48C711DA7F62E34A40309 /* [CP] Copy Pods Resources */, + CE7CAF3F2CC1562C00E0AC9D /* Embed Frameworks */, ); buildRules = ( ); @@ -455,6 +567,10 @@ uk, ); mainGroup = 020F834028DB4CCD0062FA70; + packageReferences = ( + CEBCA42F2CC13CB900076589 /* XCRemoteSwiftPackageReference "ios-branch-sdk-spm" */, + CEB1E26E2CC14EB000921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */, + ); productRefGroup = 020F834B28DB4CCD0062FA70 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -572,25 +688,45 @@ buildActionMask = 2147483647; files = ( 021D924E28DC88BB00ACC565 /* ProfileRepository.swift in Sources */, + 0288C4EC2BC6AE82009158B9 /* ManageAccountViewModel.swift in Sources */, + 0281D1552BEBA8D9006DAD7A /* DropDownPicker.swift in Sources */, 0796C8C929B7905300444B05 /* ProfileBottomSheet.swift in Sources */, 021D924C28DC884A00ACC565 /* ProfileEndpoint.swift in Sources */, + 02F81DE12BF4F009002D3604 /* CoursesToSyncView.swift in Sources */, + 027FEF372C1710040037807E /* CourseCalendarEvent.swift in Sources */, + 0281D1532BEA9A40006DAD7A /* NewCalendarView.swift in Sources */, + 02EBC7532C19CD1800BE182C /* CalendarManager.swift in Sources */, BAD9CA3F2B29BF5C00DE790A /* ProfileSupportInfoView.swift in Sources */, + 022301E82BF4C4E70028A287 /* ToggleWithDescriptionView.swift in Sources */, + 022301E42BF4B7610028A287 /* SyncCalendarOptionsView.swift in Sources */, + 021C90D52BC986B3004876AF /* DatesAndCalendarView.swift in Sources */, 020306C82932B13F000949EA /* EditProfileView.swift in Sources */, 0262149229AE57A1008BD75A /* DeleteAccountView.swift in Sources */, + 02F81DE32BF502B9002D3604 /* SyncSelector.swift in Sources */, 02D0FD092AD698380020D752 /* UserProfileView.swift in Sources */, 0259104629C39CCF004B5A55 /* SettingsViewModel.swift in Sources */, + 021C90D72BC98734004876AF /* DatesAndCalendarViewModel.swift in Sources */, 021D925528DC92F800ACC565 /* ProfileInteractor.swift in Sources */, 029301DA2938948500E99AB8 /* ProfileType.swift in Sources */, + 0294987A2C4E4332008FD0E7 /* RelativeDatesToggleView.swift in Sources */, 0259104429C39C9E004B5A55 /* SettingsView.swift in Sources */, 02B089432A9F832200754BD4 /* ProfileStorage.swift in Sources */, + 022213D72C0E18A300B917E6 /* CourseCalendarState.swift in Sources */, + 0250C1AD2C231E2500B9E554 /* ProfileCoreModel.xcdatamodeld in Sources */, 021D925228DC918D00ACC565 /* ProfileViewModel.swift in Sources */, + 022213CD2C0E050B00B917E6 /* CalendarSettings.swift in Sources */, + 022213D02C0E072400B917E6 /* ProfilePersistenceProtocol.swift in Sources */, 0248F9B128DDB09D0041327E /* Strings.swift in Sources */, 020306CA2932B14D000949EA /* EditProfileViewModel.swift in Sources */, 0259104829C3A5F0004B5A55 /* VideoQualityView.swift in Sources */, + 0288C4EA2BC6AE2D009158B9 /* ManageAccountView.swift in Sources */, + 022301E62BF4B7A20028A287 /* AssignmentStatusView.swift in Sources */, 021D924628DC634300ACC565 /* ProfileView.swift in Sources */, 02F3BFE7292539850051930C /* ProfileRouter.swift in Sources */, 02D0FD0B2AD6984D0020D752 /* UserProfileViewModel.swift in Sources */, + 020CBD8D2BC53E1B003D6B4E /* VideoSettingsView.swift in Sources */, 02F175352A4DAD030019CD70 /* ProfileAnalytics.swift in Sources */, + 02F81DDF2BF4D83E002D3604 /* CalendarDialogView.swift in Sources */, 0262149429AE57B1008BD75A /* DeleteAccountViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -600,7 +736,10 @@ buildActionMask = 2147483647; files = ( 02A9A92B29781A6300B55797 /* ProfileMock.generated.swift in Sources */, + CE1735042CD23D7A00F9606A /* DatesAndCalendarViewModelTests.swift in Sources */, 02A4833329B7710A00D33F33 /* DeleteAccountViewModelTests.swift in Sources */, + 02FE9A802BC707D500B3C206 /* SettingsViewModelTests.swift in Sources */, + CE961F032CD163FD00799B9F /* CalendarManagerTests.swift in Sources */, 02A9A91D2978194A00B55797 /* ProfileViewModelTests.swift in Sources */, 020102D129784B3100BBF80C /* EditProfileViewModelTests.swift in Sources */, ); @@ -622,7 +761,6 @@ isa = PBXVariantGroup; children = ( 021D926028DDADE600ACC565 /* en */, - 02ED50CE29A64BAD008341CD /* uk */, ); name = Localizable.strings; sourceTree = ""; @@ -758,14 +896,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -793,14 +931,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -890,14 +1028,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -988,14 +1126,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1080,14 +1218,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1171,14 +1309,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1205,7 +1343,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1226,7 +1364,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1247,7 +1385,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1268,7 +1406,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1289,7 +1427,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1310,7 +1448,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1394,14 +1532,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1429,7 +1567,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1507,14 +1645,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1541,7 +1679,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MACOSX_DEPLOYMENT_TARGET = 12.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.ProfileTests; @@ -1603,6 +1741,56 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + CEB1E26E2CC14EB000921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; + requirement = { + kind = exactVersion; + version = 1.0.0; + }; + }; + CEBCA42F2CC13CB900076589 /* XCRemoteSwiftPackageReference "ios-branch-sdk-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/BranchMetrics/ios-branch-sdk-spm"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.6.4; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + CE7CAF3C2CC1562C00E0AC9D /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E26E2CC14EB000921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; + CEB1E26F2CC14EB000921517 /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E26E2CC14EB000921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; + CEBCA4302CC13CB900076589 /* BranchSDK */ = { + isa = XCSwiftPackageProductDependency; + package = CEBCA42F2CC13CB900076589 /* XCRemoteSwiftPackageReference "ios-branch-sdk-spm" */; + productName = BranchSDK; + }; +/* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + 022213D32C0E092300B917E6 /* ProfileCoreModel.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 022213D42C0E092300B917E6 /* ProfileCoreModel.xcdatamodel */, + ); + currentVersion = 022213D42C0E092300B917E6 /* ProfileCoreModel.xcdatamodel */; + path = ProfileCoreModel.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = 020F834128DB4CCD0062FA70 /* Project object */; } diff --git a/Profile/Profile/Data/Network/ProfileEndpoint.swift b/Profile/Profile/Data/Network/ProfileEndpoint.swift index bf3b330ca..c169ec575 100644 --- a/Profile/Profile/Data/Network/ProfileEndpoint.swift +++ b/Profile/Profile/Data/Network/ProfileEndpoint.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import Alamofire enum ProfileEndpoint: EndPointType { @@ -16,27 +17,33 @@ enum ProfileEndpoint: EndPointType { case deleteProfilePicture(username: String) case logOut(refreshToken: String, clientID: String) case deleteAccount(password: String) + case enrollmentsStatus(username: String) + case getCourseDates(courseID: String) var path: String { switch self { - case .getUserProfile(let username): - return "api/user/v1/accounts/\(username)" + case let .getUserProfile(username): + return "/api/user/v1/accounts/\(username)" case .logOut: - return "oauth2/revoke_token/" + return "/oauth2/revoke_token/" case let .updateUserProfile(username, _): - return "api/user/v1/accounts/\(username)" + return "/api/user/v1/accounts/\(username)" case let .uploadProfilePicture(username, _): return "/api/user/v1/accounts/\(username)/image" - case .deleteProfilePicture(username: let username): + case let .deleteProfilePicture(username): return "/api/user/v1/accounts/\(username)/image" case .deleteAccount: return "/api/user/v1/accounts/deactivate_logout/" + case let .enrollmentsStatus(username): + return "/api/mobile/v1/users/\(username)/enrollments_status/" + case let .getCourseDates(courseID): + return "/api/course_home/v1/dates/\(courseID)" } } var httpMethod: HTTPMethod { switch self { - case .getUserProfile: + case .getUserProfile, .enrollmentsStatus, .getCourseDates: return .get case .logOut: return .post @@ -82,11 +89,15 @@ enum ProfileEndpoint: EndPointType { "username": username ] return .requestParameters(parameters: params, encoding: JSONEncoding.default) - case .deleteAccount(password: let password): + case let .deleteAccount(password): let params: [String: String] = [ "password": password ] return .requestParameters(parameters: params, encoding: URLEncoding.httpBody) + case let .enrollmentsStatus(username): + return .requestParameters(parameters: nil, encoding: JSONEncoding.default) + case .getCourseDates: + return .requestParameters(encoding: JSONEncoding.default) } } } diff --git a/Profile/Profile/Data/Persistence/ProfileCoreModel.xcdatamodeld/ProfileCoreModel.xcdatamodel/contents b/Profile/Profile/Data/Persistence/ProfileCoreModel.xcdatamodeld/ProfileCoreModel.xcdatamodel/contents new file mode 100644 index 000000000..7ebe9bf79 --- /dev/null +++ b/Profile/Profile/Data/Persistence/ProfileCoreModel.xcdatamodeld/ProfileCoreModel.xcdatamodel/contents @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Profile/Profile/Data/Persistence/ProfilePersistenceProtocol.swift b/Profile/Profile/Data/Persistence/ProfilePersistenceProtocol.swift new file mode 100644 index 000000000..e87f1aa19 --- /dev/null +++ b/Profile/Profile/Data/Persistence/ProfilePersistenceProtocol.swift @@ -0,0 +1,40 @@ +// +// ProfilePersistenceProtocol.swift +// Profile +// +// Created by  Stepanok Ivan on 03.06.2024. +// + +import CoreData +import Core + +//sourcery: AutoMockable +public protocol ProfilePersistenceProtocol { + func getCourseState(courseID: String) -> CourseCalendarState? + func getAllCourseStates() -> [CourseCalendarState] + func saveCourseState(state: CourseCalendarState) + func removeCourseState(courseID: String) + func deleteAllCourseStatesAndEvents() + func saveCourseCalendarEvent(_ event: CourseCalendarEvent) + func removeCourseCalendarEvents(for courseId: String) + func removeAllCourseCalendarEvents() + func getCourseCalendarEvents(for courseId: String) -> [CourseCalendarEvent] +} + +#if DEBUG +public struct ProfilePersistenceMock: ProfilePersistenceProtocol { + public func getCourseState(courseID: String) -> CourseCalendarState? { nil } + public func getAllCourseStates() -> [CourseCalendarState] {[]} + public func saveCourseState(state: CourseCalendarState) {} + public func removeCourseState(courseID: String) {} + public func deleteAllCourseStatesAndEvents() {} + public func saveCourseCalendarEvent(_ event: CourseCalendarEvent) {} + public func removeCourseCalendarEvents(for courseId: String) {} + public func removeAllCourseCalendarEvents() {} + public func getCourseCalendarEvents(for courseId: String) -> [CourseCalendarEvent] { [] } +} +#endif + +public final class ProfileBundle { + private init() {} +} diff --git a/Profile/Profile/Data/ProfileRepository.swift b/Profile/Profile/Data/ProfileRepository.swift index 7608ff849..8bad93b00 100644 --- a/Profile/Profile/Data/ProfileRepository.swift +++ b/Profile/Profile/Data/ProfileRepository.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation import Alamofire public protocol ProfileRepositoryProtocol { @@ -22,6 +23,8 @@ public protocol ProfileRepositoryProtocol { func deleteAccount(password: String) async throws -> Bool func getSettings() -> UserSettings func saveSettings(_ settings: UserSettings) + func enrollmentsStatus() async throws -> [CourseForSync] + func getCourseDates(courseID: String) async throws -> CourseDates } public class ProfileRepository: ProfileRepositoryProtocol { @@ -149,6 +152,20 @@ public class ProfileRepository: ProfileRepositoryProtocol { public func saveSettings(_ settings: UserSettings) { storage.userSettings = settings } + + public func enrollmentsStatus() async throws -> [CourseForSync] { + let username = storage.user?.username ?? "" + let result = try await api.requestData(ProfileEndpoint.enrollmentsStatus(username: username)) + .mapResponse(DataLayer.EnrollmentsStatus.self).domain + return result + } + + public func getCourseDates(courseID: String) async throws -> CourseDates { + let courseDates = try await api.requestData( + ProfileEndpoint.getCourseDates(courseID: courseID) + ).mapResponse(DataLayer.CourseDates.self).domain(useRelativeDates: storage.useRelativeDates) + return courseDates + } } // Mark - For testing and SwiftUI preview @@ -164,7 +181,8 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol { yearOfBirth: 0, country: "", shortBiography: "", - isFullProfile: false) + isFullProfile: false, + email: "") } func getMyProfileOffline() -> Core.UserProfile? { @@ -182,7 +200,8 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol { of his music, writing and drawings, on film, and in interviews. His songwriting partnership with Paul McCartney remains the most successful in history """, - isFullProfile: true + isFullProfile: true, + email: "" ) } @@ -201,7 +220,8 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol { of his music, writing and drawings, on film, and in interviews. His songwriting partnership with Paul McCartney remains the most successful in history """, - isFullProfile: true + isFullProfile: true, + email: "" ) } @@ -224,7 +244,8 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol { yearOfBirth: 1970, country: "USA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) } @@ -234,6 +255,38 @@ class ProfileRepositoryMock: ProfileRepositoryProtocol { return UserSettings(wifiOnly: true, streamingQuality: .auto, downloadQuality: .auto) } public func saveSettings(_ settings: UserSettings) {} + + public func enrollmentsStatus() async throws -> [CourseForSync] { + let result = [ + DataLayer.EnrollmentsStatusElement(courseID: "1", courseName: "Course 1", recentlyActive: true), + DataLayer.EnrollmentsStatusElement(courseID: "2", courseName: "Course 2", recentlyActive: false), + DataLayer.EnrollmentsStatusElement(courseID: "3", courseName: "Course 3", recentlyActive: false), + DataLayer.EnrollmentsStatusElement(courseID: "4", courseName: "Course 4", recentlyActive: true), + DataLayer.EnrollmentsStatusElement(courseID: "5", courseName: "Course 5", recentlyActive: true), + DataLayer.EnrollmentsStatusElement(courseID: "6", courseName: "Course 6", recentlyActive: false), + DataLayer.EnrollmentsStatusElement(courseID: "7", courseName: "Course 7", recentlyActive: true), + DataLayer.EnrollmentsStatusElement(courseID: "8", courseName: "Course 8", recentlyActive: true), + DataLayer.EnrollmentsStatusElement(courseID: "9", courseName: "Course 9", recentlyActive: true), + ] + + return result.domain + } + + func getCourseDates(courseID: String) async throws -> CourseDates { + return CourseDates( + datesBannerInfo: DatesBannerInfo( + missedDeadlines: false, + contentTypeGatingEnabled: false, + missedGatedContent: false, + verifiedUpgradeLink: "", + status: .datesTabInfoBanner + ), + courseDateBlocks: [], + hasEnded: true, + learnerIsFullAccess: true, + userTimezone: nil + ) + } } // swiftlint:enable all #endif diff --git a/Profile/Profile/Data/ProfileStorage.swift b/Profile/Profile/Data/ProfileStorage.swift new file mode 100644 index 000000000..68347091f --- /dev/null +++ b/Profile/Profile/Data/ProfileStorage.swift @@ -0,0 +1,38 @@ +// +// ProfileStorage.swift +// Profile +// +// Created by  Stepanok Ivan on 30.08.2023. +// + +import Foundation +import Core +import UIKit + +//sourcery: AutoMockable +public protocol ProfileStorage { + var userProfile: DataLayer.UserProfile? {get set} + var useRelativeDates: Bool {get set} + var calendarSettings: CalendarSettings? {get set} + var hideInactiveCourses: Bool? {get set} + var lastLoginUsername: String? {get set} + var lastCalendarName: String? {get set} + var lastCalendarUpdateDate: Date? {get set} + var firstCalendarUpdate: Bool? {get set} +} + +#if DEBUG +public class ProfileStorageMock: ProfileStorage { + + public var userProfile: DataLayer.UserProfile? + public var useRelativeDates: Bool = true + public var calendarSettings: CalendarSettings? + public var hideInactiveCourses: Bool? + public var lastLoginUsername: String? + public var lastCalendarName: String? + public var lastCalendarUpdateDate: Date? + public var firstCalendarUpdate: Bool? + + public init() {} +} +#endif diff --git a/Profile/Profile/Domain/ProfileInteractor.swift b/Profile/Profile/Domain/ProfileInteractor.swift index 18e09aec2..bce7cf620 100644 --- a/Profile/Profile/Domain/ProfileInteractor.swift +++ b/Profile/Profile/Domain/ProfileInteractor.swift @@ -23,6 +23,8 @@ public protocol ProfileInteractorProtocol { func deleteAccount(password: String) async throws -> Bool func getSettings() -> UserSettings func saveSettings(_ settings: UserSettings) + func enrollmentsStatus() async throws -> [CourseForSync] + func getCourseDates(courseID: String) async throws -> CourseDates } public class ProfileInteractor: ProfileInteractorProtocol { @@ -80,6 +82,14 @@ public class ProfileInteractor: ProfileInteractorProtocol { public func saveSettings(_ settings: UserSettings) { return repository.saveSettings(settings) } + + public func enrollmentsStatus() async throws -> [CourseForSync] { + return try await repository.enrollmentsStatus() + } + + public func getCourseDates(courseID: String) async throws -> CourseDates { + return try await repository.getCourseDates(courseID: courseID) + } } // Mark - For testing and SwiftUI preview diff --git a/Profile/Profile/Presentation/DatesAndCalendar/CalendarManager.swift b/Profile/Profile/Presentation/DatesAndCalendar/CalendarManager.swift new file mode 100644 index 000000000..da8920ed1 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/CalendarManager.swift @@ -0,0 +1,416 @@ +// +// CalendarManager.swift +// Profile +// +// Created by  Stepanok Ivan on 12.06.2024. +// + +import SwiftUI +import Combine +import EventKit +import Theme +import BranchSDK +import CryptoKit +import Core +import OEXFoundation + +// MARK: - CalendarManager +public class CalendarManager: CalendarManagerProtocol { + let eventStore = EKEventStore() + private let alertOffset = -1 + private var persistence: ProfilePersistenceProtocol + private var interactor: ProfileInteractorProtocol + private var profileStorage: ProfileStorage + + public init( + persistence: ProfilePersistenceProtocol, + interactor: ProfileInteractorProtocol, + profileStorage: ProfileStorage + ) { + self.persistence = persistence + self.interactor = interactor + self.profileStorage = profileStorage + } + + var authorizationStatus: EKAuthorizationStatus { + return EKEventStore.authorizationStatus(for: .event) + } + + var calendarName: String { + profileStorage.calendarSettings?.calendarName + ?? ProfileLocalization.Calendar.courseDates((Bundle.main.applicationName ?? "")) + } + + var colorSelection: DropDownPicker.DownPickerOption? { + .init( + color: DropDownColor( + rawValue: profileStorage.calendarSettings?.colorSelection ?? "" + ) ?? .accent + ) + } + + var calendarSource: EKSource? { + eventStore.refreshSourcesIfNecessary() + + let iCloud = eventStore.sources.first( + where: { $0.sourceType == .calDAV && $0.title.localizedCaseInsensitiveContains("icloud") }) + let local = eventStore.sources.first(where: { $0.sourceType == .local }) + let fallback = eventStore.defaultCalendarForNewEvents?.source + guard let accountSelection = profileStorage.calendarSettings?.accountSelection else { + return iCloud ?? local ?? fallback + } + switch accountSelection { + case ProfileLocalization.Calendar.Dropdown.icloud: + return iCloud ?? local ?? fallback + case ProfileLocalization.Calendar.Dropdown.local: + return fallback ?? local + default: + return iCloud ?? local ?? fallback + } + } + + var calendar: EKCalendar? { + eventStore.calendars(for: .event).first(where: { $0.title == calendarName }) + } + + public func courseStatus(courseID: String) -> SyncStatus { + let states = persistence.getAllCourseStates() + if states.contains(where: { $0.courseID == courseID }) { + return .synced + } else { + return .offline + } + } + + public func createCalendarIfNeeded() { + if eventStore.calendars(for: .event).first(where: { $0.title == calendarName }) == nil { + let calendar = EKCalendar(for: .event, eventStore: eventStore) + calendar.title = calendarName + + if let swiftUIColor = colorSelection?.color { + let uiColor = UIColor(swiftUIColor) + calendar.cgColor = uiColor.cgColor + } else { + calendar.cgColor = Theme.Colors.accentColor.cgColor + } + + calendar.source = calendarSource + do { + try eventStore.saveCalendar(calendar, commit: true) + } catch { + print(">>>> 🥷", error) + } + } + } + + public func isDatesChanged(courseID: String, checksum: String) -> Bool { + guard let oldState = persistence.getCourseState(courseID: courseID) else { return false } + return checksum != oldState.checksum + } + + public func syncCourse(courseID: String, courseName: String, dates: CourseDates) async { + createCalendarIfNeeded() + guard let calendar else { return } + if saveEvents(for: dates.dateBlocks, courseID: courseID, courseName: courseName, calendar: calendar) { + saveCourseDatesChecksum(courseID: courseID, checksum: dates.checksum) + } else { + debugLog("Failed to sync calendar for courseID: \(courseID)") + } + } + + public func removeOutdatedEvents(courseID: String) async { + let events = persistence.getCourseCalendarEvents(for: courseID) + for event in events { + deleteEventFromCalendar(eventIdentifier: event.eventIdentifier) + } + if var state = persistence.getCourseState(courseID: courseID) { + persistence.saveCourseState(state: CourseCalendarState(courseID: state.courseID, checksum: "")) + } + persistence.removeCourseCalendarEvents(for: courseID) + } + + func deleteEventFromCalendar(eventIdentifier: String) { + if let event = self.eventStore.event(withIdentifier: eventIdentifier) { + do { + try self.eventStore.remove(event, span: .thisEvent) + } catch let error { + debugLog("Failed to remove event: \(error)") + } + } + } + + @MainActor + public func requestAccess() async -> Bool { + await withCheckedContinuation { continuation in + eventStore.requestAccess(to: .event) { granted, _ in + if granted { + continuation.resume(returning: true) + } else { + continuation.resume(returning: false) + } + } + } + } + + public func clearAllData(removeCalendar: Bool) { + persistence.deleteAllCourseStatesAndEvents() + if removeCalendar { + removeOldCalendar() + } + profileStorage.firstCalendarUpdate = false + profileStorage.hideInactiveCourses = nil + profileStorage.lastCalendarName = nil + profileStorage.calendarSettings = nil + profileStorage.lastCalendarUpdateDate = nil + } + + private func saveCourseDatesChecksum(courseID: String, checksum: String) { + var states = persistence.getAllCourseStates() + states.append(CourseCalendarState(courseID: courseID, checksum: checksum)) + for state in states { + persistence.saveCourseState(state: state) + } + } + + private func saveEvents( + for dateBlocks: [Date: [CourseDateBlock]], + courseID: String, + courseName: String, + calendar: EKCalendar + ) -> Bool { + let events = generateEvents(for: dateBlocks, courseName: courseName, calendar: calendar) + var saveSuccessful = true + events.forEach { event in + if !eventExists(event, in: calendar) { + do { + try eventStore.save(event, span: .thisEvent) + persistence.saveCourseCalendarEvent( + CourseCalendarEvent(courseID: courseID, eventIdentifier: event.eventIdentifier) + ) + } catch { + saveSuccessful = false + } + } + } + return saveSuccessful + } + + private func eventExists(_ event: EKEvent, in calendar: EKCalendar) -> Bool { + let predicate = eventStore.predicateForEvents( + withStart: event.startDate, + end: event.endDate, + calendars: [calendar] + ) + let existingEvents = eventStore.events(matching: predicate) + + return existingEvents.contains { existingEvent in + existingEvent.title == event.title && + existingEvent.startDate == event.startDate && + existingEvent.endDate == event.endDate && + existingEvent.notes == event.notes + } + } + + public func filterCoursesBySelected(fetchedCourses: [CourseForSync]) async -> [CourseForSync] { + let courseCalendarStates = persistence.getAllCourseStates() + if !courseCalendarStates.isEmpty { + let coursesToDelete = courseCalendarStates.filter { course in + !fetchedCourses.contains { $0.courseID == course.courseID } + } + let inactiveCourses = fetchedCourses.filter { course in + courseCalendarStates.contains { $0.courseID == course.courseID } && !course.recentlyActive + } + + for course in coursesToDelete { + await removeOutdatedEvents(courseID: course.courseID) + } + + for course in inactiveCourses { + await removeOutdatedEvents(courseID: course.courseID) + } + + let newlyActiveCourses = fetchedCourses.filter { fetchedCourse in + courseCalendarStates.contains { $0.courseID == fetchedCourse.courseID } && fetchedCourse.recentlyActive + } + + return fetchedCourses.filter { course in + courseCalendarStates.contains { $0.courseID == course.courseID } && course.recentlyActive + } + } else { + return fetchedCourses + } + } + + private func generateEvents( + for dateBlocks: [Date: [CourseDateBlock]], + courseName: String, + calendar: EKCalendar + ) -> [EKEvent] { + var events: [EKEvent] = [] + dateBlocks.forEach { item in + let blocks = item.value + if blocks.count > 1 { + if let generatedEvent = calendarEvent(for: blocks, courseName: courseName, calendar: calendar) { + events.append(generatedEvent) + } + } else { + if let block = blocks.first { + if let generatedEvent = calendarEvent(for: block, courseName: courseName, calendar: calendar) { + events.append(generatedEvent) + } + } + } + } + return events + } + + private func calendarEvent(for block: CourseDateBlock, courseName: String, calendar: EKCalendar) -> EKEvent? { + guard !block.title.isEmpty else { return nil } + + let title = block.title + let startDate = block.date.addingTimeInterval(Double(alertOffset) * 3600) + let secondAlert = startDate.addingTimeInterval(Double(alertOffset) * 86400) + let endDate = block.date + var notes = "\(calendar.title)\n\n\(block.title)" + + if let link = generateDeeplink(componentBlockID: block.firstComponentBlockID) { + notes += "\n\(link)" + } + + return generateEvent( + title: title, + startDate: startDate, + endDate: endDate, + secondAlert: secondAlert, + notes: notes, + location: courseName, + calendar: calendar + ) + } + + private func calendarEvent(for blocks: [CourseDateBlock], courseName: String, calendar: EKCalendar) -> EKEvent? { + guard let block = blocks.first, !block.title.isEmpty else { return nil } + + let title = block.title + let startDate = block.date.addingTimeInterval(Double(alertOffset) * 3600) + let secondAlert = startDate.addingTimeInterval(Double(alertOffset) * 86400) + let endDate = block.date + let notes = "\(calendar.title)\n\n" + blocks.compactMap { block -> String in + if let link = generateDeeplink(componentBlockID: block.firstComponentBlockID) { + return "\(block.title)\n\(link)" + } else { + return block.title + } + }.joined(separator: "\n\n") + + return generateEvent( + title: title, + startDate: startDate, + endDate: endDate, + secondAlert: secondAlert, + notes: notes, + location: courseName, + calendar: calendar + ) + } + + private func removeCalendar(for courseID: String, calendarName: String, completion: ((Bool) -> Void)? = nil) { + guard let calendar = localCalendar(for: courseID, calendarName: calendarName) else { completion?(true); return } + do { + try eventStore.removeCalendar(calendar, commit: true) + persistence.removeCourseCalendarEvents(for: courseID) + completion?(true) + } catch { + completion?(false) + } + } + + private func localCalendar(for courseID: String, calendarName: String) -> EKCalendar? { + if authorizationStatus != .authorized { return nil } + let calendarName = "\(calendarName) - \(courseID)" + var calendars = eventStore.calendars(for: .event).filter { $0.title == calendarName } + if calendars.isEmpty { + return nil + } else { + let calendar = calendars.removeLast() + calendars.forEach { try? eventStore.removeCalendar($0, commit: true) } + return calendar + } + } + + private func generateDeeplink(componentBlockID: String) -> String? { + guard !componentBlockID.isEmpty else { + return nil + } + let branchUniversalObject = BranchUniversalObject( + canonicalIdentifier: "\(CalendarDeepLinkType.courseComponent.rawValue)/\(componentBlockID)" + ) + let dictionary: NSMutableDictionary = [ + CalendarDeepLinkKeys.screenName.rawValue: CalendarDeepLinkType.courseComponent.rawValue, + CalendarDeepLinkKeys.courseID.rawValue: profileStorage.calendarSettings?.calendarName ?? "", + CalendarDeepLinkKeys.componentID.rawValue: componentBlockID + ] + let metadata = BranchContentMetadata() + metadata.customMetadata = dictionary + branchUniversalObject.contentMetadata = metadata + let properties = BranchLinkProperties() + let shortUrl = branchUniversalObject.getShortUrl(with: properties) + return shortUrl + } + + private func generateEvent( + title: String, + startDate: Date, + endDate: Date, + secondAlert: Date, + notes: String, + location: String, + calendar: EKCalendar + ) -> EKEvent { + let event = EKEvent(eventStore: eventStore) + event.title = title + event.location = location + event.startDate = startDate + event.endDate = endDate + event.calendar = calendar + event.notes = notes + + if startDate > Date() { + let alarm = EKAlarm(absoluteDate: startDate) + event.addAlarm(alarm) + } + + if secondAlert > Date() { + let alarm = EKAlarm(absoluteDate: secondAlert) + event.addAlarm(alarm) + } + return event + } + + public func removeOldCalendar() { + guard let lastCalendarName = profileStorage.lastCalendarName else { return } + if let oldCalendar = eventStore.calendars(for: .event).first(where: { $0.title == lastCalendarName }) { + do { + try eventStore.removeCalendar(oldCalendar, commit: true) + debugLog("Old calendar '\(lastCalendarName)' removed successfully") + } catch { + debugLog("Failed to remove old calendar '\(lastCalendarName)': \(error.localizedDescription)") + } + } else { + debugLog("Old calendar '\(lastCalendarName)' not found") + } + profileStorage.lastCalendarName = nil + } +} + +// MARK: - Enums and Constants + +enum CalendarDeepLinkType: String { + case courseComponent = "course_component" +} + +private enum CalendarDeepLinkKeys: String, RawStringExtractable { + case courseID = "course_id" + case screenName = "screen_name" + case componentID = "component_id" +} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift new file mode 100644 index 000000000..b5d0aa1a2 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/CoursesToSyncView.swift @@ -0,0 +1,164 @@ +// +// CoursesToSyncView.swift +// Profile +// +// Created by  Stepanok Ivan on 15.05.2024. +// + +import SwiftUI +import Theme +import Core + +public struct CoursesToSyncView: View { + + @ObservedObject + private var viewModel: DatesAndCalendarViewModel + + @Environment(\.isHorizontal) private var isHorizontal + + public init(viewModel: DatesAndCalendarViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + GeometryReader { proxy in + ZStack(alignment: .top) { + ThemeAssets.headerBackground.swiftUIImage + .resizable() + .edgesIgnoringSafeArea(.top) + .frame(maxWidth: .infinity, maxHeight: 200) + .accessibilityIdentifier("title_bg_image") + + VStack(alignment: .leading, spacing: 8) { + // MARK: Navigation and Title + NavigationTitle( + title: ProfileLocalization.CoursesToSync.title, + backAction: { + viewModel.router.back() + } + ) + + // MARK: Body + ScrollView { + Group { + Text(ProfileLocalization.CoursesToSync.description) + .font(Theme.Fonts.labelMedium) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.top, 24) + .padding(.horizontal, 24) + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .leading + ) + + ToggleWithDescriptionView( + text: ProfileLocalization.CoursesToSync.hideInactiveCourses, + description: ProfileLocalization.CoursesToSync.hideInactiveCoursesDescription, + toggle: $viewModel.hideInactiveCourses + ) + .padding(.horizontal, 24) + .padding(.vertical, 16) + + SyncSelector(sync: $viewModel.synced) + .padding(.horizontal, 24) + + coursesList + } + .padding(.horizontal, isHorizontal ? 48 : 0) + } + .frameLimit(width: proxy.size.width) + .roundedBackground(Theme.Colors.background) + .ignoresSafeArea(.all, edges: .bottom) + } + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + + if viewModel.showError { + ErrorAlertView(errorMessage: $viewModel.errorMessage) + } + } + .ignoresSafeArea(.all, edges: .horizontal) + } + } + + private var coursesList: some View { + VStack(alignment: .leading, spacing: 24) { + if viewModel.coursesForSync.allSatisfy({ !$0.synced }) && viewModel.synced { + noSyncedCourses + } else { + ForEach( + Array( + viewModel.coursesForSync.filter({ course in + course.synced == viewModel.synced && (!viewModel.hideInactiveCourses || course.recentlyActive) + }) + .sorted { $0.recentlyActive && !$1.recentlyActive } + .enumerated() + ), + id: \.offset + ) { _, course in + HStack { + CheckBoxView( + checked: Binding( + get: { course.synced }, + set: { _ in viewModel.toggleSync(for: course) } + ), + text: course.name, + color: Theme.Colors.textPrimary.opacity(course.recentlyActive ? 1 : 0.8) + ) + + if !course.recentlyActive { + Text(ProfileLocalization.CoursesToSync.inactive) + .font(Theme.Fonts.labelSmall) + .foregroundStyle(Theme.Colors.textPrimary.opacity(0.8)) + } + } + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .leading + ) + } + } + Spacer(minLength: 100) + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + } + + private var noSyncedCourses: some View { + VStack(spacing: 8) { + Spacer() + CoreAssets.learnEmpty.swiftUIImage + .resizable() + .frame(width: 96, height: 96) + .foregroundStyle(Theme.Colors.textSecondaryLight) + Text(ProfileLocalization.Sync.noSynced) + .foregroundStyle(Theme.Colors.textPrimary) + .font(Theme.Fonts.titleMedium) + Text(ProfileLocalization.Sync.noSyncedDescription) + .multilineTextAlignment(.center) + .foregroundStyle(Theme.Colors.textPrimary) + .font(Theme.Fonts.labelMedium) + .frame(width: 245) + } + } +} + +#if DEBUG +struct CoursesToSyncView_Previews: PreviewProvider { + static var previews: some View { + let vm = DatesAndCalendarViewModel( + router: ProfileRouterMock(), + interactor: ProfileInteractor(repository: ProfileRepositoryMock()), + profileStorage: ProfileStorageMock(), + persistence: ProfilePersistenceMock(), + calendarManager: CalendarManagerMock(), + connectivity: Connectivity() + ) + return CoursesToSyncView(viewModel: vm) + .previewDisplayName("Courses to Sync") + .loadFonts() + } +} +#endif diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift new file mode 100644 index 000000000..35ea4a7f7 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarView.swift @@ -0,0 +1,197 @@ +// +// DatesAndCalendarView.swift +// Profile +// +// Created by  Stepanok Ivan on 12.04.2024. +// + +import SwiftUI +import Theme +import Core + +public struct DatesAndCalendarView: View { + + @ObservedObject + private var viewModel: DatesAndCalendarViewModel + + @State private var screenDimmed: Bool = false + + @Environment(\.isHorizontal) private var isHorizontal + + public init(viewModel: DatesAndCalendarViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + GeometryReader { proxy in + ZStack(alignment: .top) { + ThemeAssets.headerBackground.swiftUIImage + .resizable() + .edgesIgnoringSafeArea(.top) + .frame(maxWidth: .infinity, maxHeight: 200) + .accessibilityIdentifier("title_bg_image") + + VStack { + // MARK: Navigation and Title + NavigationTitle( + title: ProfileLocalization.DatesAndCalendar.title, + backAction: { + viewModel.router.back() + } + ) + + // MARK: Body + ScrollView { + Group { + calendarSyncCard + RelativeDatesToggleView(useRelativeDates: $viewModel.profileStorage.useRelativeDates) + } + .padding(.horizontal, isHorizontal ? 48 : 0) + } + .frameLimit(width: proxy.size.width) + .roundedBackground(Theme.Colors.background) + .ignoresSafeArea(.all, edges: .bottom) + } + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + + if screenDimmed { + Color.black.opacity(0.3) + .ignoresSafeArea() + .onTapGesture { + viewModel.openNewCalendarView = false + screenDimmed = false + viewModel.showCalendaAccessDenied = false + viewModel.calendarName = viewModel.oldCalendarName + viewModel.colorSelection = viewModel.oldColorSelection + } + } + + // Error Alert if needed + if viewModel.showError { + ErrorAlertView(errorMessage: $viewModel.errorMessage) + } + + if viewModel.openNewCalendarView { + NewCalendarView( + title: .newCalendar, + viewModel: viewModel, + beginSyncingTapped: { + guard viewModel.isInternetAvaliable else { + viewModel.openNewCalendarView = false + screenDimmed = false + viewModel.calendarName = viewModel.oldCalendarName + viewModel.colorSelection = viewModel.oldColorSelection + return + } + if viewModel.calendarName == "" { + viewModel.calendarName = viewModel.calendarNameHint + } + viewModel.saveCalendarOptions() + viewModel.router.back(animated: false) + viewModel.router.showSyncCalendarOptions() + }, + onCloseTapped: { + viewModel.calendarName = viewModel.oldCalendarName + viewModel.colorSelection = viewModel.oldColorSelection + viewModel.openNewCalendarView = false + screenDimmed = false + } + ) + .transition(.move(edge: .bottom)) + .frame(alignment: .center) + .onAppear { + screenDimmed = true + } + } + + if viewModel.showCalendaAccessDenied { + CalendarDialogView( + type: .calendarAccess, + action: { + viewModel.showCalendaAccessDenied = false + screenDimmed = false + viewModel.openAppSettings() + }, + onCloseTapped: { + viewModel.showCalendaAccessDenied = false + screenDimmed = false + } + ) + .transition(.move(edge: .bottom)) + .frame(alignment: .center) + .onAppear { + screenDimmed = true + } + } + + } + .ignoresSafeArea(.all, edges: .horizontal) + } + } + + // MARK: - Calendar Sync Card + private var calendarSyncCard: some View { + VStack(alignment: .leading) { + Text(ProfileLocalization.CalendarSync.title) + .multilineTextAlignment(.leading) + .padding(.top, 24) + .padding(.horizontal, 24) + .font(Theme.Fonts.bodyMedium) + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .center, spacing: 16) { + CoreAssets.calendarSyncIcon.swiftUIImage + .foregroundStyle(Theme.Colors.textPrimary) + .padding(.bottom, 16) + + Text(ProfileLocalization.CalendarSync.title) + .font(Theme.Fonts.bodyLarge) + .bold() + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("calendar_sync_title") + + Text(ProfileLocalization.CalendarSync.description) + .font(Theme.Fonts.bodyMedium) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("calendar_sync_description") + + StyledButton( + ProfileLocalization.CalendarSync.button, + action: { + Task { + await viewModel.requestCalendarPermission() + } + }, + horizontalPadding: true + ) + .fixedSize() + .accessibilityIdentifier("calendar_sync_button") + } + .frame(minWidth: 0, + maxWidth: .infinity, + alignment: .top) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity, alignment: .topLeading) + .padding(.top, 24) + .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground, strokeColor: .clear) + } + } +} + +#if DEBUG +struct DatesAndCalendarView_Previews: PreviewProvider { + static var previews: some View { + let vm = DatesAndCalendarViewModel( + router: ProfileRouterMock(), + interactor: ProfileInteractor(repository: ProfileRepositoryMock()), + profileStorage: ProfileStorageMock(), + persistence: ProfilePersistenceMock(), + calendarManager: CalendarManagerMock(), + connectivity: Connectivity() + ) + DatesAndCalendarView(viewModel: vm) + .loadFonts() + } +} +#endif diff --git a/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift new file mode 100644 index 000000000..ff6ad8c60 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/DatesAndCalendarViewModel.swift @@ -0,0 +1,435 @@ +// +// DatesAndCalendarViewModel.swift +// Profile +// +// Created by  Stepanok Ivan on 12.04.2024. +// + +import SwiftUI +import Combine +import EventKit +import Theme +import BranchSDK +import CryptoKit +import Core +import OEXFoundation + +// MARK: - DatesAndCalendarViewModel + +public class DatesAndCalendarViewModel: ObservableObject { + @Published var showCalendaAccessDenied: Bool = false + @Published var showDisableCalendarSync: Bool = false + @Published var showError: Bool = false + @Published var openNewCalendarView: Bool = false + + @Published var accountSelection: DropDownPicker.DownPickerOption? = .init( + title: ProfileLocalization.Calendar.Dropdown.icloud + ) + @Published var calendarName: String = "" + @Published var oldCalendarName: String = "" + @Published var colorSelection: DropDownPicker.DownPickerOption? = .init(color: .accent) + @Published var oldColorSelection: DropDownPicker.DownPickerOption? = .init(color: .accent) + + @Published var assignmentStatus: AssignmentStatus = .synced + @Published var courseCalendarSync: Bool = true + @Published var reconnectRequired: Bool = false + @Published var openChangeSyncView: Bool = false + @Published var syncingCoursesCount: Int = 0 + + @Published var coursesForSync = [CourseForSync]() + + private var coursesForSyncBeforeChanges = [CourseForSync]() + + private(set) var coursesForDeleting = [CourseForSync]() + private(set) var coursesForAdding = [CourseForSync]() + + @Published var synced: Bool = true + @Published var hideInactiveCourses: Bool = false + + var errorMessage: String? { + didSet { + DispatchQueue.main.async { + withAnimation { + self.showError = self.errorMessage != nil + } + } + } + } + + private let accounts: [DropDownPicker.DownPickerOption] = [ + .init(title: ProfileLocalization.Calendar.Dropdown.icloud), + .init(title: ProfileLocalization.Calendar.Dropdown.local) + ] + let colors: [DropDownPicker.DownPickerOption] = [ + .init(color: .accent), + .init(color: .red), + .init(color: .orange), + .init(color: .yellow), + .init(color: .green), + .init(color: .blue), + .init(color: .purple), + .init(color: .brown) + ] + + var router: ProfileRouter + private var interactor: ProfileInteractorProtocol + @Published var profileStorage: ProfileStorage + private var persistence: ProfilePersistenceProtocol + private var calendarManager: CalendarManagerProtocol + private var connectivity: ConnectivityProtocol + + private var cancellables = Set() + var calendarNameHint: String + + public init( + router: ProfileRouter, + interactor: ProfileInteractorProtocol, + profileStorage: ProfileStorage, + persistence: ProfilePersistenceProtocol, + calendarManager: CalendarManagerProtocol, + connectivity: ConnectivityProtocol + ) { + self.router = router + self.interactor = interactor + self.profileStorage = profileStorage + self.persistence = persistence + self.calendarManager = calendarManager + self.connectivity = connectivity + self.calendarNameHint = ProfileLocalization.Calendar.courseDates((Bundle.main.applicationName ?? "")) + } + + @MainActor + var isInternetAvaliable: Bool { + let avaliable = connectivity.isInternetAvaliable + if !avaliable { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } + return avaliable + } + + // MARK: - Lifecycle Functions + + func loadCalendarOptions() { + guard let calendarSettings = profileStorage.calendarSettings else { return } + self.colorSelection = colors.first(where: { $0.colorString == calendarSettings.colorSelection }) + self.accountSelection = accounts.first(where: { $0.title == calendarSettings.accountSelection }) + self.oldCalendarName = profileStorage.lastCalendarName ?? calendarName + self.oldColorSelection = colorSelection + if let calendarName = calendarSettings.calendarName { + self.calendarName = calendarName + } + self.courseCalendarSync = calendarSettings.courseCalendarSync + self.hideInactiveCourses = profileStorage.hideInactiveCourses ?? false + + $hideInactiveCourses + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] hide in + guard let self = self else { return } + self.profileStorage.hideInactiveCourses = hide + }) + .store(in: &cancellables) + + $courseCalendarSync + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] sync in + guard let self = self else { return } + if !sync { + Task { + await self.showDisableCalendarSync() + } + } + }) + .store(in: &cancellables) + + updateCoursesCount() + } + + func clearAllData() { + calendarManager.clearAllData(removeCalendar: true) + router.back(animated: false) + courseCalendarSync = true + showDisableCalendarSync = false + openNewCalendarView = false + router.showDatesAndCalendar() + } + + func deleteOrAddNewDatesIfNeeded() async { + if !coursesForDeleting.isEmpty { + await removeDeselectedCoursesFromCalendar() + } + if !coursesForAdding.isEmpty { + await fetchCourses() + } + } + + func saveCalendarOptions() { + if var calendarSettings = profileStorage.calendarSettings { + oldCalendarName = calendarName + oldColorSelection = colorSelection + calendarSettings.calendarName = calendarName + profileStorage.lastCalendarName = calendarName + + if let colorSelection, let colorString = colorSelection.colorString { + calendarSettings.colorSelection = colorString + } + + if let accountSelection = accountSelection?.title { + calendarSettings.accountSelection = accountSelection + } + + calendarSettings.courseCalendarSync = self.courseCalendarSync + profileStorage.calendarSettings = calendarSettings + } else { + if let colorSelection, + let colorString = colorSelection.colorString, + let accountSelection = accountSelection?.title { + profileStorage.calendarSettings = CalendarSettings( + colorSelection: colorString, + calendarName: calendarName, + accountSelection: accountSelection, + courseCalendarSync: self.courseCalendarSync + ) + profileStorage.lastCalendarName = calendarName + } + } + } + + // MARK: - Fetch Courses and Sync + @MainActor + func fetchCourses() async { + guard connectivity.isInternetAvaliable else { return } + assignmentStatus = .loading + guard await calendarManager.requestAccess() else { + await showCalendarAccessDenied() + return + } + calendarManager.createCalendarIfNeeded() + do { + let fetchedCourses = try await interactor.enrollmentsStatus() + self.coursesForSync = fetchedCourses + let courseCalendarStates = persistence.getAllCourseStates() + if profileStorage.firstCalendarUpdate == false && courseCalendarStates.isEmpty { + await syncAllActiveCourses() + } else { + coursesForSync = coursesForSync.map { course in + var updatedCourse = course + updatedCourse.synced = courseCalendarStates.contains { + $0.courseID == course.courseID + } && course.recentlyActive + return updatedCourse + } + + let addingIDs = Set(coursesForAdding.map { $0.courseID }) + + coursesForSync = coursesForSync.map { course in + var updatedCourse = course + if addingIDs.contains(course.courseID) { + updatedCourse.synced = true + } + return updatedCourse + } + + for course in coursesForSync.filter { $0.synced } { + do { + let courseDates = try await interactor.getCourseDates(courseID: course.courseID) + await syncSelectedCourse( + courseID: course.courseID, + courseName: course.name, + courseDates: courseDates, + active: course.recentlyActive + ) + } catch { + assignmentStatus = .failed + } + } + coursesForAdding = [] + profileStorage.firstCalendarUpdate = true + updateCoursesCount() + } + assignmentStatus = .synced + } catch { + self.assignmentStatus = .failed + debugLog("Error fetching courses: \(error)") + } + } + + private func updateCoursesCount() { + syncingCoursesCount = coursesForSync.filter { $0.recentlyActive && $0.synced }.count + } + + @MainActor + private func syncAllActiveCourses() async { + guard profileStorage.firstCalendarUpdate == false else { + coursesForAdding = [] + coursesForSyncBeforeChanges = [] + assignmentStatus = .synced + updateCoursesCount() + return + } + let selectedCourses = await calendarManager.filterCoursesBySelected(fetchedCourses: coursesForSync) + let activeSelectedCourses = selectedCourses.filter { $0.recentlyActive } + assignmentStatus = .loading + for course in activeSelectedCourses { + do { + let courseDates = try await interactor.getCourseDates(courseID: course.courseID) + await syncSelectedCourse( + courseID: course.courseID, + courseName: course.name, + courseDates: courseDates, + active: course.recentlyActive + ) + } catch { + assignmentStatus = .failed + } + } + profileStorage.firstCalendarUpdate = true + coursesForAdding = [] + coursesForSyncBeforeChanges = [] + assignmentStatus = .synced + updateCoursesCount() + } + + private func filterCoursesBySynced() -> [CourseForSync] { + let syncedCourses = coursesForSync.filter { $0.synced && $0.recentlyActive } + return syncedCourses + } + + func deleteOldCalendarIfNeeded() async { + guard let calSettings = profileStorage.calendarSettings else { return } + let courseCalendarStates = persistence.getAllCourseStates() + let courseCountChanges = courseCalendarStates.count != coursesForSync.count + let nameChanged = oldCalendarName != calendarName + let colorChanged = colorSelection != colors.first(where: { $0.colorString == calSettings.colorSelection }) + let accountChanged = accountSelection != accounts.first(where: { $0.title == calSettings.accountSelection }) + + guard nameChanged || colorChanged || accountChanged || courseCountChanges else { return } + + calendarManager.removeOldCalendar() + saveCalendarOptions() + persistence.removeAllCourseCalendarEvents() + await fetchCourses() + } + + private func syncSelectedCourse( + courseID: String, + courseName: String, + courseDates: CourseDates, + active: Bool + ) async { + await MainActor.run { + self.assignmentStatus = .loading + } + + await calendarManager.removeOutdatedEvents(courseID: courseID) + guard active else { + await MainActor.run { + self.assignmentStatus = .synced + } + return + } + + await calendarManager.syncCourse(courseID: courseID, courseName: courseName, dates: courseDates) + if let index = self.coursesForSync.firstIndex(where: { $0.courseID == courseID && $0.recentlyActive }) { + await MainActor.run { + self.coursesForSync[index].synced = true + } + } + await MainActor.run { + self.assignmentStatus = .synced + } + } + + @MainActor + func removeDeselectedCoursesFromCalendar() async { + for course in coursesForDeleting { + await calendarManager.removeOutdatedEvents(courseID: course.courseID) + persistence.removeCourseState(courseID: course.courseID) + persistence.removeCourseCalendarEvents(for: course.courseID) + if let index = self.coursesForSync.firstIndex(where: { $0.courseID == course.courseID }) { + self.coursesForSync[index].synced = false + } + } + updateCoursesCount() + coursesForDeleting = [] + coursesForSyncBeforeChanges = [] + } + + func toggleSync(for course: CourseForSync) { + guard course.recentlyActive else { return } + if coursesForSyncBeforeChanges.isEmpty { + coursesForSyncBeforeChanges = coursesForSync + } + if let index = coursesForSync.firstIndex(where: { $0.courseID == course.courseID }) { + coursesForSync[index].synced.toggle() + updateCoursesForSyncAndDeletion(course: coursesForSync[index]) + } + } + + private func updateCoursesForSyncAndDeletion(course: CourseForSync) { + guard let initialCourse = coursesForSyncBeforeChanges.first(where: { + $0.courseID == course.courseID + }) else { return } + + if course.synced != initialCourse.synced { + if course.synced { + if !coursesForAdding.contains(where: { $0.courseID == course.courseID }) { + coursesForAdding.append(course) + } + if let index = coursesForDeleting.firstIndex(where: { $0.courseID == course.courseID }) { + coursesForDeleting.remove(at: index) + } + } else { + if !coursesForDeleting.contains(where: { $0.courseID == course.courseID }) { + coursesForDeleting.append(course) + } + if let index = coursesForAdding.firstIndex(where: { $0.courseID == course.courseID }) { + coursesForAdding.remove(at: index) + } + } + } else { + if let index = coursesForAdding.firstIndex(where: { $0.courseID == course.courseID }) { + coursesForAdding.remove(at: index) + } + if let index = coursesForDeleting.firstIndex(where: { $0.courseID == course.courseID }) { + coursesForDeleting.remove(at: index) + } + } + } + + // MARK: - Request Calendar Permission + @MainActor + func requestCalendarPermission() async { + if await calendarManager.requestAccess() { + await showNewCalendarSetup() + } else { + await showCalendarAccessDenied() + } + } + + @MainActor + private func showCalendarAccessDenied() async { + withAnimation(.bouncy(duration: 0.3)) { + self.showCalendaAccessDenied = true + } + } + + @MainActor + private func showDisableCalendarSync() async { + withAnimation(.bouncy(duration: 0.3)) { + self.showDisableCalendarSync = true + } + } + + @MainActor + private func showNewCalendarSetup() async { + withAnimation(.bouncy(duration: 0.3)) { + self.openNewCalendarView = true + } + } + + func openAppSettings() { + if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } +} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/AssignmentStatusView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/AssignmentStatusView.swift new file mode 100644 index 000000000..ace87a64c --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/AssignmentStatusView.swift @@ -0,0 +1,99 @@ +// +// AssignmentStatusView.swift +// Profile +// +// Created by  Stepanok Ivan on 15.05.2024. +// + +import SwiftUI +import Core +import Theme + +enum AssignmentStatus { + case synced + case failed + case offline + case loading + + var statusText: String { + switch self { + case .synced: + ProfileLocalization.AssignmentStatus.synced + case .failed: + ProfileLocalization.AssignmentStatus.failed + case .offline: + ProfileLocalization.AssignmentStatus.offline + case .loading: + ProfileLocalization.AssignmentStatus.syncing + } + } + + var image: Image? { + switch self { + case .synced: + CoreAssets.synced.swiftUIImage + case .failed: + CoreAssets.syncFailed.swiftUIImage + case .offline: + CoreAssets.syncOffline.swiftUIImage + case .loading: + nil + } + } +} + +struct AssignmentStatusView: View { + + private let title: String + @Binding private var status: AssignmentStatus + private let calendarColor: Color + + init(title: String, status: Binding, calendarColor: Color) { + self.title = title + self._status = status + self.calendarColor = calendarColor + } + + var body: some View { + ZStack { + HStack { + Circle() + .frame(width: 18, height: 18) + .foregroundStyle(calendarColor) + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(Theme.Fonts.labelLarge) + .foregroundStyle(Theme.Colors.textPrimary) + Text(status.statusText) + .font(Theme.Fonts.labelSmall) + .foregroundStyle(Theme.Colors.textSecondary) + } + .padding(.vertical, 10) + .multilineTextAlignment(.leading) + Spacer() + status.image + if status == .loading { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + } + .frame(height: 52) + .padding(.horizontal, 16) + } + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(Theme.Colors.textInputUnfocusedBackground) + ) + } +} + +#if DEBUG +#Preview { + AssignmentStatusView( + title: "My Assignments", + status: .constant(.loading), + calendarColor: .blue + ) + .loadFonts() +} +#endif diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/CalendarDialogView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/CalendarDialogView.swift new file mode 100644 index 000000000..cd5e89526 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/CalendarDialogView.swift @@ -0,0 +1,187 @@ +// +// CalendarDialogView.swift +// Profile +// +// Created by  Stepanok Ivan on 15.05.2024. +// + +import SwiftUI +import Core +import Theme + +struct CalendarDialogView: View { + + enum CalendarDialogType { + case calendarAccess + case disableCalendarSync(calendarName: String) + + var title: String { + switch self { + case .calendarAccess: + ProfileLocalization.CalendarDialog.calendarAccess + case .disableCalendarSync: + ProfileLocalization.CalendarDialog.disableCalendarSync + } + } + + var description: String { + switch self { + case .calendarAccess: + ProfileLocalization.CalendarDialog.calendarAccessDescription + case .disableCalendarSync(let calendarName): + ProfileLocalization.CalendarDialog.disableCalendarSyncDescription(calendarName) + } + } + } + + @Environment(\.isHorizontal) private var isHorizontal + private var onCloseTapped: (() -> Void) = {} + private var action: (() -> Void) = {} + private let type: CalendarDialogType + private let calendarCircleColor: Color? + private let calendarName: String? + + init( + type: CalendarDialogType, + calendarCircleColor: Color? = nil, + calendarName: String? = nil, + action: @escaping () -> Void, + onCloseTapped: @escaping () -> Void + ) { + self.type = type + self.calendarCircleColor = calendarCircleColor + self.calendarName = calendarName + self.action = action + self.onCloseTapped = onCloseTapped + } + + var body: some View { + ZStack { + Color.clear + .ignoresSafeArea() + if isHorizontal { + ScrollView { + content + .frame(maxWidth: 400) + .padding(24) + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(Theme.Colors.background) + ) + .padding(24) + } + } else { + content + .frame(maxWidth: 400) + .padding(24) + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(Theme.Colors.background) + ) + .padding(24) + } + } + } + + private var content: some View { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .center) { + CoreAssets.warningFilled.swiftUIImage + .resizable() + .frame(width: 24, height: 24) + Text(type.title) + .font(Theme.Fonts.titleLarge) + .bold() + Spacer() + Button(action: { + onCloseTapped() + }, label: { + Image(systemName: "xmark") + .resizable() + .frame(width: 12, height: 12) + }) + } + + if let calendarName, let calendarCircleColor { + HStack { + Circle() + .frame(width: 18, height: 18) + .foregroundStyle(calendarCircleColor) + Text(calendarName) + .strikethrough() + .font(Theme.Fonts.labelLarge) + .foregroundStyle(Theme.Colors.textPrimary) + .multilineTextAlignment(.leading) + Spacer() + } + .padding(16) + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(Theme.Colors.textInputUnfocusedBackground) + ) + } + + Text(type.description) + .font(Theme.Fonts.bodySmall) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.leading) + + VStack(spacing: 16) { + switch type { + case .calendarAccess: + StyledButton( + ProfileLocalization.CalendarDialog.grantCalendarAccess, + action: { + action() + }, + iconImage: CoreAssets.calendarAccess.swiftUIImage, + iconPosition: .right + ) + StyledButton( + ProfileLocalization.CalendarDialog.cancel, + action: { + onCloseTapped() + }, + color: Theme.Colors.background, + textColor: Theme.Colors.accentColor, + borderColor: Theme.Colors.accentColor + ) + case .disableCalendarSync: + StyledButton( + ProfileLocalization.CalendarDialog.disableSyncing, + action: { + action() + }, + color: Theme.Colors.background, + textColor: Theme.Colors.accentColor, + borderColor: Theme.Colors.accentColor + ) + + StyledButton(ProfileLocalization.CalendarDialog.cancel) { + onCloseTapped() + } + } + } + .padding(.top, 16) + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .center + ) + } + .frame(maxWidth: 360) + } +} + +#if DEBUG +#Preview { + CalendarDialogView( + type: .disableCalendarSync(calendarName: "Demo Calendar"), + calendarCircleColor: .red, + calendarName: "Demo Calendar", + action: {}, + onCloseTapped: {} + ) + .loadFonts() +} +#endif diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/DropDownPicker.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/DropDownPicker.swift new file mode 100644 index 000000000..73fe0a39a --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/DropDownPicker.swift @@ -0,0 +1,243 @@ +// +// DropDownPicker.swift +// Profile +// +// Created by  Stepanok Ivan on 08.05.2024. +// + +import SwiftUI +import Core +import Theme + +public enum DropDownPickerState { + case top + case bottom +} + +public enum DropDownColor: String { + case accent + case red + case orange + case yellow + case green + case blue + case purple + case brown + + var title: String { + switch self { + case .accent: + ProfileLocalization.Calendar.DropdownColor.accent + case .red: + ProfileLocalization.Calendar.DropdownColor.red + case .orange: + ProfileLocalization.Calendar.DropdownColor.orange + case .yellow: + ProfileLocalization.Calendar.DropdownColor.yellow + case .green: + ProfileLocalization.Calendar.DropdownColor.green + case .blue: + ProfileLocalization.Calendar.DropdownColor.blue + case .purple: + ProfileLocalization.Calendar.DropdownColor.purple + case .brown: + ProfileLocalization.Calendar.DropdownColor.brown + } + } + + var color: Color { + switch self { + case .accent: + .accentColor + case .red: + .red + case .orange: + .orange + case .yellow: + .yellow + case .green: + .green + case .blue: + .blue + case .purple: + .purple + case .brown: + .brown + } + } +} + +struct DropDownPicker: View { + + struct DownPickerOption: Hashable { + let title: String + let color: Color? + let colorString: String? + + init(title: String) { + self.title = title + self.color = nil + self.colorString = nil + } + + init(color: DropDownColor) { + self.title = color.title + self.color = color.color + self.colorString = color.rawValue + } + + func hash(into hasher: inout Hasher) { + hasher.combine(title) + } + + static func == (lhs: DownPickerOption, rhs: DownPickerOption) -> Bool { + lhs.title == rhs.title + } + } + + @Binding var selection: DownPickerOption? + var state: DropDownPickerState = .bottom + var options: [DownPickerOption] + + @State var showDropdown = false + + @State private var index = 1000.0 + @State var zindex = 1000.0 + + init(selection: Binding, state: DropDownPickerState, options: [DownPickerOption]) { + self._selection = selection + self.state = state + self.options = options + } + + var body: some View { + GeometryReader { + let size = $0.size + VStack(spacing: 0) { + if state == .top && showDropdown { + optionsView() + } + HStack { + if let color = selection?.color { + Circle() + .frame(width: 18, height: 18) + .foregroundStyle(color) + } + Text( + selection == nil + ? ProfileLocalization.DropDownPicker.select + : selection!.title + ) + .foregroundStyle(Theme.Colors.textPrimary) + .font(Theme.Fonts.bodyMedium) + Spacer(minLength: 0) + Image(systemName: state == .top ? "chevron.up" : "chevron.down") + .foregroundColor(Theme.Colors.textPrimary) + .rotationEffect(.degrees((showDropdown ? -180 : 0))) + } + .padding(.horizontal, 15) + .contentShape(.rect) + .onTapGesture { + index += 1 + zindex = index + withAnimation(.bouncy(duration: 0.2)) { + showDropdown.toggle() + } + } + .zIndex(10) + .frame(height: 48) + .background(Theme.Colors.background) + .overlay { + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.textInputStroke, lineWidth: 1) + .padding(1) + } + + if state == .bottom && showDropdown { + optionsView() + .overlay { + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.textInputStroke, lineWidth: 1) + .padding(1) + } + .padding(.top, 4) + } + } + .clipped() + .background(Theme.Colors.background) + .cornerRadius(8) + .frame(height: size.height, alignment: state == .top ? .bottom : .top) + .onTapBackground(enabled: showDropdown, { showDropdown = false }) + } + .frame(height: 48) + .zIndex(zindex) + } + + func optionsView() -> some View { + + func menuHeight() -> Double { + if options.count < 3 { + return Double(options.count * 56) + } else { + return 200.0 + } + } + + return ScrollView { + VStack(spacing: 0) { + ForEach(options, id: \.self) { option in + ZStack { + HStack { + if let color = option.color { + Circle() + .frame(width: 18, height: 18) + .foregroundStyle(color) + } + Text(option.title) + .font(Theme.Fonts.bodyMedium) + .foregroundStyle(Theme.Colors.textPrimary) + Spacer() + } + VStack { + Spacer() + if option != options.last { + Theme.Colors.textInputStroke + .frame(height: 1) + .padding(.top, 8) + .frame(alignment: .bottom) + } + } + } + .foregroundStyle(selection == option ? Color.primary : Color.gray) + .animation(.easeIn(duration: 0.2), value: selection) + .frame(height: 56) + .contentShape(.rect) + .padding(.horizontal, 15) + .onTapGesture { + withAnimation(.easeIn(duration: 0.2)) { + selection = option + showDropdown.toggle() + } + } + } + } + .padding(.top, 4) + }.frame(height: menuHeight()) + .transition(.move(edge: state == .top ? .bottom : .top)) + .zIndex(1) + } +} + +#Preview { + DropDownPicker( + selection: .constant(.init(title: "Selected")), + state: .bottom, + options: [ + .init(title: "One"), + .init( + title: "Two" + ) + ] + ) + .loadFonts() +} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift new file mode 100644 index 000000000..9d8b9dd70 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/NewCalendarView.swift @@ -0,0 +1,178 @@ +// +// NewCalendarView.swift +// Profile +// +// Created by  Stepanok Ivan on 07.05.2024. +// + +import SwiftUI +import Core +import Theme +import Combine + +struct NewCalendarView: View { + + enum Title { + case newCalendar + case changeSyncOptions + + var text: String { + switch self { + case .newCalendar: + ProfileLocalization.Calendar.newCalendar + case .changeSyncOptions: + ProfileLocalization.Calendar.changeSyncOptions + } + } + } + + @ObservedObject + private var viewModel: DatesAndCalendarViewModel + @Environment(\.isHorizontal) private var isHorizontal + private var beginSyncingTapped: (() -> Void) = {} + private var onCloseTapped: (() -> Void) = {} + @State private var calendarName: String = "" + + private let title: Title + + init( + title: Title, + viewModel: DatesAndCalendarViewModel, + beginSyncingTapped: @escaping () -> Void, + onCloseTapped: @escaping () -> Void + ) { + self.title = title + self.viewModel = viewModel + self.beginSyncingTapped = beginSyncingTapped + self.onCloseTapped = onCloseTapped + } + + var body: some View { + ZStack { + Color.clear + .ignoresSafeArea() + if isHorizontal { + ScrollView { + content + + } + } else { + content + } + } + .onAppear { + calendarName = viewModel.calendarName + } + } + + private var content: some View { + VStack(alignment: .leading) { + HStack(alignment: .center) { + Text(title.text) + .font(Theme.Fonts.titleLarge) + .bold() + Spacer() + Button(action: { + onCloseTapped() + }, label: { + Image(systemName: "xmark") + .resizable() + .frame(width: 12, height: 12) + }) + } + .padding(.bottom, 20) + + Text(ProfileLocalization.Calendar.calendarName) + .font(Theme.Fonts.bodySmall).bold() + .padding(.top, 16) + TextField(viewModel.calendarNameHint, text: $calendarName) + .onReceive(Just(calendarName), perform: { _ in + limitText(40) + }) + .font(Theme.Fonts.bodyLarge) + .padding() + .background(Theme.Colors.background) + .cornerRadius(8) + .frame(height: 48) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Theme.Colors.textInputStroke, lineWidth: 1) + .padding(1) + ) + + Text(ProfileLocalization.Calendar.color) + .font(Theme.Fonts.bodySmall).bold() + .padding(.top, 16) + DropDownPicker(selection: $viewModel.colorSelection, state: .bottom, options: viewModel.colors) + + Text(ProfileLocalization.Calendar.upcomingAssignments) + .font(Theme.Fonts.bodySmall) + .foregroundColor(Theme.Colors.textPrimary) + .padding(.vertical, 13) + .multilineTextAlignment(.center) + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .center + ) + .frame(height: 65) + + VStack(spacing: 16) { + StyledButton( + ProfileLocalization.Calendar.cancel, + action: { + onCloseTapped() + }, + color: Theme.Colors.background, + textColor: Theme.Colors.accentColor, + borderColor: Theme.Colors.accentColor + ) + + StyledButton(ProfileLocalization.Calendar.beginSyncing) { + viewModel.calendarName = calendarName + beginSyncingTapped() + } + } + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .center + ) + } + .frame(maxWidth: 360) + .padding(24) + .background( + RoundedRectangle(cornerRadius: 8) + .foregroundStyle(Theme.Colors.background) + ) + .padding(24) + } + + func limitText(_ upper: Int) { + if calendarName.count > upper { + calendarName = String(calendarName.prefix(upper)) + } + } +} + +#if DEBUG +#Preview { + NewCalendarView( + title: .changeSyncOptions, + viewModel: DatesAndCalendarViewModel( + router: ProfileRouterMock(), + interactor: ProfileInteractor( + repository: ProfileRepositoryMock() + ), + profileStorage: ProfileStorageMock(), + persistence: ProfilePersistenceMock(), + calendarManager: CalendarManagerMock(), + connectivity: Connectivity() + ), + beginSyncingTapped: { + }, + onCloseTapped: {} + ) + .loadFonts() +} +#endif diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift new file mode 100644 index 000000000..967e52245 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/RelativeDatesToggleView.swift @@ -0,0 +1,42 @@ +// +// RelativeDatesToggleView.swift +// Profile +// +// Created by  Stepanok Ivan on 22.07.2024. +// + +import SwiftUI +import Theme + +struct RelativeDatesToggleView: View { + @Binding var useRelativeDates: Bool + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(ProfileLocalization.Options.title) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textPrimary) + HStack(spacing: 16) { + Toggle("", isOn: $useRelativeDates) + .frame(width: 50) + .tint(Theme.Colors.accentColor) + Text(ProfileLocalization.Options.useRelativeDates) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textPrimary) + } + Text( + useRelativeDates + ? ProfileLocalization.Options.showRelativeDates + : ProfileLocalization.Options.showFullDates + ) + .font(Theme.Fonts.labelMedium) + .foregroundColor(Theme.Colors.textPrimary) + } + .padding(.top, 14) + .padding(.horizontal, 24) + .frame(minWidth: 0, + maxWidth: .infinity, + alignment: .leading) + .accessibilityIdentifier("relative_dates_toggle") + } +} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/SyncSelector.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/SyncSelector.swift new file mode 100644 index 000000000..42ee8a552 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/SyncSelector.swift @@ -0,0 +1,60 @@ +// +// SyncSelector.swift +// Profile +// +// Created by  Stepanok Ivan on 15.05.2024. +// + +import SwiftUI +import Core +import Theme + +struct SyncSelector: View { + @Binding var sync: Bool + + var body: some View { + HStack(spacing: 2) { + Button(action: { + sync = true + }) { + Text(ProfileLocalization.SyncSelector.synced) + .font(Theme.Fonts.bodyMedium) + .frame(maxWidth: .infinity) + .padding() + .background(sync ? Theme.Colors.accentColor : Theme.Colors.background) + .foregroundColor(sync ? Theme.Colors.white : Theme.Colors.accentColor) + .clipShape(RoundedCorners(tl: 8, bl: 8)) + } + .overlay( + RoundedCorners(tl: 8, bl: 8) + .stroke(Theme.Colors.accentColor, lineWidth: 1) + .padding(.vertical, 0.5) + ) + Button(action: { + sync = false + }) { + Text(ProfileLocalization.SyncSelector.notSynced) + .font(Theme.Fonts.bodyMedium) + .frame(maxWidth: .infinity) + .padding() + .background(sync ? Theme.Colors.background : Theme.Colors.accentColor) + .foregroundColor(sync ? Theme.Colors.accentColor : Theme.Colors.white) + .clipShape(RoundedCorners(tr: 8, br: 8)) + } + .overlay( + RoundedCorners(tr: 8, br: 8) + .stroke(Theme.Colors.accentColor, lineWidth: 1) + .padding(.vertical, 0.5) + ) + } + + .frame(height: 42) + } +} + +#if DEBUG +#Preview { + SyncSelector(sync: .constant(true)) + .padding(8) +} +#endif diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Elements/ToggleWithDescriptionView.swift b/Profile/Profile/Presentation/DatesAndCalendar/Elements/ToggleWithDescriptionView.swift new file mode 100644 index 000000000..fed25a7df --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Elements/ToggleWithDescriptionView.swift @@ -0,0 +1,98 @@ +// +// ToggleWithDescriptionView.swift +// Profile +// +// Created by  Stepanok Ivan on 15.05.2024. +// + +import SwiftUI +import Theme +import Core + +struct ToggleWithDescriptionView: View { + + let text: String + let description: String + @Binding var toggle: Bool + @Binding var showAlertIcon: Bool + + init( + text: String, + description: String, + toggle: Binding, + showAlertIcon: Binding = .constant(false) + ) { + self.text = text + self.description = description + self._toggle = toggle + self._showAlertIcon = showAlertIcon + } + + var body: some View { + VStack(alignment: .leading, spacing: 18) { +// HStack(spacing: 12) { + Toggle(isOn: $toggle, label: { + HStack { + Text(text) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(Theme.Colors.textPrimary) + if showAlertIcon { + CoreAssets.warningFilled.swiftUIImage + .resizable() + .frame(width: 24, height: 24) + } + } + }) + .tint(Theme.Colors.accentColor) +// CustomToggle(isOn: $toggle) +// .padding(.leading, 10) +// Text(text) +// .font(Theme.Fonts.bodyLarge) +// .foregroundColor(Theme.Colors.textPrimary) +// if showAlertIcon { +// CoreAssets.warningFilled.swiftUIImage +// .resizable() +// .frame(width: 24, height: 24) +// } +// } + Text(description) + .font(Theme.Fonts.labelMedium) + .foregroundColor(Theme.Colors.textPrimary) + } + .frame(minWidth: 0, + maxWidth: .infinity, + alignment: .leading) + .accessibilityIdentifier("\(text)_toggle") + } +} + +#Preview { + ToggleWithDescriptionView( + text: "Use relative dates", + description: "Show relative dates like “Tomorrow” and “Yesterday”", + toggle: .constant(true), + showAlertIcon: .constant(true) + ) + .loadFonts() +} + +struct CustomToggle: View { + @Binding var isOn: Bool + + var body: some View { + Button(action: { + isOn.toggle() + }) { + RoundedRectangle(cornerRadius: 10) + .fill(isOn ? Theme.Colors.accentColor : Color.gray) + .frame(width: 37, height: 20) + .overlay( + Circle() + .fill(Color.white) + .frame(width: 16, height: 16) + .offset(x: isOn ? 8 : -8) + .animation(.easeInOut(duration: 0.2), value: isOn) + ) + } + } +} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Models/CalendarSettings.swift b/Profile/Profile/Presentation/DatesAndCalendar/Models/CalendarSettings.swift new file mode 100644 index 000000000..b4b63bb4e --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Models/CalendarSettings.swift @@ -0,0 +1,50 @@ +// +// CalendarSettings.swift +// Profile +// +// Created by  Stepanok Ivan on 03.06.2024. +// + +import Foundation + +public struct CalendarSettings: Codable { + public var colorSelection: String + public var calendarName: String? + public var accountSelection: String + public var courseCalendarSync: Bool + + public init( + colorSelection: String, + calendarName: String?, + accountSelection: String, + courseCalendarSync: Bool + ) { + self.colorSelection = colorSelection + self.calendarName = calendarName + self.accountSelection = accountSelection + self.courseCalendarSync = courseCalendarSync + } + + enum CodingKeys: String, CodingKey { + case colorSelection + case calendarName + case accountSelection + case courseCalendarSync + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.colorSelection = try container.decode(String.self, forKey: .colorSelection) + self.calendarName = try container.decode(String.self, forKey: .calendarName) + self.accountSelection = try container.decode(String.self, forKey: .accountSelection) + self.courseCalendarSync = try container.decode(Bool.self, forKey: .courseCalendarSync) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(colorSelection, forKey: .colorSelection) + try container.encode(calendarName, forKey: .calendarName) + try container.encode(accountSelection, forKey: .accountSelection) + try container.encode(courseCalendarSync, forKey: .courseCalendarSync) + } +} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Models/CourseCalendarEvent.swift b/Profile/Profile/Presentation/DatesAndCalendar/Models/CourseCalendarEvent.swift new file mode 100644 index 000000000..3ebe6c737 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Models/CourseCalendarEvent.swift @@ -0,0 +1,18 @@ +// +// CourseCalendarEvent.swift +// Profile +// +// Created by  Stepanok Ivan on 10.06.2024. +// + +import Foundation + +public struct CourseCalendarEvent { + public let courseID: String + public let eventIdentifier: String + + public init(courseID: String, eventIdentifier: String) { + self.courseID = courseID + self.eventIdentifier = eventIdentifier + } +} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/Models/CourseCalendarState.swift b/Profile/Profile/Presentation/DatesAndCalendar/Models/CourseCalendarState.swift new file mode 100644 index 000000000..4bdfa2310 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/Models/CourseCalendarState.swift @@ -0,0 +1,18 @@ +// +// CourseCalendarState.swift +// Profile +// +// Created by  Stepanok Ivan on 03.06.2024. +// + +import Foundation + +public struct CourseCalendarState { + public let courseID: String + public var checksum: String + + public init(courseID: String, checksum: String) { + self.courseID = courseID + self.checksum = checksum + } +} diff --git a/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift new file mode 100644 index 000000000..154c869e3 --- /dev/null +++ b/Profile/Profile/Presentation/DatesAndCalendar/SyncCalendarOptionsView.swift @@ -0,0 +1,278 @@ +// +// SyncCalendarOptionsView.swift +// Profile +// +// Created by  Stepanok Ivan on 15.05.2024. +// + +import SwiftUI +import Theme +import Core + +public struct SyncCalendarOptionsView: View { + + @ObservedObject + private var viewModel: DatesAndCalendarViewModel + + @State private var screenDimmed: Bool = false + + @Environment(\.isHorizontal) private var isHorizontal + + public init(viewModel: DatesAndCalendarViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + GeometryReader { proxy in + ZStack(alignment: .top) { + ThemeAssets.headerBackground.swiftUIImage + .resizable() + .edgesIgnoringSafeArea(.top) + .frame(maxWidth: .infinity, maxHeight: 200) + .accessibilityIdentifier("title_bg_image") + + VStack(spacing: 8) { + // MARK: Navigation and Title + NavigationTitle( + title: ProfileLocalization.DatesAndCalendar.title, + backAction: { + viewModel.router.back() + } + ) + + // MARK: Body + ScrollView { + Group { + if let colorSelectionColor = viewModel.colorSelection?.color { + optionTitle(ProfileLocalization.CalendarSync.title) + .padding(.top, 24) + AssignmentStatusView( + title: viewModel.calendarName, + status: $viewModel.assignmentStatus, + calendarColor: colorSelectionColor + ) + .padding(.horizontal, 24) + } + ToggleWithDescriptionView( + text: ProfileLocalization.CourseCalendarSync.title, + description: viewModel.reconnectRequired + ? ProfileLocalization.CourseCalendarSync.Description.reconnectRequired + : ProfileLocalization.CourseCalendarSync.Description.syncing, + toggle: $viewModel.courseCalendarSync, + showAlertIcon: $viewModel.reconnectRequired + ) + .padding(.vertical, 24) + .padding(.horizontal, 24) + + StyledButton( + viewModel.reconnectRequired + ? ProfileLocalization.CourseCalendarSync.Button.reconnect + : ProfileLocalization.CourseCalendarSync.Button.changeSyncOptions, + action: { + screenDimmed = true + withAnimation(.bouncy(duration: 0.3)) { + if viewModel.reconnectRequired { + viewModel.showCalendaAccessDenied = true + } else { + viewModel.openChangeSyncView = true + } + } + }, + color: viewModel.reconnectRequired + ? Theme.Colors.accentColor + : Theme.Colors.background, + textColor: viewModel.reconnectRequired + ? Theme.Colors.styledButtonText + : Theme.Colors.accentColor, + borderColor: viewModel.reconnectRequired + ? .clear + : Theme.Colors.accentColor + ) + .padding(.horizontal, 24) + if !viewModel.reconnectRequired { + optionTitle(ProfileLocalization.CoursesToSync.title) + .padding(.top, 24) + coursesToSync + .padding(.bottom, 24) + } + RelativeDatesToggleView(useRelativeDates: $viewModel.profileStorage.useRelativeDates) + } + .padding(.horizontal, isHorizontal ? 48 : 0) + .frameLimit(width: proxy.size.width) + } + .roundedBackground(Theme.Colors.background) + .ignoresSafeArea(.all, edges: .bottom) + } + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + + if screenDimmed { + Color.black.opacity(0.3) + .ignoresSafeArea() + .onTapGesture { + viewModel.openChangeSyncView = false + viewModel.showCalendaAccessDenied = false + viewModel.showDisableCalendarSync = false + viewModel.courseCalendarSync = true + screenDimmed = false + viewModel.calendarName = viewModel.oldCalendarName + viewModel.colorSelection = viewModel.oldColorSelection + } + } + + // Error Alert if needed + if viewModel.showError { + ErrorAlertView(errorMessage: $viewModel.errorMessage) + } + + if viewModel.openChangeSyncView { + NewCalendarView( + title: .changeSyncOptions, + viewModel: viewModel, + beginSyncingTapped: { + viewModel.openChangeSyncView = false + screenDimmed = false + + guard viewModel.isInternetAvaliable else { + viewModel.calendarName = viewModel.oldCalendarName + viewModel.colorSelection = viewModel.oldColorSelection + return + } + + Task { + await viewModel.deleteOldCalendarIfNeeded() + } + }, + onCloseTapped: { + viewModel.openChangeSyncView = false + screenDimmed = false + viewModel.calendarName = viewModel.oldCalendarName + viewModel.colorSelection = viewModel.oldColorSelection + } + ) + .transition(.move(edge: .bottom)) + .frame(alignment: .center) + } else if viewModel.showCalendaAccessDenied { + CalendarDialogView( + type: .calendarAccess, + action: { + viewModel.showCalendaAccessDenied = false + screenDimmed = false + viewModel.openAppSettings() + }, + onCloseTapped: { + viewModel.showCalendaAccessDenied = false + screenDimmed = false + } + ) + .transition(.move(edge: .bottom)) + .frame(alignment: .center) + .onAppear { + screenDimmed = true + } + } else if viewModel.showDisableCalendarSync { + CalendarDialogView( + type: .disableCalendarSync(calendarName: viewModel.calendarName), + calendarCircleColor: viewModel.colorSelection?.color, + calendarName: viewModel.calendarName, + action: { + viewModel.clearAllData() + }, + onCloseTapped: { + viewModel.showDisableCalendarSync = false + screenDimmed = false + viewModel.courseCalendarSync = true + } + ) + .transition(.move(edge: .bottom)) + .frame(alignment: .center) + } + + } + .ignoresSafeArea(.all, edges: .horizontal) + } + .onFirstAppear { + Task { + await viewModel.fetchCourses() + } + } + .onChange(of: viewModel.courseCalendarSync) { sync in + if !sync { + screenDimmed = true + } + } + .onAppear { + viewModel.loadCalendarOptions() + Task { + await viewModel.deleteOrAddNewDatesIfNeeded() + } + } + } + + // MARK: - Options Title + + private func optionTitle(_ text: String) -> some View { + Text(text) + .multilineTextAlignment(.leading) + .font(Theme.Fonts.labelLarge) + .foregroundStyle(Theme.Colors.textPrimary) + .padding(.horizontal, 24) + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .leading + ) + } + + // MARK: - Courses to Sync + @ViewBuilder + private var coursesToSync: some View { + + VStack(alignment: .leading, spacing: 27) { + Button(action: { + // viewModel.trackProfileVideoSettingsClicked() + guard viewModel.isInternetAvaliable else { return } + viewModel.router.showCoursesToSync() + }, + label: { + HStack { + Text( + String( + format: ProfileLocalization.CoursesToSync.syncingCourses( + viewModel.syncingCoursesCount + ) + ) + ) + .font(Theme.Fonts.titleMedium) + Spacer() + Image(systemName: "chevron.right") + } + }) + .accessibilityIdentifier("courses_to_sync_cell") + + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(ProfileLocalization.settingsVideo) + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + } +} + +#if DEBUG +struct SyncCalendarOptionsView_Previews: PreviewProvider { + static var previews: some View { + let vm = DatesAndCalendarViewModel( + router: ProfileRouterMock(), + interactor: ProfileInteractor(repository: ProfileRepositoryMock()), + profileStorage: ProfileStorageMock(), + persistence: ProfilePersistenceMock(), + calendarManager: CalendarManagerMock(), + connectivity: Connectivity() + ) + SyncCalendarOptionsView(viewModel: vm) + .loadFonts() + } +} +#endif diff --git a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift index c2f5dc7fb..2a970f2b6 100644 --- a/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift +++ b/Profile/Profile/Presentation/DeleteAccount/DeleteAccountView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Theme public struct DeleteAccountView: View { @@ -99,15 +100,15 @@ public struct DeleteAccountView: View { maxWidth: .infinity, alignment: .topLeading) - // MARK: Comfirmation button + // MARK: Confirmation button if viewModel.isShowProgress { ProgressBar(size: 40, lineWidth: 8) .padding(.top, 20) .padding(.horizontal) - .accessibilityIdentifier("progressbar") + .accessibilityIdentifier("progress_bar") } else { StyledButton( - ProfileLocalization.DeleteAccount.comfirm, + ProfileLocalization.DeleteAccount.confirm, action: { Task { try await viewModel.deleteAccount(password: viewModel.password) diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift index 9e2fe176f..f92e991c3 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileView.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Theme public struct EditProfileView: View { @@ -90,7 +91,7 @@ public struct EditProfileView: View { .padding(.horizontal, 12) .padding(.vertical, 4) .frame(height: 200) - .hideScrollContentBackground() + .scrollContentBackground(.hidden) .background( Theme.Shapes.textInputShape .fill(Theme.Colors.textInputBackground) @@ -123,15 +124,6 @@ public struct EditProfileView: View { } }) - Button(ProfileLocalization.Edit.deleteAccount, action: { - viewModel.trackProfileDeleteAccountClicked() - viewModel.router.showDeleteProfileView() - }) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.alert) - .padding(.top, 44) - .accessibilityIdentifier("delete_account_button") - Spacer(minLength: 84) } .padding(.horizontal, 24) @@ -204,7 +196,7 @@ public struct EditProfileView: View { ProgressBar(size: 40, lineWidth: 8) .padding(.top, 150) .padding(.horizontal) - .accessibilityIdentifier("progressbar") + .accessibilityIdentifier("progress_bar") } } .navigationBarHidden(false) @@ -245,6 +237,9 @@ public struct EditProfileView: View { Theme.Colors.background .ignoresSafeArea() ) + .onFirstAppear { + viewModel.trackScreenEvent() + } } } } @@ -260,7 +255,8 @@ struct EditProfileView_Previews: PreviewProvider { yearOfBirth: 0, country: "Ukraine", shortBiography: "", - isFullProfile: true + isFullProfile: true, + email: "peter@example.org" ) EditProfileView( diff --git a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift index aee56c70c..c877a6ecd 100644 --- a/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift +++ b/Profile/Profile/Presentation/EditProfile/EditProfileViewModel.swift @@ -9,7 +9,7 @@ import Foundation import Core import SwiftUI -// swiftlint:disable file_length type_body_length +// swiftlint:disable type_body_length public struct Changes: Equatable { public var shortBiography: String public var profileType: ProfileType @@ -141,14 +141,17 @@ public class EditProfileViewModel: ObservableObject { func checkChanges() { withAnimation(.easeIn(duration: 0.1)) { - self.isChanged = - [spokenLanguageConfiguration.text.isEmpty ? false : spokenLanguageConfiguration.text != userModel.spokenLanguage, - yearsConfiguration.text.isEmpty ? false : yearsConfiguration.text != String(userModel.yearOfBirth), - countriesConfiguration.text.isEmpty ? false : countriesConfiguration.text != userModel.country, - userModel.shortBiography != profileChanges.shortBiography, - profileChanges.isAvatarChanged, - profileChanges.isAvatarDeleted, - userModel.isFullProfile != profileChanges.profileType.boolValue].contains(where: { $0 == true }) + self.isChanged = [ + spokenLanguageConfiguration.text.isEmpty + ? false + : spokenLanguageConfiguration.text != userModel.spokenLanguage, + yearsConfiguration.text.isEmpty ? false : yearsConfiguration.text != String(userModel.yearOfBirth), + countriesConfiguration.text.isEmpty ? false : countriesConfiguration.text != userModel.country, + userModel.shortBiography != profileChanges.shortBiography, + profileChanges.isAvatarChanged, + profileChanges.isAvatarDeleted, + userModel.isFullProfile != profileChanges.profileType.boolValue + ].contains(where: { $0 == true }) } } @@ -364,5 +367,9 @@ public class EditProfileViewModel: ObservableObject { func trackProfileEditDoneClicked() { analytics.profileEditDoneClicked() } + + func trackScreenEvent() { + analytics.profileScreenEvent(.profileEdit, biValue: .profileEdit) + } } -// swiftlint:enable file_length type_body_length +// swiftlint:enable type_body_length diff --git a/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift b/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift index 802571018..481dd2a15 100644 --- a/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift +++ b/Profile/Profile/Presentation/EditProfile/ProfileBottomSheet.swift @@ -39,7 +39,7 @@ struct ProfileBottomSheet: View { private var removePhoto: () -> Void @Binding private var showingBottomSheet: Bool - @Environment (\.isHorizontal) private var isHorizontal + @Environment(\.isHorizontal) private var isHorizontal private var maxWidth: CGFloat { idiom == .pad || (idiom == .phone && isHorizontal) ? 330 : .infinity diff --git a/Profile/Profile/Presentation/Profile/ProfileView.swift b/Profile/Profile/Presentation/Profile/ProfileView.swift index 03c920b2f..54f0026c7 100644 --- a/Profile/Profile/Presentation/Profile/ProfileView.swift +++ b/Profile/Profile/Presentation/Profile/ProfileView.swift @@ -9,50 +9,35 @@ import SwiftUI import Core import Kingfisher import Theme +import OEXFoundation public struct ProfileView: View { - + @StateObject private var viewModel: ProfileViewModel - @Binding var settingsTapped: Bool - - public init(viewModel: ProfileViewModel, settingsTapped: Binding) { + + public init(viewModel: ProfileViewModel) { self._viewModel = StateObject(wrappedValue: { viewModel }()) - self._settingsTapped = settingsTapped } - + public var body: some View { GeometryReader { proxy in ZStack(alignment: .top) { // MARK: - Page Body - RefreshableScrollViewCompat( - action: { - await viewModel.getMyProfile(withProgress: false) - }, - content: { + ScrollView { content .frameLimit(width: proxy.size.width) } - ) + .refreshable { + Task { + await viewModel.getMyProfile(withProgress: false) + } + + } .accessibilityAction {} .padding(.top, 8) - .onChange(of: settingsTapped, perform: { _ in - let userModel = viewModel.userModel ?? UserProfile() - viewModel.trackProfileEditClicked() - viewModel.router.showEditProfile( - userModel: userModel, - avatar: viewModel.updatedAvatar, - profileDidEdit: { updatedProfile, updatedImage in - if let updatedProfile { - self.viewModel.userModel = updatedProfile - } - if let updatedImage { - self.viewModel.updatedAvatar = updatedImage - } - } - ) - }) .navigationBarHidden(false) .navigationBarBackButtonHidden(false) + .navigationTitle(ProfileLocalization.title) // MARK: - Offline mode SnackBar OfflineSnackBarView( @@ -81,7 +66,7 @@ public struct ProfileView: View { } } } - .onFirstAppear { + .onAppear { Task { await viewModel.getMyProfile() } @@ -97,170 +82,96 @@ public struct ProfileView: View { } } } - + private var progressBar: some View { ProgressBar(size: 40, lineWidth: 8) .padding(.top, 200) .padding(.horizontal) } - + + private var editProfileButton: some View { + StyledButton( + ProfileLocalization.editProfile, + action: { + let userModel = viewModel.userModel ?? UserProfile() + viewModel.trackProfileEditClicked() + viewModel.router.showEditProfile( + userModel: userModel, + avatar: viewModel.updatedAvatar, + profileDidEdit: { updatedProfile, updatedImage in + if let updatedProfile { + self.viewModel.userModel = updatedProfile + } + if let updatedImage { + self.viewModel.updatedAvatar = updatedImage + } + } + ) + }, + color: Theme.Colors.background, + textColor: Theme.Colors.accentColor, + borderColor: Theme.Colors.accentColor + ).padding(.all, 24) + } + private var content: some View { VStack { if viewModel.isShowProgress { ProgressBar(size: 40, lineWidth: 8) .padding(.top, 200) .padding(.horizontal) - .accessibilityIdentifier("progressbar") + .accessibilityIdentifier("progress_bar") } else { - UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar) - .padding(.top, 30) - .accessibilityIdentifier("user_avatar_image") - Text(viewModel.userModel?.name ?? "") - .font(Theme.Fonts.headlineSmall) - .foregroundColor(Theme.Colors.textPrimary) - .padding(.top, 20) - .accessibilityIdentifier("user_name_text") - Text("@\(viewModel.userModel?.username ?? "")") - .font(Theme.Fonts.labelLarge) - .padding(.top, 4) - .foregroundColor(Theme.Colors.textSecondary) - .padding(.bottom, 10) - .accessibilityIdentifier("user_username_text") + HStack(alignment: .center, spacing: 12) { + UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar) + .accessibilityIdentifier("user_avatar_image") + VStack(alignment: .leading, spacing: 4) { + Text(viewModel.userModel?.name ?? "") + .font(Theme.Fonts.headlineSmall) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("user_name_text") + Text("@\(viewModel.userModel?.username ?? "")") + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textSecondary) + .accessibilityIdentifier("user_username_text") + } + Spacer() + }.padding(.all, 24) profileInfo - VStack(alignment: .leading, spacing: 14) { - settings - ProfileSupportInfoView(viewModel: viewModel) - logOutButton - } + editProfileButton Spacer() } } } // MARK: - Profile Info - @ViewBuilder private var profileInfo: some View { - if viewModel.userModel?.yearOfBirth != 0 || viewModel.userModel?.shortBiography != "" { - VStack(alignment: .leading, spacing: 14) { - Text(ProfileLocalization.info) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textSecondary) + if let bio = viewModel.userModel?.shortBiography, bio != "" { + VStack(alignment: .leading, spacing: 6) { + Text(ProfileLocalization.about) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.textPrimary) .accessibilityIdentifier("profile_info_text") - - VStack(alignment: .leading, spacing: 16) { - if viewModel.userModel?.yearOfBirth != 0 { - HStack { - Text(ProfileLocalization.Edit.Fields.yearOfBirth) - .foregroundColor(Theme.Colors.textSecondary) - .accessibilityIdentifier("yob_text") - Text(String(viewModel.userModel?.yearOfBirth ?? 0)) - .foregroundColor(Theme.Colors.textPrimary) - .accessibilityIdentifier("yob_value_text") - } - .font(Theme.Fonts.titleMedium) - } - if let bio = viewModel.userModel?.shortBiography, bio != "" { - HStack(alignment: .top) { - Text(ProfileLocalization.bio + " ") - .foregroundColor(Theme.Colors.textPrimary) - + Text(bio) - } - .accessibilityIdentifier("bio_text") - } - } - .accessibilityElement(children: .ignore) - .accessibilityLabel( - (viewModel.userModel?.yearOfBirth != 0 ? - ProfileLocalization.Edit.Fields.yearOfBirth + String(viewModel.userModel?.yearOfBirth ?? 0) : - "") + - (viewModel.userModel?.shortBiography != nil ? - ProfileLocalization.bio + (viewModel.userModel?.shortBiography ?? "") : - "") - ) - .cardStyle( - bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear - ) - }.padding(.bottom, 16) - } - } - - // MARK: - Settings - - @ViewBuilder - private var settings: some View { - Text(ProfileLocalization.settings) - .padding(.horizontal, 24) - .font(Theme.Fonts.labelLarge) - .foregroundColor(Theme.Colors.textSecondary) - .accessibilityIdentifier("settings_text") - - VStack(alignment: .leading, spacing: 27) { - Button(action: { - viewModel.trackProfileVideoSettingsClicked() - viewModel.router.showSettings() - }, label: { - HStack { - Text(ProfileLocalization.settingsVideo) - .font(Theme.Fonts.titleMedium) - Spacer() - Image(systemName: "chevron.right") - } - }) - .accessibilityIdentifier("video_settings_button") - - } - .accessibilityElement(children: .ignore) - .accessibilityLabel(ProfileLocalization.settingsVideo) - .cardStyle( - bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear - ) - } - - // MARK: - Log out - - private var logOutButton: some View { - VStack { - Button(action: { - viewModel.trackLogoutClickedClicked() - viewModel.router.presentView( - transitionStyle: .crossDissolve, - animated: true - ) { - AlertView( - alertTitle: ProfileLocalization.LogoutAlert.title, - alertMessage: ProfileLocalization.LogoutAlert.text, - positiveAction: CoreLocalization.Alert.accept, - onCloseTapped: { - viewModel.router.dismiss(animated: true) - }, - okTapped: { - viewModel.router.dismiss(animated: true) - Task { - await viewModel.logOut() - } - }, type: .logOut - ) - } - }, label: { - HStack { - Text(ProfileLocalization.logout) - Spacer() - Image(systemName: "rectangle.portrait.and.arrow.right") - } - }) + Text(bio) + .font(Theme.Fonts.bodyMedium) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("bio_text") + } .accessibilityElement(children: .ignore) - .accessibilityLabel(ProfileLocalization.logout) - .accessibilityIdentifier("logout_button") + .accessibilityLabel( + (viewModel.userModel?.yearOfBirth != 0 ? + ProfileLocalization.Edit.Fields.yearOfBirth + String(viewModel.userModel?.yearOfBirth ?? 0) : + "") + + (viewModel.userModel?.shortBiography != nil ? + ProfileLocalization.bio + (viewModel.userModel?.shortBiography ?? "") : + "") + ) + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) } - .foregroundColor(Theme.Colors.alert) - .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground, - strokeColor: .clear) - .padding(.top, 24) - .padding(.bottom, 60) } } @@ -270,19 +181,18 @@ struct ProfileView_Previews: PreviewProvider { let router = ProfileRouterMock() let vm = ProfileViewModel( interactor: ProfileInteractor.mock, - downloadManager: DownloadManagerMock(), router: router, analytics: ProfileAnalyticsMock(), config: ConfigMock(), connectivity: Connectivity() ) - - ProfileView(viewModel: vm, settingsTapped: .constant(false)) + + ProfileView(viewModel: vm) .preferredColorScheme(.light) .previewDisplayName("DiscoveryView Light") .loadFonts() - - ProfileView(viewModel: vm, settingsTapped: .constant(false)) + + ProfileView(viewModel: vm) .preferredColorScheme(.dark) .previewDisplayName("DiscoveryView Dark") .loadFonts() @@ -291,10 +201,8 @@ struct ProfileView_Previews: PreviewProvider { #endif struct UserAvatar: View { - private var url: URL? @Binding private var image: UIImage? - init(url: String, image: Binding) { if let rightUrl = URL(string: url) { self.url = rightUrl @@ -303,25 +211,21 @@ struct UserAvatar: View { } self._image = image } - var body: some View { ZStack { - Circle() - .foregroundColor(Theme.Colors.avatarStroke) - .frame(width: 104, height: 104) if let image { Image(uiImage: image) .resizable() .scaledToFill() - .frame(width: 100, height: 100) - .cornerRadius(50) + .frame(width: 80, height: 80) + .cornerRadius(40) } else { KFImage(url) .onFailureImage(CoreAssets.noCourseImage.image) .resizable() .scaledToFill() - .frame(width: 100, height: 100) - .cornerRadius(50) + .frame(width: 80, height: 80) + .cornerRadius(40) } } } diff --git a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift index d509224a6..39854471e 100644 --- a/Profile/Profile/Presentation/Profile/ProfileViewModel.swift +++ b/Profile/Profile/Presentation/Profile/ProfileViewModel.swift @@ -22,79 +22,28 @@ public class ProfileViewModel: ObservableObject { } } } - - private var cancellables = Set() - - enum VersionState { - case actual - case updateNeeded - case updateRequired - } - - @Published var versionState: VersionState = .actual - @Published var currentVersion: String = "" - @Published var latestVersion: String = "" let router: ProfileRouter let config: ConfigProtocol let connectivity: ConnectivityProtocol private let interactor: ProfileInteractorProtocol - private let downloadManager: DownloadManagerProtocol private let analytics: ProfileAnalytics public init( interactor: ProfileInteractorProtocol, - downloadManager: DownloadManagerProtocol, router: ProfileRouter, analytics: ProfileAnalytics, config: ConfigProtocol, connectivity: ConnectivityProtocol ) { self.interactor = interactor - self.downloadManager = downloadManager self.router = router self.analytics = analytics self.config = config self.connectivity = connectivity - generateVersionState() - } - - func openAppStore() { - guard let appStoreURL = URL(string: config.appStoreLink) else { return } - UIApplication.shared.open(appStoreURL) - } - - func generateVersionState() { - guard let info = Bundle.main.infoDictionary else { return } - guard let currentVersion = info["CFBundleShortVersionString"] as? String else { return } - self.currentVersion = currentVersion - NotificationCenter.default.publisher(for: .onActualVersionReceived) - .sink { [weak self] notification in - guard let latestVersion = notification.object as? String else { return } - DispatchQueue.main.async { [weak self] in - self?.latestVersion = latestVersion - - if latestVersion != currentVersion { - self?.versionState = .updateNeeded - } - } - }.store(in: &cancellables) } - - func contactSupport() -> URL? { - let osVersion = UIDevice.current.systemVersion - let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" - let deviceModel = UIDevice.current.model - let feedbackDetails = "OS version: \(osVersion)\nApp version: \(appVersion)\nDevice model: \(deviceModel)" - - let recipientAddress = config.feedbackEmail - let emailSubject = "Feedback" - let emailBody = "\n\n\(feedbackDetails)\n".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! - let emailURL = URL(string: "mailto:\(recipientAddress)?subject=\(emailSubject)&body=\(emailBody)") - return emailURL - } - + @MainActor public func getMyProfile(withProgress: Bool = true) async { do { @@ -110,9 +59,7 @@ public class ProfileViewModel: ObservableObject { isShowProgress = false } catch let error { isShowProgress = false - if error.isUpdateRequeiredError { - self.versionState = .updateRequired - } else if error.isInternetError { + if error.isInternetError { errorMessage = CoreLocalization.Error.slowOrNoInternetConnection } else { errorMessage = CoreLocalization.Error.unknownError @@ -120,47 +67,7 @@ public class ProfileViewModel: ObservableObject { } } - @MainActor - func logOut() async { - try? await interactor.logOut() - try? await downloadManager.cancelAllDownloading() - router.showStartupScreen() - analytics.userLogout(force: false) - } - - func trackProfileVideoSettingsClicked() { - analytics.profileVideoSettingsClicked() - } - - func trackEmailSupportClicked() { - analytics.emailSupportClicked() - } - - func trackCookiePolicyClicked() { - analytics.cookiePolicyClicked() - } - - func trackTOSClicked() { - analytics.tosClicked() - } - - func trackFAQClicked() { - analytics.faqClicked() - } - - func trackDataSellClicked() { - analytics.dataSellClicked() - } - - func trackPrivacyPolicyClicked() { - analytics.privacyPolicyClicked() - } - func trackProfileEditClicked() { analytics.profileEditClicked() } - - func trackLogoutClickedClicked() { - analytics.profileEvent(.userLogoutClicked, biValue: .userLogoutClicked) - } } diff --git a/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift b/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift index 1b8c2ae63..3683465ab 100644 --- a/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift +++ b/Profile/Profile/Presentation/Profile/Subviews/ProfileSupportInfoView.swift @@ -20,7 +20,7 @@ struct ProfileSupportInfoView: View { let title: String } - @ObservedObject var viewModel: ProfileViewModel + @ObservedObject var viewModel: SettingsViewModel var body: some View { Text(ProfileLocalization.supportInfo) @@ -28,6 +28,7 @@ struct ProfileSupportInfoView: View { .font(Theme.Fonts.labelLarge) .foregroundColor(Theme.Colors.textSecondary) .accessibilityIdentifier("support_info_text") + .padding(.top, 12) VStack(alignment: .leading, spacing: 24) { viewModel.contactSupport().map(supportInfo) @@ -118,7 +119,8 @@ struct ProfileSupportInfoView: View { WebBrowser( url: viewModel.url.absoluteString, pageTitle: viewModel.title, - showProgress: true + showProgress: true, + connectivity: self.viewModel.connectivity ) } label: { @@ -129,6 +131,7 @@ struct ProfileSupportInfoView: View { .foregroundColor(Theme.Colors.textPrimary) Spacer() Image(systemName: "chevron.right") + .flipsForRightToLeftLayoutDirection(true) } } .simultaneousGesture(TapGesture().onEnded { @@ -186,6 +189,7 @@ struct ProfileSupportInfoView: View { .font(Theme.Fonts.titleMedium) Spacer() Image(systemName: "chevron.right") + .flipsForRightToLeftLayoutDirection(true) } } .foregroundColor(.primary) diff --git a/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift index 38078a834..da7e80fd6 100644 --- a/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift +++ b/Profile/Profile/Presentation/Profile/UserProfile/UserProfileView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Kingfisher import Theme @@ -28,9 +29,7 @@ public struct UserProfileView: View { Theme.Colors.background .ignoresSafeArea() // MARK: - Page Body - RefreshableScrollViewCompat(action: { - await viewModel.getUserProfile(withProgress: false) - }) { + ScrollView { VStack { if viewModel.isShowProgress { ProgressBar(size: 40, lineWidth: 8) @@ -84,6 +83,11 @@ public struct UserProfileView: View { } .frameLimit(width: proxy.size.width) } + .refreshable { + Task { + await viewModel.getUserProfile(withProgress: false) + } + } .padding(.top, 8) .navigationBarHidden(false) .navigationBarBackButtonHidden(false) diff --git a/Profile/Profile/Presentation/ProfileAnalytics.swift b/Profile/Profile/Presentation/ProfileAnalytics.swift index 3713c15ba..2f59ddf3a 100644 --- a/Profile/Profile/Presentation/ProfileAnalytics.swift +++ b/Profile/Profile/Presentation/ProfileAnalytics.swift @@ -7,6 +7,7 @@ import Foundation import Core +import OEXFoundation //sourcery: AutoMockable public protocol ProfileAnalytics { @@ -25,7 +26,8 @@ public protocol ProfileAnalytics { func profileWifiToggle(action: String) func profileUserDeleteAccountClicked() func profileDeleteAccountSuccess(success: Bool) - func profileEvent(_ event: AnalyticsEvent, biValue: EventBIValue) + func profileTrackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) + func profileScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) } #if DEBUG @@ -45,6 +47,7 @@ class ProfileAnalyticsMock: ProfileAnalytics { public func profileWifiToggle(action: String) {} public func profileUserDeleteAccountClicked() {} public func profileDeleteAccountSuccess(success: Bool) {} - public func profileEvent(_ event: AnalyticsEvent, biValue: EventBIValue) {} + public func profileTrackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) {} + public func profileScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) {} } #endif diff --git a/Profile/Profile/Presentation/ProfileRouter.swift b/Profile/Profile/Presentation/ProfileRouter.swift index 8d9539e92..dca085668 100644 --- a/Profile/Profile/Presentation/ProfileRouter.swift +++ b/Profile/Profile/Presentation/ProfileRouter.swift @@ -20,6 +20,16 @@ public protocol ProfileRouter: BaseRouter { func showSettings() + func showVideoSettings() + + func showManageAccount() + + func showDatesAndCalendar() + + func showSyncCalendarOptions() + + func showCoursesToSync() + func showVideoQualityView(viewModel: SettingsViewModel) func showVideoDownloadQualityView( @@ -46,6 +56,16 @@ public class ProfileRouterMock: BaseRouterMock, ProfileRouter { public func showSettings() {} + public func showVideoSettings() {} + + public func showDatesAndCalendar() {} + + public func showSyncCalendarOptions() {} + + public func showCoursesToSync() {} + + public func showManageAccount() {} + public func showVideoQualityView(viewModel: SettingsViewModel) {} public func showVideoDownloadQualityView( diff --git a/Profile/Profile/Presentation/Settings/ManageAccountView.swift b/Profile/Profile/Presentation/Settings/ManageAccountView.swift new file mode 100644 index 000000000..8b791d42b --- /dev/null +++ b/Profile/Profile/Presentation/Settings/ManageAccountView.swift @@ -0,0 +1,216 @@ +// +// ManageAccountView.swift +// Profile +// +// Created by  Stepanok Ivan on 10.04.2024. +// + +import SwiftUI +import Core +import OEXFoundation +import Theme + +public struct ManageAccountView: View { + + @ObservedObject + private var viewModel: ManageAccountViewModel + + @Environment(\.isHorizontal) private var isHorizontal + + public init(viewModel: ManageAccountViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + GeometryReader { proxy in + ZStack(alignment: .top) { + VStack { + ThemeAssets.headerBackground.swiftUIImage + .resizable() + .edgesIgnoringSafeArea(.top) + } + .frame(maxWidth: .infinity, maxHeight: 200) + .accessibilityIdentifier("auth_bg_image") + + // MARK: - Page name + VStack(alignment: .center) { + ZStack { + HStack { + Text(ProfileLocalization.manageAccount) + .titleSettings(color: Theme.Colors.loginNavigationText) + .accessibilityIdentifier("manage_account_text") + } + VStack { + BackNavigationButton( + color: Theme.Colors.loginNavigationText, + action: { + viewModel.router.back() + } + ) + .backViewStyle() + .padding(.leading, isHorizontal ? 48 : 0) + .accessibilityIdentifier("back_button") + + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + } + + // MARK: - Page Body + ScrollView { + VStack(alignment: .leading, spacing: 12) { + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + .accessibilityIdentifier("progress_bar") + } else { + userAvatar + editProfileButton + deleteAccount + } + } + } + .refreshable { + Task { + await viewModel.getMyProfile(withProgress: false) + } + } + .frameLimit(width: proxy.size.width) + .padding(.top, 24) + .padding(.horizontal, isHorizontal ? 24 : 0) + .roundedBackground(Theme.Colors.background) + } + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + .navigationTitle(ProfileLocalization.manageAccount) + + // MARK: - Offline mode SnackBar + OfflineSnackBarView( + connectivity: viewModel.connectivity, + reloadAction: { + await viewModel.getMyProfile(withProgress: false) + } + ) + + // MARK: - Error Alert + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) + } + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } + } + } + } + } + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + .ignoresSafeArea(.all, edges: .horizontal) + .onFirstAppear { + Task { + await viewModel.getMyProfile() + } + } + } + + private var userAvatar: some View { + HStack(alignment: .center, spacing: 12) { + UserAvatar(url: viewModel.userModel?.avatarUrl ?? "", image: $viewModel.updatedAvatar) + .accessibilityIdentifier("user_avatar_image") + VStack(alignment: .leading, spacing: 4) { + Text(viewModel.userModel?.name ?? "") + .font(Theme.Fonts.headlineSmall) + .foregroundColor(Theme.Colors.textPrimary) + .accessibilityIdentifier("user_name_text") + Text("\(viewModel.userModel?.email ?? "")") + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textSecondary) + .accessibilityIdentifier("user_username_text") + } + Spacer() + }.padding(.all, 24) + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .center + ) + } + + private var deleteAccount: some View { + Button(action: { + viewModel.trackProfileDeleteAccountClicked() + viewModel.router.showDeleteProfileView() + }, label: { + HStack { + CoreAssets.deleteAccount.swiftUIImage + Text(ProfileLocalization.Edit.deleteAccount) + } + }) + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .center + ) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.alert) + .padding(.top, 12) + .accessibilityIdentifier("delete_account_button") + } + + private var editProfileButton: some View { + HStack(alignment: .center) { + StyledButton( + ProfileLocalization.editProfile, + action: { + let userModel = viewModel.userModel ?? UserProfile() + viewModel.trackProfileEditClicked() + viewModel.router.showEditProfile( + userModel: userModel, + avatar: viewModel.updatedAvatar, + profileDidEdit: { updatedProfile, updatedImage in + if let updatedProfile { + self.viewModel.userModel = updatedProfile + } + if let updatedImage { + self.viewModel.updatedAvatar = updatedImage + } + } + ) + }, + color: Theme.Colors.background, + textColor: Theme.Colors.accentColor, + borderColor: Theme.Colors.accentColor + ).padding(.horizontal, 24) + } + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .center + ) + } +} + +#if DEBUG +struct ManageAccountView_Previews: PreviewProvider { + static var previews: some View { + let router = ProfileRouterMock() + let vm = ManageAccountViewModel( + router: router, + analytics: ProfileAnalyticsMock(), + config: ConfigMock(), + connectivity: Connectivity(), + interactor: ProfileInteractor.mock + ) + + ManageAccountView(viewModel: vm) + .loadFonts() + } +} +#endif diff --git a/Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift b/Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift new file mode 100644 index 000000000..55014a340 --- /dev/null +++ b/Profile/Profile/Presentation/Settings/ManageAccountViewModel.swift @@ -0,0 +1,76 @@ +// +// ManageAccountViewModel.swift +// Profile +// +// Created by  Stepanok Ivan on 10.04.2024. +// + +import Foundation +import Core +import SwiftUI + +public class ManageAccountViewModel: ObservableObject { + + @Published public var userModel: UserProfile? + @Published public var updatedAvatar: UIImage? + @Published private(set) var isShowProgress = false + @Published var showError: Bool = false + var errorMessage: String? { + didSet { + withAnimation { + showError = errorMessage != nil + } + } + } + + let router: ProfileRouter + let analytics: ProfileAnalytics + let config: ConfigProtocol + let connectivity: ConnectivityProtocol + private let interactor: ProfileInteractorProtocol + + public init( + router: ProfileRouter, + analytics: ProfileAnalytics, + config: ConfigProtocol, + connectivity: ConnectivityProtocol, + interactor: ProfileInteractorProtocol + ) { + self.router = router + self.analytics = analytics + self.config = config + self.connectivity = connectivity + self.interactor = interactor + } + + @MainActor + public func getMyProfile(withProgress: Bool = true) async { + do { + let userModel = interactor.getMyProfileOffline() + if userModel == nil && connectivity.isInternetAvaliable { + isShowProgress = withProgress + } else { + self.userModel = userModel + } + if connectivity.isInternetAvaliable { + self.userModel = try await interactor.getMyProfile() + } + isShowProgress = false + } catch let error { + isShowProgress = false + if error.isInternetError { + errorMessage = CoreLocalization.Error.slowOrNoInternetConnection + } else { + errorMessage = CoreLocalization.Error.unknownError + } + } + } + + func trackProfileDeleteAccountClicked() { + analytics.profileDeleteAccountClicked() + } + + func trackProfileEditClicked() { + analytics.profileEditClicked() + } +} diff --git a/Profile/Profile/Presentation/Settings/SettingsView.swift b/Profile/Profile/Presentation/Settings/SettingsView.swift index 20b6f1e1e..e257cb967 100644 --- a/Profile/Profile/Presentation/Settings/SettingsView.swift +++ b/Profile/Profile/Presentation/Settings/SettingsView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Kingfisher import Theme @@ -15,6 +16,8 @@ public struct SettingsView: View { @ObservedObject private var viewModel: SettingsViewModel + @Environment(\.isHorizontal) private var isHorizontal + public init(viewModel: SettingsViewModel) { self.viewModel = viewModel } @@ -22,79 +25,68 @@ public struct SettingsView: View { public var body: some View { GeometryReader { proxy in ZStack(alignment: .top) { + VStack { + ThemeAssets.headerBackground.swiftUIImage + .resizable() + .edgesIgnoringSafeArea(.top) + } + .frame(maxWidth: .infinity, maxHeight: 50) + .accessibilityIdentifier("auth_bg_image") - // MARK: - Page Body - ScrollView { - VStack(alignment: .leading, spacing: 24) { - if viewModel.isShowProgress { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 200) - .padding(.horizontal) - .accessibilityIdentifier("progressbar") - } else { - // MARK: Wi-fi - HStack { - SettingsCell( - title: ProfileLocalization.Settings.wifiTitle, - description: ProfileLocalization.Settings.wifiDescription - ) - Toggle(isOn: $viewModel.wifiOnly, label: {}) - .toggleStyle(SwitchToggleStyle(tint: Theme.Colors.toggleSwitchColor)) - .frame(width: 50) - .accessibilityIdentifier("download_agreement_switch") - }.foregroundColor(Theme.Colors.textPrimary) - Divider() - - // MARK: Streaming Quality - HStack { - Button(action: { - viewModel.router.showVideoQualityView(viewModel: viewModel) - }, label: { - SettingsCell(title: ProfileLocalization.Settings.videoQualityTitle, - description: viewModel.selectedQuality.settingsDescription()) - }) - .accessibilityIdentifier("video_stream_quality_button") - // Spacer() - Image(systemName: "chevron.right") - .padding(.trailing, 12) - .frame(width: 10) - .accessibilityIdentifier("video_stream_quality_image") - } - Divider() - - // MARK: Download Quality - HStack { - Button { - viewModel.router.showVideoDownloadQualityView( - downloadQuality: viewModel.userSettings.downloadQuality, - didSelect: viewModel.update(downloadQuality:), - analytics: viewModel.analytics - ) - } label: { - SettingsCell( - title: CoreLocalization.Settings.videoDownloadQualityTitle, - description: viewModel.userSettings.downloadQuality.settingsDescription - ) + // MARK: - Page name + VStack(alignment: .center) { + ZStack { + HStack { + Text(ProfileLocalization.settings) + .titleSettings(color: Theme.Colors.loginNavigationText) + .accessibilityIdentifier("register_text") + } + VStack { + BackNavigationButton( + color: Theme.Colors.loginNavigationText, + action: { + viewModel.router.back() } - .accessibilityIdentifier("video_download_quality_button") - // Spacer() - Image(systemName: "chevron.right") - .padding(.trailing, 12) - .frame(width: 10) - .accessibilityIdentifier("video_download_quality_image") + ) + .backViewStyle() + .padding(.leading, isHorizontal ? 48 : 0) + .accessibilityIdentifier("back_button") + + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + } + + // MARK: - Page Body + ScrollView { + VStack(alignment: .leading, spacing: 12) { + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + .accessibilityIdentifier("progress_bar") + } else { + manageAccount + settings + datesAndCalendar + ProfileSupportInfoView(viewModel: viewModel) + logOutButton } - Divider() } + .frame( + minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading + ) + .frameLimit(width: proxy.size.width) + .padding(.top, 24) + .padding(.horizontal, isHorizontal ? 24 : 0) } - .frame( - minWidth: 0, - maxWidth: .infinity, - alignment: .topLeading - ) - .padding(.horizontal, 24) - .frameLimit(width: proxy.size.width) + .roundedBackground(Theme.Colors.background) } - .padding(.top, 8) + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + .navigationTitle(ProfileLocalization.settings) // MARK: - Error Alert if viewModel.showError { @@ -110,14 +102,144 @@ public struct SettingsView: View { } } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(ProfileLocalization.Settings.videoSettingsTitle) - .background( - Theme.Colors.background - .ignoresSafeArea() - ) } + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + .ignoresSafeArea(.all, edges: .horizontal) + } + + // MARK: - Dates & Calendar + + @ViewBuilder + private var datesAndCalendar: some View { + + VStack(alignment: .leading, spacing: 27) { + Button(action: { +// viewModel.trackProfileVideoSettingsClicked() + viewModel.router.showDatesAndCalendar() + }, label: { + HStack { + Text(ProfileLocalization.datesAndCalendar) + .font(Theme.Fonts.titleMedium) + Spacer() + Image(systemName: "chevron.right") + } + }) + .accessibilityIdentifier("dates_and_calendar_cell") + + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(ProfileLocalization.datesAndCalendar) + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + } + + // MARK: - Manage Account + @ViewBuilder + private var manageAccount: some View { + VStack(alignment: .leading, spacing: 27) { + Button(action: { + viewModel.trackProfileVideoSettingsClicked() + viewModel.router.showManageAccount() + }, label: { + HStack { + Text(ProfileLocalization.manageAccount) + .font(Theme.Fonts.titleMedium) + Spacer() + Image(systemName: "chevron.right") + .flipsForRightToLeftLayoutDirection(true) + } + }) + .accessibilityIdentifier("video_settings_button") + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(ProfileLocalization.manageAccount) + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + } + + // MARK: - Settings + + @ViewBuilder + private var settings: some View { + Text(ProfileLocalization.settings) + .padding(.horizontal, 24) + .font(Theme.Fonts.labelLarge) + .foregroundColor(Theme.Colors.textSecondary) + .accessibilityIdentifier("settings_text") + .padding(.top, 12) + + VStack(alignment: .leading, spacing: 27) { + Button(action: { + viewModel.trackProfileVideoSettingsClicked() + viewModel.router.showVideoSettings() + }, label: { + HStack { + Text(ProfileLocalization.settingsVideo) + .font(Theme.Fonts.titleMedium) + Spacer() + Image(systemName: "chevron.right") + .flipsForRightToLeftLayoutDirection(true) + } + }) + .accessibilityIdentifier("video_settings_button") + + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(ProfileLocalization.settingsVideo) + .cardStyle( + bgColor: Theme.Colors.textInputUnfocusedBackground, + strokeColor: .clear + ) + } + + // MARK: - Log out + + private var logOutButton: some View { + VStack { + Button(action: { + viewModel.trackLogoutClickedClicked() + viewModel.router.presentView( + transitionStyle: .crossDissolve, + animated: true + ) { + AlertView( + alertTitle: ProfileLocalization.LogoutAlert.title, + alertMessage: ProfileLocalization.LogoutAlert.text, + positiveAction: CoreLocalization.Alert.accept, + onCloseTapped: { + viewModel.router.dismiss(animated: true) + }, + okTapped: { + viewModel.router.dismiss(animated: true) + Task { + await viewModel.logOut() + } + }, + type: .logOut + ) + } + }, label: { + HStack { + Text(ProfileLocalization.logout) + Spacer() + Image(systemName: "rectangle.portrait.and.arrow.right") + } + }) + .accessibilityElement(children: .ignore) + .accessibilityLabel(ProfileLocalization.logout) + .accessibilityIdentifier("logout_button") + } + .foregroundColor(Theme.Colors.alert) + .cardStyle(bgColor: Theme.Colors.textInputUnfocusedBackground, strokeColor: .clear) + .padding(.top, 24) + .padding(.bottom, 60) } } @@ -127,8 +249,13 @@ struct SettingsView_Previews: PreviewProvider { let router = ProfileRouterMock() let vm = SettingsViewModel( interactor: ProfileInteractor.mock, + downloadManager: DownloadManagerMock(), router: router, - analytics: CoreAnalyticsMock() + analytics: ProfileAnalyticsMock(), + coreAnalytics: CoreAnalyticsMock(), + config: ConfigMock(), + corePersistence: CorePersistenceMock(), + connectivity: Connectivity() ) SettingsView(viewModel: vm) diff --git a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift index 499623a89..98885f15c 100644 --- a/Profile/Profile/Presentation/Settings/SettingsViewModel.swift +++ b/Profile/Profile/Presentation/Settings/SettingsViewModel.swift @@ -8,6 +8,7 @@ import Foundation import Core import SwiftUI +import Combine public class SettingsViewModel: ObservableObject { @@ -40,6 +41,16 @@ public class SettingsViewModel: ObservableObject { ] .enumerated() ) + + enum VersionState { + case actual + case updateNeeded + case updateRequired + } + + @Published var versionState: VersionState = .actual + @Published var currentVersion: String = "" + @Published var latestVersion: String = "" var errorMessage: String? { didSet { @@ -50,26 +61,134 @@ public class SettingsViewModel: ObservableObject { } @Published private(set) var userSettings: UserSettings + + private var cancellables = Set() private let interactor: ProfileInteractorProtocol + private let downloadManager: DownloadManagerProtocol let router: ProfileRouter - let analytics: CoreAnalytics + let analytics: ProfileAnalytics + let coreAnalytics: CoreAnalytics + let config: ConfigProtocol + let corePersistence: CorePersistenceProtocol + let connectivity: ConnectivityProtocol - public init(interactor: ProfileInteractorProtocol, router: ProfileRouter, analytics: CoreAnalytics) { + public init( + interactor: ProfileInteractorProtocol, + downloadManager: DownloadManagerProtocol, + router: ProfileRouter, + analytics: ProfileAnalytics, + coreAnalytics: CoreAnalytics, + config: ConfigProtocol, + corePersistence: CorePersistenceProtocol, + connectivity: ConnectivityProtocol + ) { self.interactor = interactor + self.downloadManager = downloadManager self.router = router self.analytics = analytics + self.coreAnalytics = coreAnalytics + self.config = config + self.corePersistence = corePersistence + self.connectivity = connectivity let userSettings = interactor.getSettings() self.userSettings = userSettings self.wifiOnly = userSettings.wifiOnly self.selectedQuality = userSettings.streamingQuality + generateVersionState() + } + + func generateVersionState() { + guard let info = Bundle.main.infoDictionary else { return } + guard let currentVersion = info["CFBundleShortVersionString"] as? String else { return } + self.currentVersion = currentVersion + NotificationCenter.default.publisher(for: .onActualVersionReceived) + .sink { [weak self] notification in + guard let latestVersion = notification.object as? String else { return } + DispatchQueue.main.async { [weak self] in + self?.latestVersion = latestVersion + + if latestVersion != currentVersion { + self?.versionState = .updateNeeded + } + } + }.store(in: &cancellables) + } + + func contactSupport() -> URL? { + let osVersion = UIDevice.current.systemVersion + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" + let deviceModel = UIDevice.current.model + let feedbackDetails = "OS version: \(osVersion)\nApp version: \(appVersion)\nDevice model: \(deviceModel)" + + let recipientAddress = config.feedbackEmail + let emailSubject = "Feedback" + let emailBody = "\n\n\(feedbackDetails)\n".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + let emailURL = URL(string: "mailto:\(recipientAddress)?subject=\(emailSubject)&body=\(emailBody)") + return emailURL } func update(downloadQuality: DownloadQuality) { self.userSettings.downloadQuality = downloadQuality interactor.saveSettings(userSettings) } + + func openAppStore() { + guard let appStoreURL = URL(string: config.appStoreLink) else { return } + UIApplication.shared.open(appStoreURL) + } + + @MainActor + func logOut() async { + try? await interactor.logOut() + try? await downloadManager.cancelAllDownloading() + corePersistence.deleteAllProgress() + router.showStartupScreen() + analytics.userLogout(force: false) + NotificationCenter.default.post( + name: .userLoggedOut, + object: nil, + userInfo: [Notification.UserInfoKey.isForced: false] + ) + } + + func trackProfileVideoSettingsClicked() { + analytics.profileVideoSettingsClicked() + } + + func trackEmailSupportClicked() { + analytics.emailSupportClicked() + } + + func trackCookiePolicyClicked() { + analytics.cookiePolicyClicked() + } + + func trackTOSClicked() { + analytics.tosClicked() + } + + func trackFAQClicked() { + analytics.faqClicked() + } + + func trackDataSellClicked() { + analytics.dataSellClicked() + } + + func trackPrivacyPolicyClicked() { + analytics.privacyPolicyClicked() + } + + func trackProfileEditClicked() { + analytics.profileEditClicked() + } + + func trackLogoutClickedClicked() { + analytics.profileTrackEvent(.userLogoutClicked, biValue: .userLogoutClicked) + } + } public extension StreamingQuality { diff --git a/Profile/Profile/Presentation/Settings/VideoQualityView.swift b/Profile/Profile/Presentation/Settings/VideoQualityView.swift index b3decab29..4f95e139c 100644 --- a/Profile/Profile/Presentation/Settings/VideoQualityView.swift +++ b/Profile/Profile/Presentation/Settings/VideoQualityView.swift @@ -7,6 +7,7 @@ import SwiftUI import Core +import OEXFoundation import Kingfisher import Theme @@ -14,6 +15,7 @@ public struct VideoQualityView: View { @ObservedObject private var viewModel: SettingsViewModel + @Environment(\.isHorizontal) private var isHorizontal public init(viewModel: SettingsViewModel) { self.viewModel = viewModel @@ -22,72 +24,102 @@ public struct VideoQualityView: View { public var body: some View { GeometryReader { proxy in ZStack(alignment: .top) { - // MARK: - Page Body - ScrollView { - VStack(alignment: .leading, spacing: 24) { - if viewModel.isShowProgress { - ProgressBar(size: 40, lineWidth: 8) - .padding(.top, 200) - .padding(.horizontal) - .accessibilityIdentifier("progressbar") - } else { + VStack { + ThemeAssets.headerBackground.swiftUIImage + .resizable() + .edgesIgnoringSafeArea(.top) + } + .frame(maxWidth: .infinity, maxHeight: 200) + .accessibilityIdentifier("auth_bg_image") + + // MARK: - Page name + VStack(alignment: .center) { + ZStack { + HStack { + Text(ProfileLocalization.Settings.videoQualityTitle) + .titleSettings(color: Theme.Colors.loginNavigationText) + .accessibilityIdentifier("manage_account_text") + } + VStack { + BackNavigationButton( + color: Theme.Colors.loginNavigationText, + action: { + viewModel.router.back() + } + ) + .backViewStyle() + .padding(.leading, isHorizontal ? 48 : 0) + .accessibilityIdentifier("back_button") - ForEach(viewModel.quality, id: \.offset) { _, quality in - Button(action: { - viewModel.analytics.videoQualityChanged( - .videoStreamQualityChanged, - bivalue: .videoStreamQualityChanged, - value: quality.value ?? "", - oldValue: viewModel.selectedQuality.value ?? "" - ) - viewModel.selectedQuality = quality - }, label: { - HStack { - SettingsCell( - title: quality.title(), - description: quality.description() + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + } + // MARK: - Page Body + ScrollView { + VStack(alignment: .leading, spacing: 24) { + if viewModel.isShowProgress { + ProgressBar(size: 40, lineWidth: 8) + .padding(.top, 200) + .padding(.horizontal) + .accessibilityIdentifier("progress_bar") + } else { + ForEach(viewModel.quality, id: \.offset) { _, quality in + Button(action: { + viewModel.coreAnalytics.videoQualityChanged( + .videoStreamQualityChanged, + bivalue: .videoStreamQualityChanged, + value: quality.value ?? "", + oldValue: viewModel.selectedQuality.value ?? "" ) - Spacer() - CoreAssets.checkmark.swiftUIImage - .renderingMode(.template) - .foregroundColor(Theme.Colors.accentXColor) - .opacity(quality == viewModel.selectedQuality ? 1 : 0) - }.foregroundColor(Theme.Colors.textPrimary) - }) - .accessibilityIdentifier("select_quality_button") - Divider() + viewModel.selectedQuality = quality + }, label: { + HStack { + SettingsCell( + title: quality.title(), + description: quality.description() + ) + Spacer() + CoreAssets.checkmark.swiftUIImage + .renderingMode(.template) + .foregroundColor(Theme.Colors.accentXColor) + .opacity(quality == viewModel.selectedQuality ? 1 : 0) + }.foregroundColor(Theme.Colors.textPrimary) + }) + .accessibilityIdentifier("select_quality_button") + Divider() + } } - } - }.frame(minWidth: 0, - maxWidth: .infinity, - alignment: .topLeading) - .padding(.horizontal, 24) - .frameLimit(width: proxy.size.width) - } - .padding(.top, 8) - - // MARK: - Error Alert - if viewModel.showError { - VStack { - Spacer() - SnackBarView(message: viewModel.errorMessage) + }.frameLimit(width: proxy.size.width) + .padding(.horizontal, 24) + .padding(.top, 24) } - .transition(.move(edge: .bottom)) - .onAppear { - doAfter(Theme.Timeout.snackbarMessageLongTimeout) { - viewModel.errorMessage = nil + .roundedBackground(Theme.Colors.background) + + // MARK: - Error Alert + if viewModel.showError { + VStack { + Spacer() + SnackBarView(message: viewModel.errorMessage) + } + .transition(.move(edge: .bottom)) + .onAppear { + doAfter(Theme.Timeout.snackbarMessageLongTimeout) { + viewModel.errorMessage = nil + } } } } } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .navigationTitle(ProfileLocalization.Settings.videoQualityTitle) - .background( - Theme.Colors.background - .ignoresSafeArea() - ) } + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + .navigationTitle(ProfileLocalization.Settings.videoQualityTitle) + .ignoresSafeArea(.all, edges: .horizontal) + .background( + Theme.Colors.background + .ignoresSafeArea() + ) } } @@ -97,8 +129,13 @@ struct VideoQualityView_Previews: PreviewProvider { let router = ProfileRouterMock() let vm = SettingsViewModel( interactor: ProfileInteractor.mock, + downloadManager: DownloadManagerMock(), router: router, - analytics: CoreAnalyticsMock() + analytics: ProfileAnalyticsMock(), + coreAnalytics: CoreAnalyticsMock(), + config: ConfigMock(), + corePersistence: CorePersistenceMock(), + connectivity: Connectivity() ) VideoQualityView(viewModel: vm) diff --git a/Profile/Profile/Presentation/Settings/VideoSettingsView.swift b/Profile/Profile/Presentation/Settings/VideoSettingsView.swift new file mode 100644 index 000000000..561c9cab1 --- /dev/null +++ b/Profile/Profile/Presentation/Settings/VideoSettingsView.swift @@ -0,0 +1,150 @@ +// +// VideoSettingsView.swift +// Profile +// +// Created by  Stepanok Ivan on 09.04.2024. +// + +import SwiftUI +import Core +import Theme + +public struct VideoSettingsView: View { + + @ObservedObject + private var viewModel: SettingsViewModel + @Environment(\.isHorizontal) private var isHorizontal + + public init(viewModel: SettingsViewModel) { + self.viewModel = viewModel + } + + public var body: some View { + GeometryReader { proxy in + ZStack(alignment: .top) { + VStack { + ThemeAssets.headerBackground.swiftUIImage + .resizable() + .edgesIgnoringSafeArea(.top) + } + .frame(maxWidth: .infinity, maxHeight: 200) + .accessibilityIdentifier("auth_bg_image") + + // MARK: - Page name + VStack(alignment: .center) { + ZStack { + HStack { + Text(ProfileLocalization.Settings.videoSettingsTitle) + .titleSettings(color: Theme.Colors.loginNavigationText) + .accessibilityIdentifier("manage_account_text") + } + VStack { + BackNavigationButton( + color: Theme.Colors.loginNavigationText, + action: { + viewModel.router.back() + } + ) + .backViewStyle() + .padding(.leading, isHorizontal ? 48 : 0) + .accessibilityIdentifier("back_button") + + }.frame(minWidth: 0, + maxWidth: .infinity, + alignment: .topLeading) + } + // MARK: - Page Body + ScrollView { + VStack(alignment: .leading, spacing: 24) { + // MARK: Wi-fi + HStack { + SettingsCell( + title: ProfileLocalization.Settings.wifiTitle, + description: ProfileLocalization.Settings.wifiDescription + ) + Toggle(isOn: $viewModel.wifiOnly, label: {}) + .toggleStyle(SwitchToggleStyle(tint: Theme.Colors.toggleSwitchColor)) + .frame(width: 50) + .accessibilityIdentifier("download_agreement_switch") + }.foregroundColor(Theme.Colors.textPrimary) + Divider() + + // MARK: Streaming Quality + HStack { + Button(action: { + viewModel.router.showVideoQualityView(viewModel: viewModel) + }, label: { + SettingsCell(title: ProfileLocalization.Settings.videoQualityTitle, + description: viewModel.selectedQuality.settingsDescription()) + }) + .accessibilityIdentifier("video_stream_quality_button") + Image(systemName: "chevron.right") + .padding(.trailing, 12) + .frame(width: 10) + .accessibilityIdentifier("video_stream_quality_image") + } + Divider() + + // MARK: Download Quality + HStack { + Button { + viewModel.router.showVideoDownloadQualityView( + downloadQuality: viewModel.userSettings.downloadQuality, + didSelect: viewModel.update(downloadQuality:), + analytics: viewModel.coreAnalytics + ) + } label: { + SettingsCell( + title: CoreLocalization.Settings.videoDownloadQualityTitle, + description: viewModel.userSettings.downloadQuality.settingsDescription + ) + } + .accessibilityIdentifier("video_download_quality_button") + Image(systemName: "chevron.right") + .padding(.trailing, 12) + .frame(width: 10) + .accessibilityIdentifier("video_download_quality_image") + } + Divider() + } + .frameLimit(width: proxy.size.width) + .padding(.horizontal, 24) + .padding(.top, 24) + } + .roundedBackground(Theme.Colors.background) + } + } + } + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + .navigationTitle(ProfileLocalization.Settings.videoSettingsTitle) + .ignoresSafeArea(.all, edges: .horizontal) + .background( + Theme.Colors.background + .ignoresSafeArea() + ) + } +} + +#if DEBUG +struct VideoSettingsView_Previews: PreviewProvider { + static var previews: some View { + let router = ProfileRouterMock() + let vm = SettingsViewModel( + interactor: ProfileInteractor.mock, + downloadManager: DownloadManagerMock(), + router: router, + analytics: ProfileAnalyticsMock(), + coreAnalytics: CoreAnalyticsMock(), + config: ConfigMock(), + corePersistence: CorePersistenceMock(), + connectivity: Connectivity() + ) + + VideoSettingsView(viewModel: vm) + .preferredColorScheme(.light) + .previewDisplayName("SettingsView Light") + .loadFonts() + } +} +#endif diff --git a/Profile/Profile/SwiftGen/Strings.swift b/Profile/Profile/SwiftGen/Strings.swift index d1adacf55..3401ceb79 100644 --- a/Profile/Profile/SwiftGen/Strings.swift +++ b/Profile/Profile/SwiftGen/Strings.swift @@ -10,16 +10,20 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces public enum ProfileLocalization { + /// About Me + public static let about = ProfileLocalization.tr("Localizable", "ABOUT", fallback: "About Me") /// Bio: public static let bio = ProfileLocalization.tr("Localizable", "BIO", fallback: "Bio:") /// Contact support public static let contact = ProfileLocalization.tr("Localizable", "CONTACT", fallback: "Contact support") /// Cookie policy public static let cookiePolicy = ProfileLocalization.tr("Localizable", "COOKIE_POLICY", fallback: "Cookie policy") + /// Dates & Calendar + public static let datesAndCalendar = ProfileLocalization.tr("Localizable", "DATES_AND_CALENDAR", fallback: "Dates & Calendar") /// Do not sell my personal information public static let doNotSellInformation = ProfileLocalization.tr("Localizable", "DO_NOT_SELL_INFORMATION", fallback: "Do not sell my personal information") - /// Edit profile - public static let editProfile = ProfileLocalization.tr("Localizable", "EDIT_PROFILE", fallback: "Edit profile") + /// Edit Profile + public static let editProfile = ProfileLocalization.tr("Localizable", "EDIT_PROFILE", fallback: "Edit Profile") /// View FAQ public static let faqTitle = ProfileLocalization.tr("Localizable", "FAQ_TITLE", fallback: "View FAQ") /// full profile @@ -30,6 +34,8 @@ public enum ProfileLocalization { public static let limitedProfile = ProfileLocalization.tr("Localizable", "LIMITED_PROFILE", fallback: "limited profile") /// Log out public static let logout = ProfileLocalization.tr("Localizable", "LOGOUT", fallback: "Log out") + /// Manage Account + public static let manageAccount = ProfileLocalization.tr("Localizable", "MANAGE_ACCOUNT", fallback: "Manage Account") /// Privacy policy public static let privacy = ProfileLocalization.tr("Localizable", "PRIVACY", fallback: "Privacy policy") /// Settings @@ -49,13 +55,131 @@ public enum ProfileLocalization { public static let title = ProfileLocalization.tr("Localizable", "TITLE", fallback: "Profile") /// Year of birth: public static let yearOfBirth = ProfileLocalization.tr("Localizable", "YEAR_OF_BIRTH", fallback: "Year of birth:") + public enum AssignmentStatus { + /// Sync Failed + public static let failed = ProfileLocalization.tr("Localizable", "ASSIGNMENT_STATUS.FAILED", fallback: "Sync Failed") + /// Offline + public static let offline = ProfileLocalization.tr("Localizable", "ASSIGNMENT_STATUS.OFFLINE", fallback: "Offline") + /// Synced + public static let synced = ProfileLocalization.tr("Localizable", "ASSIGNMENT_STATUS.SYNCED", fallback: "Synced") + /// Syncing to calendar... + public static let syncing = ProfileLocalization.tr("Localizable", "ASSIGNMENT_STATUS.SYNCING", fallback: "Syncing to calendar...") + } + public enum Calendar { + /// Account + public static let account = ProfileLocalization.tr("Localizable", "CALENDAR.ACCOUNT", fallback: "Account") + /// Begin Syncing + public static let beginSyncing = ProfileLocalization.tr("Localizable", "CALENDAR.BEGIN_SYNCING", fallback: "Begin Syncing") + /// Calendar Name + public static let calendarName = ProfileLocalization.tr("Localizable", "CALENDAR.CALENDAR_NAME", fallback: "Calendar Name") + /// Cancel + public static let cancel = ProfileLocalization.tr("Localizable", "CALENDAR.CANCEL", fallback: "Cancel") + /// Change Sync Options + public static let changeSyncOptions = ProfileLocalization.tr("Localizable", "CALENDAR.CHANGE_SYNC_OPTIONS", fallback: "Change Sync Options") + /// Color + public static let color = ProfileLocalization.tr("Localizable", "CALENDAR.COLOR", fallback: "Color") + /// %@ Course Dates + public static func courseDates(_ p1: Any) -> String { + return ProfileLocalization.tr("Localizable", "CALENDAR.COURSE_DATES", String(describing: p1), fallback: "%@ Course Dates") + } + /// New Calendar + public static let newCalendar = ProfileLocalization.tr("Localizable", "CALENDAR.NEW_CALENDAR", fallback: "New Calendar") + /// Upcoming assignments for active courses will appear on this calendar + public static let upcomingAssignments = ProfileLocalization.tr("Localizable", "CALENDAR.UPCOMING_ASSIGNMENTS", fallback: "Upcoming assignments for active courses will appear on this calendar") + public enum Dropdown { + /// iCloud + public static let icloud = ProfileLocalization.tr("Localizable", "CALENDAR.DROPDOWN.ICLOUD", fallback: "iCloud") + /// Local + public static let local = ProfileLocalization.tr("Localizable", "CALENDAR.DROPDOWN.LOCAL", fallback: "Local") + } + public enum DropdownColor { + /// Accent + public static let accent = ProfileLocalization.tr("Localizable", "CALENDAR.DROPDOWN_COLOR.ACCENT", fallback: "Accent") + /// Blue + public static let blue = ProfileLocalization.tr("Localizable", "CALENDAR.DROPDOWN_COLOR.BLUE", fallback: "Blue") + /// Brown + public static let brown = ProfileLocalization.tr("Localizable", "CALENDAR.DROPDOWN_COLOR.BROWN", fallback: "Brown") + /// Green + public static let green = ProfileLocalization.tr("Localizable", "CALENDAR.DROPDOWN_COLOR.GREEN", fallback: "Green") + /// Orange + public static let orange = ProfileLocalization.tr("Localizable", "CALENDAR.DROPDOWN_COLOR.ORANGE", fallback: "Orange") + /// Purple + public static let purple = ProfileLocalization.tr("Localizable", "CALENDAR.DROPDOWN_COLOR.PURPLE", fallback: "Purple") + /// Red + public static let red = ProfileLocalization.tr("Localizable", "CALENDAR.DROPDOWN_COLOR.RED", fallback: "Red") + /// Yellow + public static let yellow = ProfileLocalization.tr("Localizable", "CALENDAR.DROPDOWN_COLOR.YELLOW", fallback: "Yellow") + } + } + public enum CalendarDialog { + /// Calendar Access + public static let calendarAccess = ProfileLocalization.tr("Localizable", "CALENDAR_DIALOG.CALENDAR_ACCESS", fallback: "Calendar Access") + /// To show upcoming assignments and course milestones on your calendar, we need permission to access your calendar. + public static let calendarAccessDescription = ProfileLocalization.tr("Localizable", "CALENDAR_DIALOG.CALENDAR_ACCESS_DESCRIPTION", fallback: "To show upcoming assignments and course milestones on your calendar, we need permission to access your calendar.") + /// Cancel + public static let cancel = ProfileLocalization.tr("Localizable", "CALENDAR_DIALOG.CANCEL", fallback: "Cancel") + /// Disable Calendar Sync + public static let disableCalendarSync = ProfileLocalization.tr("Localizable", "CALENDAR_DIALOG.DISABLE_CALENDAR_SYNC", fallback: "Disable Calendar Sync") + /// Disabling calendar sync will delete the calendar “%@”. You can turn calendar sync back on at any time. + public static func disableCalendarSyncDescription(_ p1: Any) -> String { + return ProfileLocalization.tr("Localizable", "CALENDAR_DIALOG.DISABLE_CALENDAR_SYNC_DESCRIPTION", String(describing: p1), fallback: "Disabling calendar sync will delete the calendar “%@”. You can turn calendar sync back on at any time.") + } + /// Disable Syncing + public static let disableSyncing = ProfileLocalization.tr("Localizable", "CALENDAR_DIALOG.DISABLE_SYNCING", fallback: "Disable Syncing") + /// Grant Calendar Access + public static let grantCalendarAccess = ProfileLocalization.tr("Localizable", "CALENDAR_DIALOG.GRANT_CALENDAR_ACCESS", fallback: "Grant Calendar Access") + } + public enum CalendarSync { + /// Set Up Calendar Sync + public static let button = ProfileLocalization.tr("Localizable", "CALENDAR_SYNC.BUTTON", fallback: "Set Up Calendar Sync") + /// Set up calendar sync to show your upcoming assignments and course milestones on your calendar. New assignments and shifted course dates will sync automatically + public static let description = ProfileLocalization.tr("Localizable", "CALENDAR_SYNC.DESCRIPTION", fallback: "Set up calendar sync to show your upcoming assignments and course milestones on your calendar. New assignments and shifted course dates will sync automatically") + /// Calendar Sync + public static let title = ProfileLocalization.tr("Localizable", "CALENDAR_SYNC.TITLE", fallback: "Calendar Sync") + } + public enum CoursesToSync { + /// Disabling sync for a course will remove all events connected to the course from your synced calendar. + public static let description = ProfileLocalization.tr("Localizable", "COURSES_TO_SYNC.DESCRIPTION", fallback: "Disabling sync for a course will remove all events connected to the course from your synced calendar.") + /// Hide Inactive Courses + public static let hideInactiveCourses = ProfileLocalization.tr("Localizable", "COURSES_TO_SYNC.HIDE_INACTIVE_COURSES", fallback: "Hide Inactive Courses") + /// Automatically remove events from courses you haven’t viewed in the last month + public static let hideInactiveCoursesDescription = ProfileLocalization.tr("Localizable", "COURSES_TO_SYNC.HIDE_INACTIVE_COURSES_DESCRIPTION", fallback: "Automatically remove events from courses you haven’t viewed in the last month") + /// Inactive + public static let inactive = ProfileLocalization.tr("Localizable", "COURSES_TO_SYNC.INACTIVE", fallback: "Inactive") + /// Syncing %d Courses + public static func syncingCourses(_ p1: Int) -> String { + return ProfileLocalization.tr("Localizable", "COURSES_TO_SYNC.SYNCING_COURSES", p1, fallback: "Syncing %d Courses") + } + /// Courses to Sync + public static let title = ProfileLocalization.tr("Localizable", "COURSES_TO_SYNC.TITLE", fallback: "Courses to Sync") + } + public enum CourseCalendarSync { + /// Course Calendar Sync + public static let title = ProfileLocalization.tr("Localizable", "COURSE_CALENDAR_SYNC.TITLE", fallback: "Course Calendar Sync") + public enum Button { + /// Change Sync Options + public static let changeSyncOptions = ProfileLocalization.tr("Localizable", "COURSE_CALENDAR_SYNC.BUTTON.CHANGE_SYNC_OPTIONS", fallback: "Change Sync Options") + /// Reconnect Calendar + public static let reconnect = ProfileLocalization.tr("Localizable", "COURSE_CALENDAR_SYNC.BUTTON.RECONNECT", fallback: "Reconnect Calendar") + } + public enum Description { + /// Please reconnect your calendar to resume syncing + public static let reconnectRequired = ProfileLocalization.tr("Localizable", "COURSE_CALENDAR_SYNC.DESCRIPTION.RECONNECT_REQUIRED", fallback: "Please reconnect your calendar to resume syncing") + /// Currently syncing events to your calendar + public static let syncing = ProfileLocalization.tr("Localizable", "COURSE_CALENDAR_SYNC.DESCRIPTION.SYNCING", fallback: "Currently syncing events to your calendar") + } + } + public enum DatesAndCalendar { + /// Dates & Calendar + public static let title = ProfileLocalization.tr("Localizable", "DATES_AND_CALENDAR.TITLE", fallback: "Dates & Calendar") + } public enum DeleteAccount { /// Are you sure you want to public static let areYouSure = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.ARE_YOU_SURE", fallback: "Are you sure you want to ") /// Back to profile public static let backToProfile = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.BACK_TO_PROFILE", fallback: "Back to profile") /// Yes, delete account - public static let comfirm = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.COMFIRM", fallback: "Yes, delete account") + public static let confirm = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.CONFIRM", fallback: "Yes, delete account") /// To confirm this action, please enter your account password. public static let description = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.DESCRIPTION", fallback: "To confirm this action, please enter your account password.") /// The password is incorrect. Please try again. @@ -64,8 +188,8 @@ public enum ProfileLocalization { public static let password = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.PASSWORD", fallback: "Password") /// Enter password public static let passwordDescription = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.PASSWORD_DESCRIPTION", fallback: "Enter password") - /// Delete account - public static let title = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.TITLE", fallback: "Delete account") + /// Delete Account + public static let title = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.TITLE", fallback: "Delete Account") /// delete your account? public static let wantToDelete = ProfileLocalization.tr("Localizable", "DELETE_ACCOUNT.WANT_TO_DELETE", fallback: "delete your account?") } @@ -75,9 +199,13 @@ public enum ProfileLocalization { /// Warning! public static let title = ProfileLocalization.tr("Localizable", "DELETE_ALERT.TITLE", fallback: "Warning!") } + public enum DropDownPicker { + /// Select + public static let select = ProfileLocalization.tr("Localizable", "DROP_DOWN_PICKER.SELECT", fallback: "Select") + } public enum Edit { - /// Delete account - public static let deleteAccount = ProfileLocalization.tr("Localizable", "EDIT.DELETE_ACCOUNT", fallback: "Delete account") + /// Delete Account + public static let deleteAccount = ProfileLocalization.tr("Localizable", "EDIT.DELETE_ACCOUNT", fallback: "Delete Account") /// A limited profile only shares your username and profile photo. public static let limitedProfileDescription = ProfileLocalization.tr("Localizable", "EDIT.LIMITED_PROFILE_DESCRIPTION", fallback: "A limited profile only shares your username and profile photo.") /// You must be over 13 years old to have a profile with full access to information. @@ -110,8 +238,18 @@ public enum ProfileLocalization { public enum LogoutAlert { /// Are you sure you want to log out? public static let text = ProfileLocalization.tr("Localizable", "LOGOUT_ALERT.TEXT", fallback: "Are you sure you want to log out?") - /// Comfirm log out - public static let title = ProfileLocalization.tr("Localizable", "LOGOUT_ALERT.TITLE", fallback: "Comfirm log out") + /// Confirm log out + public static let title = ProfileLocalization.tr("Localizable", "LOGOUT_ALERT.TITLE", fallback: "Confirm log out") + } + public enum Options { + /// Show full dates like “January 1, 2021” + public static let showFullDates = ProfileLocalization.tr("Localizable", "OPTIONS.SHOW_FULL_DATES", fallback: "Show full dates like “January 1, 2021”") + /// Show relative dates like “Tomorrow” and “Yesterday” + public static let showRelativeDates = ProfileLocalization.tr("Localizable", "OPTIONS.SHOW_RELATIVE_DATES", fallback: "Show relative dates like “Tomorrow” and “Yesterday”") + /// Options + public static let title = ProfileLocalization.tr("Localizable", "OPTIONS.TITLE", fallback: "Options") + /// Use relative dates + public static let useRelativeDates = ProfileLocalization.tr("Localizable", "OPTIONS.USE_RELATIVE_DATES", fallback: "Use relative dates") } public enum Settings { /// Lower data usage @@ -147,6 +285,18 @@ public enum ProfileLocalization { /// Wi-fi only download public static let wifiTitle = ProfileLocalization.tr("Localizable", "SETTINGS.WIFI_TITLE", fallback: "Wi-fi only download") } + public enum Sync { + /// No Synced Courses + public static let noSynced = ProfileLocalization.tr("Localizable", "SYNC.NO_SYNCED", fallback: "No Synced Courses") + /// No courses are currently being synced to your calendar. + public static let noSyncedDescription = ProfileLocalization.tr("Localizable", "SYNC.NO_SYNCED_DESCRIPTION", fallback: "No courses are currently being synced to your calendar.") + } + public enum SyncSelector { + /// Not Synced + public static let notSynced = ProfileLocalization.tr("Localizable", "SYNC_SELECTOR.NOT_SYNCED", fallback: "Not Synced") + /// To Sync + public static let synced = ProfileLocalization.tr("Localizable", "SYNC_SELECTOR.SYNCED", fallback: "To Sync") + } public enum UnsavedDataAlert { /// Changes you have made will be discarded. public static let text = ProfileLocalization.tr("Localizable", "UNSAVED_DATA_ALERT.TEXT", fallback: "Changes you have made will be discarded.") diff --git a/Profile/Profile/en.lproj/Localizable.strings b/Profile/Profile/en.lproj/Localizable.strings index df2a437f8..8a91753ee 100644 --- a/Profile/Profile/en.lproj/Localizable.strings +++ b/Profile/Profile/en.lproj/Localizable.strings @@ -8,11 +8,13 @@ "TITLE" = "Profile"; "INFO" = "Profile info"; -"EDIT_PROFILE" = "Edit profile"; +"ABOUT" = "About Me"; +"EDIT_PROFILE" = "Edit Profile"; "YEAR_OF_BIRTH" = "Year of birth:"; "BIO" = "Bio:"; "SETTINGS" = "Settings"; "SETTINGS_VIDEO" = "Video settings"; +"DATES_AND_CALENDAR" = "Dates & Calendar"; "SUPPORT_INFO" = "Support info"; "CONTACT" = "Contact support"; "TERMS" = "Terms of use"; @@ -20,13 +22,14 @@ "COOKIE_POLICY" = "Cookie policy"; "DO_NOT_SELL_INFORMATION" = "Do not sell my personal information"; "FAQ_TITLE" = "View FAQ"; +"MANAGE_ACCOUNT" = "Manage Account"; "LOGOUT" = "Log out"; "SWITCH_TO" = "Switch to"; "FULL_PROFILE" = "full profile"; "LIMITED_PROFILE" = "limited profile"; -"LOGOUT_ALERT.TITLE" = "Comfirm log out"; +"LOGOUT_ALERT.TITLE" = "Confirm log out"; "LOGOUT_ALERT.TEXT" = "Are you sure you want to log out?"; "DELETE_ALERT.TITLE" = "Warning!"; @@ -37,7 +40,7 @@ "EDIT.TOO_YONG_USER" = "You must be over 13 years old to have a profile with full access to information."; "EDIT.LIMITED_PROFILE_DESCRIPTION" = "A limited profile only shares your username and profile photo."; -"EDIT.DELETE_ACCOUNT" = "Delete account"; +"EDIT.DELETE_ACCOUNT" = "Delete Account"; "EDIT.FIELDS.YEAR_OF_BIRTH" = "Year of birth"; "EDIT.FIELDS.LOCATION" = "Location"; @@ -49,13 +52,13 @@ "EDIT.BOTTOM_SHEET.REMOVE" = "Remove photo"; "EDIT.BOTTOM_SHEET.CANCEL" = "Cancel"; -"DELETE_ACCOUNT.TITLE" = "Delete account"; +"DELETE_ACCOUNT.TITLE" = "Delete Account"; "DELETE_ACCOUNT.ARE_YOU_SURE" = "Are you sure you want to "; "DELETE_ACCOUNT.WANT_TO_DELETE" = "delete your account?"; "DELETE_ACCOUNT.DESCRIPTION" = "To confirm this action, please enter your account password."; "DELETE_ACCOUNT.PASSWORD" = "Password"; "DELETE_ACCOUNT.PASSWORD_DESCRIPTION" = "Enter password"; -"DELETE_ACCOUNT.COMFIRM" = "Yes, delete account"; +"DELETE_ACCOUNT.CONFIRM" = "Yes, delete account"; "DELETE_ACCOUNT.BACK_TO_PROFILE" = "Back to profile"; "DELETE_ACCOUNT.INCORRECT_PASSWORD" = "The password is incorrect. Please try again."; @@ -80,3 +83,73 @@ "SETTINGS.TAP_TO_INSTALL" = "Tap to install required app update"; "ERROR.CANNOT_SEND_EMAIL" = "Cannot send email. It seems your email client is not set up."; + +"CALENDAR.NEW_CALENDAR" = "New Calendar"; +"CALENDAR.CHANGE_SYNC_OPTIONS" = "Change Sync Options"; +"CALENDAR.ACCOUNT" = "Account"; +"CALENDAR.CALENDAR_NAME" = "Calendar Name"; +"CALENDAR.COLOR" = "Color"; +"CALENDAR.UPCOMING_ASSIGNMENTS" = "Upcoming assignments for active courses will appear on this calendar"; +"CALENDAR.CANCEL" = "Cancel"; +"CALENDAR.BEGIN_SYNCING" = "Begin Syncing"; + +"ASSIGNMENT_STATUS.SYNCED" = "Synced"; +"ASSIGNMENT_STATUS.FAILED" = "Sync Failed"; +"ASSIGNMENT_STATUS.OFFLINE" = "Offline"; +"ASSIGNMENT_STATUS.SYNCING" = "Syncing to calendar..."; + +"CALENDAR_DIALOG.CALENDAR_ACCESS" = "Calendar Access"; +"CALENDAR_DIALOG.DISABLE_CALENDAR_SYNC" = "Disable Calendar Sync"; +"CALENDAR_DIALOG.CALENDAR_ACCESS_DESCRIPTION" = "To show upcoming assignments and course milestones on your calendar, we need permission to access your calendar."; +"CALENDAR_DIALOG.DISABLE_CALENDAR_SYNC_DESCRIPTION" = "Disabling calendar sync will delete the calendar “%@”. You can turn calendar sync back on at any time."; +"CALENDAR_DIALOG.GRANT_CALENDAR_ACCESS" = "Grant Calendar Access"; +"CALENDAR_DIALOG.DISABLE_SYNCING" = "Disable Syncing"; +"CALENDAR_DIALOG.CANCEL" = "Cancel"; + +"DATES_AND_CALENDAR.TITLE" = "Dates & Calendar"; +"CALENDAR_SYNC.TITLE" = "Calendar Sync"; +"CALENDAR_SYNC.DESCRIPTION" = "Set up calendar sync to show your upcoming assignments and course milestones on your calendar. New assignments and shifted course dates will sync automatically"; +"CALENDAR_SYNC.BUTTON" = "Set Up Calendar Sync"; +"OPTIONS.TITLE" = "Options"; +"OPTIONS.USE_RELATIVE_DATES" = "Use relative dates"; +"OPTIONS.SHOW_RELATIVE_DATES" = "Show relative dates like “Tomorrow” and “Yesterday”"; +"OPTIONS.SHOW_FULL_DATES" = "Show full dates like “January 1, 2021”"; + +"DATES_AND_CALENDAR.TITLE" = "Dates & Calendar"; +"COURSE_CALENDAR_SYNC.TITLE" = "Course Calendar Sync"; +"COURSE_CALENDAR_SYNC.DESCRIPTION.RECONNECT_REQUIRED" = "Please reconnect your calendar to resume syncing"; +"COURSE_CALENDAR_SYNC.DESCRIPTION.SYNCING" = "Currently syncing events to your calendar"; +"COURSE_CALENDAR_SYNC.BUTTON.RECONNECT" = "Reconnect Calendar"; +"COURSE_CALENDAR_SYNC.BUTTON.CHANGE_SYNC_OPTIONS" = "Change Sync Options"; +"COURSES_TO_SYNC.SYNCING_COURSES" = "Syncing %d Courses"; +"OPTIONS.TITLE" = "Options"; +"OPTIONS.USE_RELATIVE_DATES" = "Use relative dates"; +"OPTIONS.SHOW_RELATIVE_DATES" = "Show relative dates like “Tomorrow” and “Yesterday”"; + +"COURSES_TO_SYNC.TITLE" = "Courses to Sync"; +"COURSES_TO_SYNC.DESCRIPTION" = "Disabling sync for a course will remove all events connected to the course from your synced calendar."; +"COURSES_TO_SYNC.HIDE_INACTIVE_COURSES" = "Hide Inactive Courses"; +"COURSES_TO_SYNC.HIDE_INACTIVE_COURSES_DESCRIPTION" = "Automatically remove events from courses you haven’t viewed in the last month"; +"COURSES_TO_SYNC.INACTIVE" = "Inactive"; + +"CALENDAR.DROPDOWN.ICLOUD" = "iCloud"; +"CALENDAR.DROPDOWN.LOCAL" = "Local"; + +"CALENDAR.DROPDOWN_COLOR.ACCENT" = "Accent"; +"CALENDAR.DROPDOWN_COLOR.RED" = "Red"; +"CALENDAR.DROPDOWN_COLOR.ORANGE" = "Orange"; +"CALENDAR.DROPDOWN_COLOR.YELLOW" = "Yellow"; +"CALENDAR.DROPDOWN_COLOR.GREEN" = "Green"; +"CALENDAR.DROPDOWN_COLOR.BLUE" = "Blue"; +"CALENDAR.DROPDOWN_COLOR.PURPLE" = "Purple"; +"CALENDAR.DROPDOWN_COLOR.BROWN" = "Brown"; + +"CALENDAR.COURSE_DATES" = "%@ Course Dates"; + +"DROP_DOWN_PICKER.SELECT" = "Select"; + +"SYNC_SELECTOR.SYNCED" = "To Sync"; +"SYNC_SELECTOR.NOT_SYNCED" = "Not Synced"; + +"SYNC.NO_SYNCED" = "No Synced Courses"; +"SYNC.NO_SYNCED_DESCRIPTION" = "No courses are currently being synced to your calendar."; diff --git a/Profile/Profile/uk.lproj/Localizable.strings b/Profile/Profile/uk.lproj/Localizable.strings deleted file mode 100644 index f0e4d0503..000000000 --- a/Profile/Profile/uk.lproj/Localizable.strings +++ /dev/null @@ -1,80 +0,0 @@ -/* - Localizable.strings - Profile - - Created by  Stepanok Ivan on 23.09.2022. - -*/ - -"TITLE" = "Профіль"; -"INFO" = "Дані профілю"; -"EDIT_PROFILE" = "Редагування"; -"YEAR_OF_BIRTH" = "Рік народження:"; -"BIO" = "Біо:"; -"SETTINGS" = "Налаштування"; -"SETTINGS_VIDEO" = "Налаштування відео"; -"SUPPORT_INFO" = "Інформація про підтримку"; -"CONTACT" = "Cлужби підтримки"; -"TERMS" = "Умови використання"; -"PRIVACY" = "Політика конфіденційності"; -"COOKIE_POLICY" = "Cookie policy"; -"DO_NOT_SELL_INFORMATION" = "Do not sell my personal information"; -"FAQ_TITLE" = "View FAQ"; -"LOGOUT" = "Вийти"; -"SWITCH_TO" = "Переключити на"; -"FULL_PROFILE" = "повний профіль"; -"LIMITED_PROFILE" = "обмежений профіль"; - -"LOGOUT_ALERT.TITLE" = "Підтвердження виходу"; -"LOGOUT_ALERT.TEXT" = "Ви впевнені, що бажаєте вийти?"; - -"DELETE_ALERT.TITLE" = "Увага!"; -"DELETE_ALERT.TEXT" = "Ви дійсно хочете видалити свій обліковий запис?"; - -"UNSAVED_DATA_ALERT.TITLE" = "Є незбережені дані"; -"UNSAVED_DATA_ALERT.TEXT" = "Ви дійсно хочете вийти без збереження?"; - -"EDIT.TOO_YONG_USER" = "Вам має бути більше 13 років, щоб мати профіль із повним доступом до інформації."; -"EDIT.LIMITED_PROFILE_DESCRIPTION" = "В обмеженому профілі доступні лише ваше ім’я користувача."; -"EDIT.DELETE_ACCOUNT" = "Видалити акаунт"; - -"EDIT.FIELDS.YEAR_OF_BIRTH" = "Рік народження:"; -"EDIT.FIELDS.LOCATION" = "Країна"; -"EDIT.FIELDS.SPOKEN_LANGUGAE" = "Мова спілкування"; -"EDIT.FIELDS.ABOUT_ME" = "Про мене:"; - -"EDIT.BOTTOM_SHEET.TITLE" = "Змінити фото профілю"; -"EDIT.BOTTOM_SHEET.SELECT" = "Обрати із галереї"; -"EDIT.BOTTOM_SHEET.REMOVE" = "Видалити зображення"; -"EDIT.BOTTOM_SHEET.CANCEL" = "Скасувати"; - -"DELETE_ACCOUNT.TITLE" = "Видалення акаунту"; -"DELETE_ACCOUNT.ARE_YOU_SURE" = "Ви впевнені, що хочете "; -"DELETE_ACCOUNT.WANT_TO_DELETE" = "видалити свій обліковий запис?"; -"DELETE_ACCOUNT.DESCRIPTION" = "Для підтвердження цієї дії необхідно ввести пароль свого облікового запису."; -"DELETE_ACCOUNT.PASSWORD" = "Пароль"; -"DELETE_ACCOUNT.PASSWORD_DESCRIPTION" = "Введіть пароль"; -"DELETE_ACCOUNT.COMFIRM" = "Так, видалити акаунт"; -"DELETE_ACCOUNT.BACK_TO_PROFILE" = "Повернутись до профілю"; -"DELETE_ACCOUNT.INCORRECT_PASSWORD" = "Пароль неправильний. Будь ласка спробуйте ще раз."; - -"SETTINGS.VIDEO_SETTINGS_TITLE" = "Налаштування відео"; -"SETTINGS.WIFI_TITLE" = "Тільки Wi-fi"; -"SETTINGS.WIFI_DESCRIPTION" = "Завантажувати відео, лише коли Wi-Fi увімкнено"; -"SETTINGS.VIDEO_QUALITY_TITLE" = "Якість потокового відео"; -"SETTINGS.VIDEO_QUALITY_DESCRIPTION" = "Авто (Рекомендовано)"; - -"SETTINGS.QUALITY_AUTO_TITLE" = "Авто"; -"SETTINGS.QUALITY_AUTO_DESCRIPTION" = "Рекомендовано"; -"SETTINGS.QUALITY_360_TITLE" = "360p"; -"SETTINGS.QUALITY_360_DESCRIPTION" = "економія трафіку"; -"SETTINGS.QUALITY_540_TITLE" = "540p"; -"SETTINGS.QUALITY_720_TITLE" = "720p"; -"SETTINGS.QUALITY_AUTO_DESCRIPTION" = "Найкраща якість"; - -"SETTINGS.VERSION" = "Версія:"; -"SETTINGS.UP_TO_DATE" = "Оновлено"; -"SETTINGS.TAP_TO_UPDATE" = "Клацніть, щоб оновити до версії"; -"SETTINGS.TAP_TO_INSTALL" = "Клацніть, щоб встановити обов'язкове оновлення програми"; - -"ERROR.CANNOT_SEND_EMAIL" = "Cannot send email. It seems your email client is not set up."; diff --git a/Profile/ProfileTests/CalendarManagerTests.swift b/Profile/ProfileTests/CalendarManagerTests.swift new file mode 100644 index 000000000..de245eaea --- /dev/null +++ b/Profile/ProfileTests/CalendarManagerTests.swift @@ -0,0 +1,290 @@ +// +// CalendarManagerTests.swift +// Profile +// +// Created by Ivan Stepanok on 29.10.2024. +// + + +import SwiftyMocky +import XCTest +import EventKit +@testable import Profile +@testable import Core +import Theme +import SwiftUICore + +final class CalendarManagerTests: XCTestCase { + + func testCourseStatusSynced() { + let persistence = ProfilePersistenceProtocolMock() + let interactor = ProfileInteractorProtocolMock() + let profileStorage = ProfileStorageMock() + + let manager = CalendarManager( + persistence: persistence, + interactor: interactor, + profileStorage: profileStorage + ) + + let states = [CourseCalendarState(courseID: "course-1", checksum: "checksum-1")] + Given(persistence, .getAllCourseStates(willReturn: states)) + + let status = manager.courseStatus(courseID: "course-1") + + Verify(persistence, 1, .getAllCourseStates()) + XCTAssertEqual(status, .synced) + } + + func testCourseStatusOffline() { + let persistence = ProfilePersistenceProtocolMock() + let interactor = ProfileInteractorProtocolMock() + let profileStorage = ProfileStorageMock() + + let manager = CalendarManager( + persistence: persistence, + interactor: interactor, + profileStorage: profileStorage + ) + + let states = [CourseCalendarState(courseID: "course-2", checksum: "checksum-2")] + Given(persistence, .getAllCourseStates(willReturn: states)) + + let status = manager.courseStatus(courseID: "course-1") + + Verify(persistence, 1, .getAllCourseStates()) + XCTAssertEqual(status, .offline) + } + + func testIsDatesChanged() { + let persistence = ProfilePersistenceProtocolMock() + let interactor = ProfileInteractorProtocolMock() + let profileStorage = ProfileStorageMock() + + let manager = CalendarManager( + persistence: persistence, + interactor: interactor, + profileStorage: profileStorage + ) + + let state = CourseCalendarState(courseID: "course-1", checksum: "old-checksum") + Given(persistence, .getCourseState(courseID: .value("course-1"), willReturn: state)) + + let changed = manager.isDatesChanged(courseID: "course-1", checksum: "new-checksum") + + Verify(persistence, 1, .getCourseState(courseID: .value("course-1"))) + XCTAssertTrue(changed) + } + + func testIsDatesNotChanged() { + let persistence = ProfilePersistenceProtocolMock() + let interactor = ProfileInteractorProtocolMock() + let profileStorage = ProfileStorageMock() + + let manager = CalendarManager( + persistence: persistence, + interactor: interactor, + profileStorage: profileStorage + ) + + let state = CourseCalendarState(courseID: "course-1", checksum: "same-checksum") + Given(persistence, .getCourseState(courseID: .value("course-1"), willReturn: state)) + + let changed = manager.isDatesChanged(courseID: "course-1", checksum: "same-checksum") + + Verify(persistence, 1, .getCourseState(courseID: .value("course-1"))) + XCTAssertFalse(changed) + } + + func testClearAllData() { + let persistence = ProfilePersistenceProtocolMock() + let interactor = ProfileInteractorProtocolMock() + let profileStorage = ProfileStorageMock() + + // Setup initial values + profileStorage.firstCalendarUpdate = true + profileStorage.hideInactiveCourses = true + profileStorage.lastCalendarName = "Test Calendar" + profileStorage.calendarSettings = CalendarSettings( + colorSelection: "accent", + calendarName: "Test Calendar", + accountSelection: "iCloud", + courseCalendarSync: true + ) + profileStorage.lastCalendarUpdateDate = Date() + + // Verify initial values are set + XCTAssertTrue(profileStorage.firstCalendarUpdate ?? false) + XCTAssertTrue(profileStorage.hideInactiveCourses ?? false) + XCTAssertNotNil(profileStorage.lastCalendarName) + XCTAssertNotNil(profileStorage.calendarSettings) + XCTAssertNotNil(profileStorage.lastCalendarUpdateDate) + + let manager = CalendarManager( + persistence: persistence, + interactor: interactor, + profileStorage: profileStorage + ) + + manager.clearAllData(removeCalendar: true) + + // Verify persistence method was called + Verify(persistence, 1, .deleteAllCourseStatesAndEvents()) + + // Verify all values were cleared + XCTAssertEqual(profileStorage.firstCalendarUpdate, false) + XCTAssertNil(profileStorage.hideInactiveCourses) + XCTAssertNil(profileStorage.lastCalendarName) + XCTAssertNil(profileStorage.calendarSettings) + XCTAssertNil(profileStorage.lastCalendarUpdateDate) + } + + func testFilterCoursesBySelected() async throws { + let persistence = ProfilePersistenceProtocolMock() + let interactor = ProfileInteractorProtocolMock() + let profileStorage = ProfileStorageMock() + + let manager = CalendarManager( + persistence: persistence, + interactor: interactor, + profileStorage: profileStorage + ) + + let states = [ + CourseCalendarState(courseID: "course-1", checksum: "checksum-1"), + CourseCalendarState(courseID: "course-2", checksum: "checksum-2"), + CourseCalendarState(courseID: "course-3", checksum: "checksum-3") + ] + + let fetchedCourses = [ + CourseForSync( + id: UUID(), + courseID: "course-1", + name: "Course 1", + synced: true, + recentlyActive: true + ), + CourseForSync( + id: UUID(), + courseID: "course-2", + name: "Course 2", + synced: true, + recentlyActive: false + ), + CourseForSync( + id: UUID(), + courseID: "course-4", + name: "Course 4", + synced: false, + recentlyActive: true + ) + ] + + // Setup mocks + Given(persistence, .getAllCourseStates(willReturn: states)) + Given(persistence, .getCourseCalendarEvents(for: .any, willReturn: [])) + Given(persistence, .getCourseState(courseID: .any, willReturn: nil)) +// Given(persistence, .removeCourseCalendarEvents(for: .any, willProduce: { _ in })) + + // Execute filtering + let filteredCourses = await manager.filterCoursesBySelected(fetchedCourses: fetchedCourses) + + // Verify calls + Verify(persistence, 1, .getAllCourseStates()) + + // Verify course-3 was removed (exists in states but not in fetched) + Verify(persistence, 1, .getCourseCalendarEvents(for: .value("course-3"))) + Verify(persistence, 1, .removeCourseCalendarEvents(for: .value("course-3"))) + + // Verify course-2 was removed (inactive) + Verify(persistence, 1, .getCourseCalendarEvents(for: .value("course-2"))) + Verify(persistence, 1, .removeCourseCalendarEvents(for: .value("course-2"))) + + // Verify results + XCTAssertEqual(filteredCourses.count, 1) + XCTAssertEqual(filteredCourses.first?.courseID, "course-1") + XCTAssertEqual(filteredCourses.first?.name, "Course 1") + XCTAssertTrue(filteredCourses.first?.synced ?? false) + XCTAssertTrue(filteredCourses.first?.recentlyActive ?? false) + } + + func testFilterCoursesBySelectedEmptyStates() async { + let persistence = ProfilePersistenceProtocolMock() + let interactor = ProfileInteractorProtocolMock() + let profileStorage = ProfileStorageMock() + + let manager = CalendarManager( + persistence: persistence, + interactor: interactor, + profileStorage: profileStorage + ) + + Given(persistence, .getAllCourseStates(willReturn: [])) + + let fetchedCourses = [ + CourseForSync( + id: UUID(), + courseID: "course-1", + name: "Course 1", + synced: true, + recentlyActive: true + ), + CourseForSync( + id: UUID(), + courseID: "course-2", + name: "Course 2", + synced: true, + recentlyActive: false + ) + ] + + let filteredCourses = await manager.filterCoursesBySelected(fetchedCourses: fetchedCourses) + + Verify(persistence, 1, .getAllCourseStates()) + XCTAssertEqual(filteredCourses, fetchedCourses) + } + + func testCalendarNameFromSettings() { + let persistence = ProfilePersistenceProtocolMock() + let interactor = ProfileInteractorProtocolMock() + let profileStorage = ProfileStorageMock() + + let settings = CalendarSettings( + colorSelection: "accent", + calendarName: "Test Calendar", + accountSelection: "iCloud", + courseCalendarSync: true + ) + Given(profileStorage, .calendarSettings(getter: settings)) + + let manager = CalendarManager( + persistence: persistence, + interactor: interactor, + profileStorage: profileStorage + ) + + XCTAssertEqual(manager.calendarName, "Test Calendar") + } + + func testColorSelectionFromSettings() { + let persistence = ProfilePersistenceProtocolMock() + let interactor = ProfileInteractorProtocolMock() + let profileStorage = ProfileStorageMock() + + let settings = CalendarSettings( + colorSelection: "accent", + calendarName: "Test Calendar", + accountSelection: "iCloud", + courseCalendarSync: true + ) + Given(profileStorage, .calendarSettings(getter: settings)) + + let manager = CalendarManager( + persistence: persistence, + interactor: interactor, + profileStorage: profileStorage + ) + + XCTAssertEqual(manager.colorSelection?.color, Color.accentColor) + } +} diff --git a/Profile/ProfileTests/DatesAndCalendarViewModelTests.swift b/Profile/ProfileTests/DatesAndCalendarViewModelTests.swift new file mode 100644 index 000000000..443dcf875 --- /dev/null +++ b/Profile/ProfileTests/DatesAndCalendarViewModelTests.swift @@ -0,0 +1,328 @@ +// +// DatesAndCalendarViewModelTests.swift +// Profile +// +// Created by Ivan Stepanok on 30.10.2024. +// + + +import SwiftyMocky +import XCTest +import EventKit +@testable import Profile +@testable import Core +import Theme +import SwiftUICore +import Combine + +final class DatesAndCalendarViewModelTests: XCTestCase { + + var cancellables: Set! + + override func setUp() { + super.setUp() + cancellables = [] + } + + func testLoadCalendarOptions() { + // Given + let router = ProfileRouterMock() + let interactor = ProfileInteractorProtocolMock() + let persistence = ProfilePersistenceProtocolMock() + let calendarManager = CalendarManagerProtocolMock() + let connectivity = ConnectivityProtocolMock() + let profileStorage = ProfileStorageMock() + + let settings = CalendarSettings( + colorSelection: "accent", + calendarName: "Test Calendar", + accountSelection: "iCloud", + courseCalendarSync: true + ) + Given(profileStorage, .calendarSettings(getter: settings)) + Given(profileStorage, .lastCalendarName(getter: "Old Calendar")) + Given(profileStorage, .hideInactiveCourses(getter: true)) + + let viewModel = DatesAndCalendarViewModel( + router: router, + interactor: interactor, + profileStorage: profileStorage, + persistence: persistence, + calendarManager: calendarManager, + connectivity: connectivity + ) + + // When + viewModel.loadCalendarOptions() + + // Then + XCTAssertEqual(viewModel.colorSelection?.colorString, "accent") + XCTAssertEqual(viewModel.accountSelection?.title, "iCloud") + XCTAssertEqual(viewModel.calendarName, "Test Calendar") + XCTAssertEqual(viewModel.oldCalendarName, "Old Calendar") + XCTAssertTrue(viewModel.courseCalendarSync) + XCTAssertTrue(viewModel.hideInactiveCourses) + } + + func testClearAllData() { + // Given + let router = ProfileRouterMock() + let interactor = ProfileInteractorProtocolMock() + let persistence = ProfilePersistenceProtocolMock() + let calendarManager = CalendarManagerProtocolMock() + let connectivity = ConnectivityProtocolMock() + let profileStorage = ProfileStorageMock() + + let viewModel = DatesAndCalendarViewModel( + router: router, + interactor: interactor, + profileStorage: profileStorage, + persistence: persistence, + calendarManager: calendarManager, + connectivity: connectivity + ) + + // When + viewModel.clearAllData() + + // Then + Verify(calendarManager, 1, .clearAllData(removeCalendar: .value(true))) + Verify(router, 1, .back(animated: .value(false))) + Verify(router, 1, .showDatesAndCalendar()) + XCTAssertTrue(viewModel.courseCalendarSync) + XCTAssertFalse(viewModel.showDisableCalendarSync) + XCTAssertFalse(viewModel.openNewCalendarView) + } + + func testSaveCalendarOptions() { + // Given + let router = ProfileRouterMock() + let interactor = ProfileInteractorProtocolMock() + let persistence = ProfilePersistenceProtocolMock() + let calendarManager = CalendarManagerProtocolMock() + let connectivity = ConnectivityProtocolMock() + let profileStorage = ProfileStorageMock() + + var settings = CalendarSettings( + colorSelection: "accent", + calendarName: "Old Calendar", + accountSelection: "iCloud", + courseCalendarSync: true + ) + Given(profileStorage, .calendarSettings(getter: settings)) + + let viewModel = DatesAndCalendarViewModel( + router: router, + interactor: interactor, + profileStorage: profileStorage, + persistence: persistence, + calendarManager: calendarManager, + connectivity: connectivity + ) + + // When + viewModel.calendarName = "New Calendar" + viewModel.colorSelection = .init(color: .red) + viewModel.accountSelection = .init(title: "Local") + viewModel.courseCalendarSync = false + viewModel.saveCalendarOptions() + + // Then + XCTAssertEqual(profileStorage.calendarSettings?.calendarName, "New Calendar") + XCTAssertEqual(profileStorage.calendarSettings?.colorSelection, "red") + XCTAssertEqual(profileStorage.calendarSettings?.accountSelection, "Local") + XCTAssertFalse(profileStorage.calendarSettings?.courseCalendarSync ?? true) + XCTAssertEqual(profileStorage.lastCalendarName, "New Calendar") + } + + func testFetchCoursesSuccess() async { + // Given + let router = ProfileRouterMock() + let interactor = ProfileInteractorProtocolMock() + let persistence = ProfilePersistenceProtocolMock() + let calendarManager = CalendarManagerProtocolMock() + let connectivity = ConnectivityProtocolMock() + let profileStorage = ProfileStorageMock() + + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(calendarManager, .requestAccess(willReturn: true)) + + let courses = [ + CourseForSync( + id: UUID(), + courseID: "course-1", + name: "Course 1", + synced: true, + recentlyActive: true + ) + ] + Given(interactor, .enrollmentsStatus(willReturn: courses)) + Given(persistence, .getAllCourseStates(willReturn: [])) + + let viewModel = DatesAndCalendarViewModel( + router: router, + interactor: interactor, + profileStorage: profileStorage, + persistence: persistence, + calendarManager: calendarManager, + connectivity: connectivity + ) + + // When + await viewModel.fetchCourses() + + // Then + XCTAssertEqual(viewModel.assignmentStatus, .synced) + XCTAssertEqual(viewModel.coursesForSync.count, 1) + XCTAssertEqual(viewModel.coursesForSync.first?.courseID, "course-1") + Verify(calendarManager, 1, .createCalendarIfNeeded()) + Verify(interactor, 1, .enrollmentsStatus()) + } + + func testRequestCalendarPermissionSuccess() async { + // Given + let router = ProfileRouterMock() + let interactor = ProfileInteractorProtocolMock() + let persistence = ProfilePersistenceProtocolMock() + let calendarManager = CalendarManagerProtocolMock() + let connectivity = ConnectivityProtocolMock() + let profileStorage = ProfileStorageMock() + + Given(calendarManager, .requestAccess(willReturn: true)) + + let viewModel = DatesAndCalendarViewModel( + router: router, + interactor: interactor, + profileStorage: profileStorage, + persistence: persistence, + calendarManager: calendarManager, + connectivity: connectivity + ) + + // When + await viewModel.requestCalendarPermission() + + // Then + XCTAssertTrue(viewModel.openNewCalendarView) + XCTAssertFalse(viewModel.showCalendaAccessDenied) + } + + func testRequestCalendarPermissionDenied() async { + // Given + let router = ProfileRouterMock() + let interactor = ProfileInteractorProtocolMock() + let persistence = ProfilePersistenceProtocolMock() + let calendarManager = CalendarManagerProtocolMock() + let connectivity = ConnectivityProtocolMock() + let profileStorage = ProfileStorageMock() + + Given(calendarManager, .requestAccess(willReturn: false)) + + let viewModel = DatesAndCalendarViewModel( + router: router, + interactor: interactor, + profileStorage: profileStorage, + persistence: persistence, + calendarManager: calendarManager, + connectivity: connectivity + ) + + // When + await viewModel.requestCalendarPermission() + + // Then + XCTAssertTrue(viewModel.showCalendaAccessDenied) + XCTAssertFalse(viewModel.openNewCalendarView) + } + + func testToggleSyncForCourse() { + // Given + let router = ProfileRouterMock() + let interactor = ProfileInteractorProtocolMock() + let persistence = ProfilePersistenceProtocolMock() + let calendarManager = CalendarManagerProtocolMock() + let connectivity = ConnectivityProtocolMock() + let profileStorage = ProfileStorageMock() + + let course = CourseForSync( + id: UUID(), + courseID: "course-1", + name: "Course 1", + synced: false, + recentlyActive: true + ) + + let viewModel = DatesAndCalendarViewModel( + router: router, + interactor: interactor, + profileStorage: profileStorage, + persistence: persistence, + calendarManager: calendarManager, + connectivity: connectivity + ) + viewModel.coursesForSync = [course] + + // When + viewModel.toggleSync(for: course) + + // Then + XCTAssertTrue(viewModel.coursesForSync.first?.synced ?? false) + XCTAssertEqual(viewModel.coursesForAdding.count, 1) + XCTAssertEqual(viewModel.coursesForAdding.first?.courseID, "course-1") + } + + func testDeleteOldCalendarIfNeeded() async { + // Given + let router = ProfileRouterMock() + let interactor = ProfileInteractorProtocolMock() + let persistence = ProfilePersistenceProtocolMock() + let calendarManager = CalendarManagerProtocolMock() + let connectivity = ConnectivityProtocolMock() + let profileStorage = ProfileStorageMock() + + let settings = CalendarSettings( + colorSelection: "accent", + calendarName: "Old Calendar", + accountSelection: "iCloud", + courseCalendarSync: true + ) + + let states = [ + CourseCalendarState(courseID: "123", checksum: "checksum"), + CourseCalendarState(courseID: "124", checksum: "checksum2") + ] + + Given(persistence, .getAllCourseStates(willReturn: states)) + Given(profileStorage, .calendarSettings(getter: settings)) + Given(connectivity, .isInternetAvaliable(getter: true)) + Given(calendarManager, .requestAccess(willReturn: true)) + + let courses = [ + CourseForSync( + id: UUID(), + courseID: "course-1", + name: "Course 1", + synced: true, + recentlyActive: true + ) + ] + Given(interactor, .enrollmentsStatus(willReturn: courses)) + + let viewModel = DatesAndCalendarViewModel( + router: router, + interactor: interactor, + profileStorage: profileStorage, + persistence: persistence, + calendarManager: calendarManager, + connectivity: connectivity + ) + viewModel.calendarName = "New Calendar" + + // When + await viewModel.deleteOldCalendarIfNeeded() + + // Then + Verify(calendarManager, 1, .removeOldCalendar()) + Verify(persistence, 1, .removeAllCourseCalendarEvents()) + } +} diff --git a/Profile/ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift b/Profile/ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift index c6ba81755..2d5b2ed61 100644 --- a/Profile/ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/DeleteAccount/DeleteAccountViewModelTests.swift @@ -9,6 +9,7 @@ import SwiftyMocky import XCTest @testable import Core @testable import Profile +import OEXFoundation import Alamofire import SwiftUI diff --git a/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift index 2980fb4a8..50f77a155 100644 --- a/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/EditProfile/EditProfileViewModelTests.swift @@ -28,7 +28,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: false + isFullProfile: false, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -65,7 +66,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: false + isFullProfile: false, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -102,7 +104,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: false + isFullProfile: false, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -134,7 +137,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -166,7 +170,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -198,7 +203,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -230,7 +236,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -262,7 +269,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -294,7 +302,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -330,7 +339,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -366,7 +376,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -401,7 +412,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -436,7 +448,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -484,7 +497,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -527,7 +541,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -583,7 +598,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -637,7 +653,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -675,7 +692,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -707,7 +725,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: false + isFullProfile: false, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -738,7 +757,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: true + isFullProfile: true, + email: "" ) let languages = [ @@ -775,7 +795,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: false + isFullProfile: false, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) @@ -806,7 +827,8 @@ final class EditProfileViewModelTests: XCTestCase { country: "UA", spokenLanguage: "UA", shortBiography: "Bio", - isFullProfile: false + isFullProfile: false, + email: "" ) Given(interactor, .getSpokenLanguages(willReturn: [])) diff --git a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift index 40e56bbfa..44d3b96be 100644 --- a/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift +++ b/Profile/ProfileTests/Presentation/Profile/ProfileViewModelTests.swift @@ -30,7 +30,8 @@ final class ProfileViewModelTests: XCTestCase { yearOfBirth: 2000, country: "Ua", shortBiography: "Bio", - isFullProfile: false + isFullProfile: false, + email: "" ) Given(interactor, .getUserProfile(username: .value("Steve"), willReturn: user)) @@ -92,7 +93,6 @@ final class ProfileViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( interactor: interactor, - downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -107,7 +107,8 @@ final class ProfileViewModelTests: XCTestCase { yearOfBirth: 2000, country: "Ua", shortBiography: "Bio", - isFullProfile: false + isFullProfile: false, + email: "" ) Given(connectivity, .isInternetAvaliable(getter: true)) @@ -131,7 +132,6 @@ final class ProfileViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( interactor: interactor, - downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -146,7 +146,8 @@ final class ProfileViewModelTests: XCTestCase { yearOfBirth: 2000, country: "Ua", shortBiography: "Bio", - isFullProfile: false + isFullProfile: false, + email: "" ) Given(connectivity, .isInternetAvaliable(getter: false)) @@ -169,7 +170,6 @@ final class ProfileViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( interactor: interactor, - downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -184,7 +184,8 @@ final class ProfileViewModelTests: XCTestCase { yearOfBirth: 2000, country: "Ua", shortBiography: "Bio", - isFullProfile: false + isFullProfile: false, + email: "" ) let noInternetError = AFError.sessionInvalidated(error: URLError(.notConnectedToInternet)) @@ -209,7 +210,6 @@ final class ProfileViewModelTests: XCTestCase { let connectivity = ConnectivityProtocolMock() let viewModel = ProfileViewModel( interactor: interactor, - downloadManager: DownloadManagerMock(), router: router, analytics: analytics, config: ConfigMock(), @@ -227,121 +227,4 @@ final class ProfileViewModelTests: XCTestCase { XCTAssertFalse(viewModel.isShowProgress) XCTAssertTrue(viewModel.showError) } - - func testLogOutSuccess() async throws { - let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - downloadManager: DownloadManagerMock(), - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) - - Given(connectivity, .isInternetAvaliable(getter: true)) - - await viewModel.logOut() - - Verify(router, .showStartupScreen()) - XCTAssertFalse(viewModel.showError) - } - - func testTrackProfileVideoSettingsClicked() { - let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - downloadManager: DownloadManagerMock(), - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) - - viewModel.trackProfileVideoSettingsClicked() - - Verify(analytics, 1, .profileVideoSettingsClicked()) - } - - func testTrackEmailSupportClicked() { - let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - downloadManager: DownloadManagerMock(), - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) - - viewModel.trackEmailSupportClicked() - - Verify(analytics, 1, .emailSupportClicked()) - } - - func testTrackCookiePolicyClicked() { - let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - downloadManager: DownloadManagerMock(), - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) - - viewModel.trackCookiePolicyClicked() - - Verify(analytics, 1, .cookiePolicyClicked()) - } - - func testTrackPrivacyPolicyClicked() { - let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - downloadManager: DownloadManagerMock(), - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) - - viewModel.trackPrivacyPolicyClicked() - - Verify(analytics, 1, .privacyPolicyClicked()) - } - - func testTrackProfileEditClicked() { - let interactor = ProfileInteractorProtocolMock() - let router = ProfileRouterMock() - let analytics = ProfileAnalyticsMock() - let connectivity = ConnectivityProtocolMock() - let viewModel = ProfileViewModel( - interactor: interactor, - downloadManager: DownloadManagerMock(), - router: router, - analytics: analytics, - config: ConfigMock(), - connectivity: connectivity - ) - - viewModel.trackProfileEditClicked() - - Verify(analytics, 1, .profileEditClicked()) - } } diff --git a/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift b/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift new file mode 100644 index 000000000..de7d52a4c --- /dev/null +++ b/Profile/ProfileTests/Presentation/Settings/SettingsViewModelTests.swift @@ -0,0 +1,215 @@ +// +// SettingsViewModelTests.swift +// ProfileTests +// +// Created by  Stepanok Ivan on 10.04.2024. +// + +import SwiftyMocky +import XCTest +@testable import Core +@testable import Profile +import Alamofire +import SwiftUI + +final class SettingsViewModelTests: XCTestCase { + + func testLogOutSuccess() async throws { + let interactor = ProfileInteractorProtocolMock() + let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() + let coreAnalytics = CoreAnalyticsMock() + + Given( + interactor, + .getSettings( + willReturn: UserSettings( + wifiOnly: true, + streamingQuality: .auto, + downloadQuality: .auto + ) + ) + ) + + let viewModel = SettingsViewModel( + interactor: interactor, + downloadManager: DownloadManagerMock(), + router: router, + analytics: analytics, + coreAnalytics: coreAnalytics, + config: ConfigMock(), + corePersistence: CorePersistenceMock(), + connectivity: Connectivity() + ) + + await viewModel.logOut() + + Verify(router, .showStartupScreen()) + XCTAssertFalse(viewModel.showError) + } + + func testTrackProfileVideoSettingsClicked() { + let interactor = ProfileInteractorProtocolMock() + let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() + let coreAnalytics = CoreAnalyticsMock() + + Given( + interactor, + .getSettings( + willReturn: UserSettings( + wifiOnly: true, + streamingQuality: .auto, + downloadQuality: .auto + ) + ) + ) + + let viewModel = SettingsViewModel( + interactor: interactor, + downloadManager: DownloadManagerMock(), + router: router, + analytics: analytics, + coreAnalytics: coreAnalytics, + config: ConfigMock(), + corePersistence: CorePersistenceMock(), + connectivity: Connectivity() + ) + + viewModel.trackProfileVideoSettingsClicked() + + Verify(analytics, 1, .profileVideoSettingsClicked()) + } + + func testTrackEmailSupportClicked() { + let interactor = ProfileInteractorProtocolMock() + let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() + let coreAnalytics = CoreAnalyticsMock() + + Given( + interactor, + .getSettings( + willReturn: UserSettings( + wifiOnly: true, + streamingQuality: .auto, + downloadQuality: .auto + ) + ) + ) + + let viewModel = SettingsViewModel( + interactor: interactor, + downloadManager: DownloadManagerMock(), + router: router, + analytics: analytics, + coreAnalytics: coreAnalytics, + config: ConfigMock(), + corePersistence: CorePersistenceMock(), + connectivity: Connectivity() + ) + + viewModel.trackEmailSupportClicked() + + Verify(analytics, 1, .emailSupportClicked()) + } + + func testTrackCookiePolicyClicked() { + let interactor = ProfileInteractorProtocolMock() + let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() + let coreAnalytics = CoreAnalyticsMock() + + Given( + interactor, + .getSettings( + willReturn: UserSettings( + wifiOnly: true, + streamingQuality: .auto, + downloadQuality: .auto + ) + ) + ) + + let viewModel = SettingsViewModel( + interactor: interactor, + downloadManager: DownloadManagerMock(), + router: router, + analytics: analytics, + coreAnalytics: coreAnalytics, + config: ConfigMock(), + corePersistence: CorePersistenceMock(), + connectivity: Connectivity() + ) + + viewModel.trackCookiePolicyClicked() + + Verify(analytics, 1, .cookiePolicyClicked()) + } + + func testTrackPrivacyPolicyClicked() { + let interactor = ProfileInteractorProtocolMock() + let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() + let coreAnalytics = CoreAnalyticsMock() + + Given( + interactor, + .getSettings( + willReturn: UserSettings( + wifiOnly: true, + streamingQuality: .auto, + downloadQuality: .auto + ) + ) + ) + + let viewModel = SettingsViewModel( + interactor: interactor, + downloadManager: DownloadManagerMock(), + router: router, + analytics: analytics, + coreAnalytics: coreAnalytics, + config: ConfigMock(), + corePersistence: CorePersistenceMock(), + connectivity: Connectivity() + ) + + viewModel.trackPrivacyPolicyClicked() + + Verify(analytics, 1, .privacyPolicyClicked()) + } + + func testTrackProfileEditClicked() { + let interactor = ProfileInteractorProtocolMock() + let router = ProfileRouterMock() + let analytics = ProfileAnalyticsMock() + let coreAnalytics = CoreAnalyticsMock() + + Given( + interactor, + .getSettings( + willReturn: UserSettings( + wifiOnly: true, + streamingQuality: .auto, + downloadQuality: .auto + ) + ) + ) + + let viewModel = SettingsViewModel( + interactor: interactor, + downloadManager: DownloadManagerMock(), + router: router, + analytics: analytics, + coreAnalytics: coreAnalytics, + config: ConfigMock(), + corePersistence: CorePersistenceMock(), + connectivity: Connectivity() + ) + + viewModel.trackProfileEditClicked() + + Verify(analytics, 1, .profileEditClicked()) + } +} diff --git a/Profile/ProfileTests/ProfileMock.generated.swift b/Profile/ProfileTests/ProfileMock.generated.swift index 30ec58bb5..38e43e5da 100644 --- a/Profile/ProfileTests/ProfileMock.generated.swift +++ b/Profile/ProfileTests/ProfileMock.generated.swift @@ -13,6 +13,7 @@ import Profile import Foundation import SwiftUI import Combine +import OEXFoundation // MARK: - AuthInteractorProtocol @@ -93,6 +94,22 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { return __value } + open func login(ssoToken: String) throws -> User { + addInvocation(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) + let perform = methodPerformValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))) as? (String) -> Void + perform?(`ssoToken`) + var __value: User + do { + __value = try methodReturnValue(.m_login__ssoToken_ssoToken(Parameter.value(`ssoToken`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for login(ssoToken: String). Use given") + Failure("Stub return value not specified for login(ssoToken: String). Use given") + } catch { + throw error + } + return __value + } + open func resetPassword(email: String) throws -> ResetPassword { addInvocation(.m_resetPassword__email_email(Parameter.value(`email`))) let perform = methodPerformValue(.m_resetPassword__email_email(Parameter.value(`email`))) as? (String) -> Void @@ -174,6 +191,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { fileprivate enum MethodType { case m_login__username_usernamepassword_password(Parameter, Parameter) case m_login__externalToken_externalTokenbackend_backend(Parameter, Parameter) + case m_login__ssoToken_ssoToken(Parameter) case m_resetPassword__email_email(Parameter) case m_getCookies__force_force(Parameter) case m_getRegistrationFields @@ -194,6 +212,11 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBackend, rhs: rhsBackend, with: matcher), lhsBackend, rhsBackend, "backend")) return Matcher.ComparisonResult(results) + case (.m_login__ssoToken_ssoToken(let lhsSsotoken), .m_login__ssoToken_ssoToken(let rhsSsotoken)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSsotoken, rhs: rhsSsotoken, with: matcher), lhsSsotoken, rhsSsotoken, "ssoToken")) + return Matcher.ComparisonResult(results) + case (.m_resetPassword__email_email(let lhsEmail), .m_resetPassword__email_email(let rhsEmail)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEmail, rhs: rhsEmail, with: matcher), lhsEmail, rhsEmail, "email")) @@ -224,6 +247,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case let .m_login__username_usernamepassword_password(p0, p1): return p0.intValue + p1.intValue case let .m_login__externalToken_externalTokenbackend_backend(p0, p1): return p0.intValue + p1.intValue + case let .m_login__ssoToken_ssoToken(p0): return p0.intValue case let .m_resetPassword__email_email(p0): return p0.intValue case let .m_getCookies__force_force(p0): return p0.intValue case .m_getRegistrationFields: return 0 @@ -235,6 +259,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { switch self { case .m_login__username_usernamepassword_password: return ".login(username:password:)" case .m_login__externalToken_externalTokenbackend_backend: return ".login(externalToken:backend:)" + case .m_login__ssoToken_ssoToken: return ".login(ssoToken:)" case .m_resetPassword__email_email: return ".resetPassword(email:)" case .m_getCookies__force_force: return ".getCookies(force:)" case .m_getRegistrationFields: return ".getRegistrationFields()" @@ -261,6 +286,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, willReturn: User...) -> MethodStub { return Given(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), products: willReturn.map({ StubProduct.return($0 as Any) })) } + public static func login(ssoToken: Parameter, willReturn: User...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } public static func resetPassword(email: Parameter, willReturn: ResetPassword...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willReturn.map({ StubProduct.return($0 as Any) })) } @@ -297,6 +325,16 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { willProduce(stubber) return given } + public static func login(ssoToken: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func login(ssoToken: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_login__ssoToken_ssoToken(`ssoToken`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (User).self) + willProduce(stubber) + return given + } public static func resetPassword(email: Parameter, willThrow: Error...) -> MethodStub { return Given(method: .m_resetPassword__email_email(`email`), products: willThrow.map({ StubProduct.throw($0) })) } @@ -356,6 +394,7 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(username: Parameter, password: Parameter) -> Verify { return Verify(method: .m_login__username_usernamepassword_password(`username`, `password`))} @discardableResult public static func login(externalToken: Parameter, backend: Parameter) -> Verify { return Verify(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`))} + public static func login(ssoToken: Parameter) -> Verify { return Verify(method: .m_login__ssoToken_ssoToken(`ssoToken`))} public static func resetPassword(email: Parameter) -> Verify { return Verify(method: .m_resetPassword__email_email(`email`))} public static func getCookies(force: Parameter) -> Verify { return Verify(method: .m_getCookies__force_force(`force`))} public static func getRegistrationFields() -> Verify { return Verify(method: .m_getRegistrationFields)} @@ -375,6 +414,9 @@ open class AuthInteractorProtocolMock: AuthInteractorProtocol, Mock { public static func login(externalToken: Parameter, backend: Parameter, perform: @escaping (String, String) -> Void) -> Perform { return Perform(method: .m_login__externalToken_externalTokenbackend_backend(`externalToken`, `backend`), performs: perform) } + public static func login(ssoToken: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_login__ssoToken_ssoToken(`ssoToken`), performs: perform) + } public static func resetPassword(email: Parameter, perform: @escaping (String) -> Void) -> Perform { return Perform(method: .m_resetPassword__email_email(`email`), performs: perform) } @@ -581,6 +623,12 @@ open class BaseRouterMock: BaseRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -619,6 +667,7 @@ open class BaseRouterMock: BaseRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showSSOWebBrowser__title_title(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -679,6 +728,11 @@ open class BaseRouterMock: BaseRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) return Matcher.ComparisonResult(results) + case (.m_showSSOWebBrowser__title_title(let lhsTitle), .m_showSSOWebBrowser__title_title(let rhsTitle)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -732,6 +786,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showSSOWebBrowser__title_title(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -752,6 +807,7 @@ open class BaseRouterMock: BaseRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showSSOWebBrowser__title_title: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -786,6 +842,7 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} + public static func showSSOWebBrowser(title: Parameter) -> Verify { return Verify(method: .m_showSSOWebBrowser__title_title(`title`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`))} @@ -832,6 +889,9 @@ open class BaseRouterMock: BaseRouter, Mock { public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showSSOWebBrowser__title_title(`title`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -919,9 +979,9 @@ open class BaseRouterMock: BaseRouter, Mock { } } -// MARK: - ConnectivityProtocol +// MARK: - CalendarManagerProtocol -open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { +open class CalendarManagerProtocolMock: CalendarManagerProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -959,51 +1019,176 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } - public var isInternetAvaliable: Bool { - get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } - } - private var __p_isInternetAvaliable: (Bool)? - public var isMobileData: Bool { - get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } - } - private var __p_isMobileData: (Bool)? - public var internetReachableSubject: CurrentValueSubject { - get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } - } - private var __p_internetReachableSubject: (CurrentValueSubject)? + open func createCalendarIfNeeded() { + addInvocation(.m_createCalendarIfNeeded) + let perform = methodPerformValue(.m_createCalendarIfNeeded) as? () -> Void + perform?() + } + + open func filterCoursesBySelected(fetchedCourses: [CourseForSync]) -> [CourseForSync] { + addInvocation(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) + let perform = methodPerformValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))) as? ([CourseForSync]) -> Void + perform?(`fetchedCourses`) + var __value: [CourseForSync] + do { + __value = try methodReturnValue(.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>.value(`fetchedCourses`))).casted() + } catch { + onFatalFailure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + Failure("Stub return value not specified for filterCoursesBySelected(fetchedCourses: [CourseForSync]). Use given") + } + return __value + } + + open func removeOldCalendar() { + addInvocation(.m_removeOldCalendar) + let perform = methodPerformValue(.m_removeOldCalendar) as? () -> Void + perform?() + } + + open func removeOutdatedEvents(courseID: String) { + addInvocation(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_removeOutdatedEvents__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + } + + open func syncCourse(courseID: String, courseName: String, dates: CourseDates) { + addInvocation(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) + let perform = methodPerformValue(.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter.value(`courseID`), Parameter.value(`courseName`), Parameter.value(`dates`))) as? (String, String, CourseDates) -> Void + perform?(`courseID`, `courseName`, `dates`) + } + + open func requestAccess() -> Bool { + addInvocation(.m_requestAccess) + let perform = methodPerformValue(.m_requestAccess) as? () -> Void + perform?() + var __value: Bool + do { + __value = try methodReturnValue(.m_requestAccess).casted() + } catch { + onFatalFailure("Stub return value not specified for requestAccess(). Use given") + Failure("Stub return value not specified for requestAccess(). Use given") + } + return __value + } + + open func courseStatus(courseID: String) -> SyncStatus { + addInvocation(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: SyncStatus + do { + __value = try methodReturnValue(.m_courseStatus__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch { + onFatalFailure("Stub return value not specified for courseStatus(courseID: String). Use given") + Failure("Stub return value not specified for courseStatus(courseID: String). Use given") + } + return __value + } + open func clearAllData(removeCalendar: Bool) { + addInvocation(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) + let perform = methodPerformValue(.m_clearAllData__removeCalendar_removeCalendar(Parameter.value(`removeCalendar`))) as? (Bool) -> Void + perform?(`removeCalendar`) + } + open func isDatesChanged(courseID: String, checksum: String) -> Bool { + addInvocation(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) + let perform = methodPerformValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))) as? (String, String) -> Void + perform?(`courseID`, `checksum`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter.value(`courseID`), Parameter.value(`checksum`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + Failure("Stub return value not specified for isDatesChanged(courseID: String, checksum: String). Use given") + } + return __value + } fileprivate enum MethodType { - case p_isInternetAvaliable_get - case p_isMobileData_get - case p_internetReachableSubject_get + case m_createCalendarIfNeeded + case m_filterCoursesBySelected__fetchedCourses_fetchedCourses(Parameter<[CourseForSync]>) + case m_removeOldCalendar + case m_removeOutdatedEvents__courseID_courseID(Parameter) + case m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(Parameter, Parameter, Parameter) + case m_requestAccess + case m_courseStatus__courseID_courseID(Parameter) + case m_clearAllData__removeCalendar_removeCalendar(Parameter) + case m_isDatesChanged__courseID_courseIDchecksum_checksum(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match - case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match - case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + switch (lhs, rhs) { + case (.m_createCalendarIfNeeded, .m_createCalendarIfNeeded): return .match + + case (.m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let lhsFetchedcourses), .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(let rhsFetchedcourses)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsFetchedcourses, rhs: rhsFetchedcourses, with: matcher), lhsFetchedcourses, rhsFetchedcourses, "fetchedCourses")) + return Matcher.ComparisonResult(results) + + case (.m_removeOldCalendar, .m_removeOldCalendar): return .match + + case (.m_removeOutdatedEvents__courseID_courseID(let lhsCourseid), .m_removeOutdatedEvents__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let lhsCourseid, let lhsCoursename, let lhsDates), .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(let rhsCourseid, let rhsCoursename, let rhsDates)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCoursename, rhs: rhsCoursename, with: matcher), lhsCoursename, rhsCoursename, "courseName")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDates, rhs: rhsDates, with: matcher), lhsDates, rhsDates, "dates")) + return Matcher.ComparisonResult(results) + + case (.m_requestAccess, .m_requestAccess): return .match + + case (.m_courseStatus__courseID_courseID(let lhsCourseid), .m_courseStatus__courseID_courseID(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + return Matcher.ComparisonResult(results) + + case (.m_clearAllData__removeCalendar_removeCalendar(let lhsRemovecalendar), .m_clearAllData__removeCalendar_removeCalendar(let rhsRemovecalendar)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRemovecalendar, rhs: rhsRemovecalendar, with: matcher), lhsRemovecalendar, rhsRemovecalendar, "removeCalendar")) + return Matcher.ComparisonResult(results) + + case (.m_isDatesChanged__courseID_courseIDchecksum_checksum(let lhsCourseid, let lhsChecksum), .m_isDatesChanged__courseID_courseIDchecksum_checksum(let rhsCourseid, let rhsChecksum)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsChecksum, rhs: rhsChecksum, with: matcher), lhsChecksum, rhsChecksum, "checksum")) + return Matcher.ComparisonResult(results) default: return .none } } func intValue() -> Int { switch self { - case .p_isInternetAvaliable_get: return 0 - case .p_isMobileData_get: return 0 - case .p_internetReachableSubject_get: return 0 + case .m_createCalendarIfNeeded: return 0 + case let .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(p0): return p0.intValue + case .m_removeOldCalendar: return 0 + case let .m_removeOutdatedEvents__courseID_courseID(p0): return p0.intValue + case let .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case .m_requestAccess: return 0 + case let .m_courseStatus__courseID_courseID(p0): return p0.intValue + case let .m_clearAllData__removeCalendar_removeCalendar(p0): return p0.intValue + case let .m_isDatesChanged__courseID_courseIDchecksum_checksum(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { - case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" - case .p_isMobileData_get: return "[get] .isMobileData" - case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + case .m_createCalendarIfNeeded: return ".createCalendarIfNeeded()" + case .m_filterCoursesBySelected__fetchedCourses_fetchedCourses: return ".filterCoursesBySelected(fetchedCourses:)" + case .m_removeOldCalendar: return ".removeOldCalendar()" + case .m_removeOutdatedEvents__courseID_courseID: return ".removeOutdatedEvents(courseID:)" + case .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates: return ".syncCourse(courseID:courseName:dates:)" + case .m_requestAccess: return ".requestAccess()" + case .m_courseStatus__courseID_courseID: return ".courseStatus(courseID:)" + case .m_clearAllData__removeCalendar_removeCalendar: return ".clearAllData(removeCalendar:)" + case .m_isDatesChanged__courseID_courseIDchecksum_checksum: return ".isDatesChanged(courseID:checksum:)" } } } @@ -1016,30 +1201,94 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { super.init(products) } - public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willReturn: [CourseForSync]...) -> MethodStub { + return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { - return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func requestAccess(willReturn: Bool...) -> MethodStub { + return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { - return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + public static func courseStatus(courseID: Parameter, willReturn: SyncStatus...) -> MethodStub { + return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, willProduce: (Stubber<[CourseForSync]>) -> Void) -> MethodStub { + let willReturn: [[CourseForSync]] = [] + let given: Given = { return Given(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseForSync]).self) + willProduce(stubber) + return given + } + public static func requestAccess(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_requestAccess, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given + } + public static func courseStatus(courseID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [SyncStatus] = [] + let given: Given = { return Given(method: .m_courseStatus__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (SyncStatus).self) + willProduce(stubber) + return given + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given } - } public struct Verify { fileprivate var method: MethodType - public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } - public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } - public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } + public static func createCalendarIfNeeded() -> Verify { return Verify(method: .m_createCalendarIfNeeded)} + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>) -> Verify { return Verify(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`))} + public static func removeOldCalendar() -> Verify { return Verify(method: .m_removeOldCalendar)} + public static func removeOutdatedEvents(courseID: Parameter) -> Verify { return Verify(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`))} + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter) -> Verify { return Verify(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`))} + public static func requestAccess() -> Verify { return Verify(method: .m_requestAccess)} + public static func courseStatus(courseID: Parameter) -> Verify { return Verify(method: .m_courseStatus__courseID_courseID(`courseID`))} + public static func clearAllData(removeCalendar: Parameter) -> Verify { return Verify(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`))} + public static func isDatesChanged(courseID: Parameter, checksum: Parameter) -> Verify { return Verify(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`))} } public struct Perform { fileprivate var method: MethodType var performs: Any + public static func createCalendarIfNeeded(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_createCalendarIfNeeded, performs: perform) + } + public static func filterCoursesBySelected(fetchedCourses: Parameter<[CourseForSync]>, perform: @escaping ([CourseForSync]) -> Void) -> Perform { + return Perform(method: .m_filterCoursesBySelected__fetchedCourses_fetchedCourses(`fetchedCourses`), performs: perform) + } + public static func removeOldCalendar(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeOldCalendar, performs: perform) + } + public static func removeOutdatedEvents(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_removeOutdatedEvents__courseID_courseID(`courseID`), performs: perform) + } + public static func syncCourse(courseID: Parameter, courseName: Parameter, dates: Parameter, perform: @escaping (String, String, CourseDates) -> Void) -> Perform { + return Perform(method: .m_syncCourse__courseID_courseIDcourseName_courseNamedates_dates(`courseID`, `courseName`, `dates`), performs: perform) + } + public static func requestAccess(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_requestAccess, performs: perform) + } + public static func courseStatus(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_courseStatus__courseID_courseID(`courseID`), performs: perform) + } + public static func clearAllData(removeCalendar: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_clearAllData__removeCalendar_removeCalendar(`removeCalendar`), performs: perform) + } + public static func isDatesChanged(courseID: Parameter, checksum: Parameter, perform: @escaping (String, String) -> Void) -> Perform { + return Perform(method: .m_isDatesChanged__courseID_courseIDchecksum_checksum(`courseID`, `checksum`), performs: perform) + } } public func given(_ method: Given) { @@ -1115,9 +1364,9 @@ open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { } } -// MARK: - CoreAnalytics +// MARK: - ConfigProtocol -open class CoreAnalyticsMock: CoreAnalytics, Mock { +open class ConfigProtocolMock: ConfigProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -1155,118 +1404,249 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } + public var baseURL: URL { + get { invocations.append(.p_baseURL_get); return __p_baseURL ?? givenGetterValue(.p_baseURL_get, "ConfigProtocolMock - stub value for baseURL was not defined") } + } + private var __p_baseURL: (URL)? + public var baseSSOURL: URL { + get { invocations.append(.p_baseSSOURL_get); return __p_baseSSOURL ?? givenGetterValue(.p_baseSSOURL_get, "ConfigProtocolMock - stub value for baseSSOURL was not defined") } + } + private var __p_baseSSOURL: (URL)? + public var ssoFinishedURL: URL { + get { invocations.append(.p_ssoFinishedURL_get); return __p_ssoFinishedURL ?? givenGetterValue(.p_ssoFinishedURL_get, "ConfigProtocolMock - stub value for ssoFinishedURL was not defined") } + } + private var __p_ssoFinishedURL: (URL)? + public var ssoButtonTitle: [String: Any] { + get { invocations.append(.p_ssoButtonTitle_get); return __p_ssoButtonTitle ?? givenGetterValue(.p_ssoButtonTitle_get, "ConfigProtocolMock - stub value for ssoButtonTitle was not defined") } + } + private var __p_ssoButtonTitle: ([String: Any])? - open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void - perform?(`event`, `parameters`) - } + public var oAuthClientId: String { + get { invocations.append(.p_oAuthClientId_get); return __p_oAuthClientId ?? givenGetterValue(.p_oAuthClientId_get, "ConfigProtocolMock - stub value for oAuthClientId was not defined") } + } + private var __p_oAuthClientId: (String)? - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { - addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void - perform?(`event`, `biValue`, `parameters`) - } + public var tokenType: TokenType { + get { invocations.append(.p_tokenType_get); return __p_tokenType ?? givenGetterValue(.p_tokenType_get, "ConfigProtocolMock - stub value for tokenType was not defined") } + } + private var __p_tokenType: (TokenType)? - open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { - addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) - let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void - perform?(`event`, `biValue`, `action`, `rating`) - } + public var feedbackEmail: String { + get { invocations.append(.p_feedbackEmail_get); return __p_feedbackEmail ?? givenGetterValue(.p_feedbackEmail_get, "ConfigProtocolMock - stub value for feedbackEmail was not defined") } + } + private var __p_feedbackEmail: (String)? - open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { - addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) - let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void - perform?(`event`, `bivalue`, `value`, `oldValue`) - } + public var appStoreLink: String { + get { invocations.append(.p_appStoreLink_get); return __p_appStoreLink ?? givenGetterValue(.p_appStoreLink_get, "ConfigProtocolMock - stub value for appStoreLink was not defined") } + } + private var __p_appStoreLink: (String)? - open func trackEvent(_ event: AnalyticsEvent) { - addInvocation(.m_trackEvent__event(Parameter.value(`event`))) - let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void - perform?(`event`) - } + public var faq: URL? { + get { invocations.append(.p_faq_get); return __p_faq ?? optionalGivenGetterValue(.p_faq_get, "ConfigProtocolMock - stub value for faq was not defined") } + } + private var __p_faq: (URL)? - open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) - let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void - perform?(`event`, `biValue`) - } + public var platformName: String { + get { invocations.append(.p_platformName_get); return __p_platformName ?? givenGetterValue(.p_platformName_get, "ConfigProtocolMock - stub value for platformName was not defined") } + } + private var __p_platformName: (String)? + public var agreement: AgreementConfig { + get { invocations.append(.p_agreement_get); return __p_agreement ?? givenGetterValue(.p_agreement_get, "ConfigProtocolMock - stub value for agreement was not defined") } + } + private var __p_agreement: (AgreementConfig)? - fileprivate enum MethodType { - case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) - case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) - case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) - case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) - case m_trackEvent__event(Parameter) - case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + public var firebase: FirebaseConfig { + get { invocations.append(.p_firebase_get); return __p_firebase ?? givenGetterValue(.p_firebase_get, "ConfigProtocolMock - stub value for firebase was not defined") } + } + private var __p_firebase: (FirebaseConfig)? - static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { - switch (lhs, rhs) { - case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + public var facebook: FacebookConfig { + get { invocations.append(.p_facebook_get); return __p_facebook ?? givenGetterValue(.p_facebook_get, "ConfigProtocolMock - stub value for facebook was not defined") } + } + private var __p_facebook: (FacebookConfig)? - case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) - return Matcher.ComparisonResult(results) + public var microsoft: MicrosoftConfig { + get { invocations.append(.p_microsoft_get); return __p_microsoft ?? givenGetterValue(.p_microsoft_get, "ConfigProtocolMock - stub value for microsoft was not defined") } + } + private var __p_microsoft: (MicrosoftConfig)? - case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) - return Matcher.ComparisonResult(results) + public var google: GoogleConfig { + get { invocations.append(.p_google_get); return __p_google ?? givenGetterValue(.p_google_get, "ConfigProtocolMock - stub value for google was not defined") } + } + private var __p_google: (GoogleConfig)? - case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) - return Matcher.ComparisonResult(results) + public var appleSignIn: AppleSignInConfig { + get { invocations.append(.p_appleSignIn_get); return __p_appleSignIn ?? givenGetterValue(.p_appleSignIn_get, "ConfigProtocolMock - stub value for appleSignIn was not defined") } + } + private var __p_appleSignIn: (AppleSignInConfig)? - case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - return Matcher.ComparisonResult(results) + public var features: FeaturesConfig { + get { invocations.append(.p_features_get); return __p_features ?? givenGetterValue(.p_features_get, "ConfigProtocolMock - stub value for features was not defined") } + } + private var __p_features: (FeaturesConfig)? - case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) - return Matcher.ComparisonResult(results) - default: return .none - } - } + public var theme: ThemeConfig { + get { invocations.append(.p_theme_get); return __p_theme ?? givenGetterValue(.p_theme_get, "ConfigProtocolMock - stub value for theme was not defined") } + } + private var __p_theme: (ThemeConfig)? - func intValue() -> Int { - switch self { - case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue - case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue - case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue - case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue - case let .m_trackEvent__event(p0): return p0.intValue - case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue - } - } - func assertionName() -> String { - switch self { - case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" - case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" - case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" - case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" - case .m_trackEvent__event: return ".trackEvent(_:)" - case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + public var uiComponents: UIComponentsConfig { + get { invocations.append(.p_uiComponents_get); return __p_uiComponents ?? givenGetterValue(.p_uiComponents_get, "ConfigProtocolMock - stub value for uiComponents was not defined") } + } + private var __p_uiComponents: (UIComponentsConfig)? + + public var discovery: DiscoveryConfig { + get { invocations.append(.p_discovery_get); return __p_discovery ?? givenGetterValue(.p_discovery_get, "ConfigProtocolMock - stub value for discovery was not defined") } + } + private var __p_discovery: (DiscoveryConfig)? + + public var dashboard: DashboardConfig { + get { invocations.append(.p_dashboard_get); return __p_dashboard ?? givenGetterValue(.p_dashboard_get, "ConfigProtocolMock - stub value for dashboard was not defined") } + } + private var __p_dashboard: (DashboardConfig)? + + public var braze: BrazeConfig { + get { invocations.append(.p_braze_get); return __p_braze ?? givenGetterValue(.p_braze_get, "ConfigProtocolMock - stub value for braze was not defined") } + } + private var __p_braze: (BrazeConfig)? + + public var branch: BranchConfig { + get { invocations.append(.p_branch_get); return __p_branch ?? givenGetterValue(.p_branch_get, "ConfigProtocolMock - stub value for branch was not defined") } + } + private var __p_branch: (BranchConfig)? + + public var program: DiscoveryConfig { + get { invocations.append(.p_program_get); return __p_program ?? givenGetterValue(.p_program_get, "ConfigProtocolMock - stub value for program was not defined") } + } + private var __p_program: (DiscoveryConfig)? + + public var URIScheme: String { + get { invocations.append(.p_URIScheme_get); return __p_URIScheme ?? givenGetterValue(.p_URIScheme_get, "ConfigProtocolMock - stub value for URIScheme was not defined") } + } + private var __p_URIScheme: (String)? + + + + + + + fileprivate enum MethodType { + case p_baseURL_get + case p_baseSSOURL_get + case p_ssoFinishedURL_get + case p_ssoButtonTitle_get + case p_oAuthClientId_get + case p_tokenType_get + case p_feedbackEmail_get + case p_appStoreLink_get + case p_faq_get + case p_platformName_get + case p_agreement_get + case p_firebase_get + case p_facebook_get + case p_microsoft_get + case p_google_get + case p_appleSignIn_get + case p_features_get + case p_theme_get + case p_uiComponents_get + case p_discovery_get + case p_dashboard_get + case p_braze_get + case p_branch_get + case p_program_get + case p_URIScheme_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_baseURL_get,.p_baseURL_get): return Matcher.ComparisonResult.match + case (.p_baseSSOURL_get,.p_baseSSOURL_get): return Matcher.ComparisonResult.match + case (.p_ssoFinishedURL_get,.p_ssoFinishedURL_get): return Matcher.ComparisonResult.match + case (.p_ssoButtonTitle_get,.p_ssoButtonTitle_get): return Matcher.ComparisonResult.match + case (.p_oAuthClientId_get,.p_oAuthClientId_get): return Matcher.ComparisonResult.match + case (.p_tokenType_get,.p_tokenType_get): return Matcher.ComparisonResult.match + case (.p_feedbackEmail_get,.p_feedbackEmail_get): return Matcher.ComparisonResult.match + case (.p_appStoreLink_get,.p_appStoreLink_get): return Matcher.ComparisonResult.match + case (.p_faq_get,.p_faq_get): return Matcher.ComparisonResult.match + case (.p_platformName_get,.p_platformName_get): return Matcher.ComparisonResult.match + case (.p_agreement_get,.p_agreement_get): return Matcher.ComparisonResult.match + case (.p_firebase_get,.p_firebase_get): return Matcher.ComparisonResult.match + case (.p_facebook_get,.p_facebook_get): return Matcher.ComparisonResult.match + case (.p_microsoft_get,.p_microsoft_get): return Matcher.ComparisonResult.match + case (.p_google_get,.p_google_get): return Matcher.ComparisonResult.match + case (.p_appleSignIn_get,.p_appleSignIn_get): return Matcher.ComparisonResult.match + case (.p_features_get,.p_features_get): return Matcher.ComparisonResult.match + case (.p_theme_get,.p_theme_get): return Matcher.ComparisonResult.match + case (.p_uiComponents_get,.p_uiComponents_get): return Matcher.ComparisonResult.match + case (.p_discovery_get,.p_discovery_get): return Matcher.ComparisonResult.match + case (.p_dashboard_get,.p_dashboard_get): return Matcher.ComparisonResult.match + case (.p_braze_get,.p_braze_get): return Matcher.ComparisonResult.match + case (.p_branch_get,.p_branch_get): return Matcher.ComparisonResult.match + case (.p_program_get,.p_program_get): return Matcher.ComparisonResult.match + case (.p_URIScheme_get,.p_URIScheme_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_baseURL_get: return 0 + case .p_baseSSOURL_get: return 0 + case .p_ssoFinishedURL_get: return 0 + case .p_ssoButtonTitle_get: return 0 + case .p_oAuthClientId_get: return 0 + case .p_tokenType_get: return 0 + case .p_feedbackEmail_get: return 0 + case .p_appStoreLink_get: return 0 + case .p_faq_get: return 0 + case .p_platformName_get: return 0 + case .p_agreement_get: return 0 + case .p_firebase_get: return 0 + case .p_facebook_get: return 0 + case .p_microsoft_get: return 0 + case .p_google_get: return 0 + case .p_appleSignIn_get: return 0 + case .p_features_get: return 0 + case .p_theme_get: return 0 + case .p_uiComponents_get: return 0 + case .p_discovery_get: return 0 + case .p_dashboard_get: return 0 + case .p_braze_get: return 0 + case .p_branch_get: return 0 + case .p_program_get: return 0 + case .p_URIScheme_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_baseURL_get: return "[get] .baseURL" + case .p_baseSSOURL_get: return "[get] .baseSSOURL" + case .p_ssoFinishedURL_get: return "[get] .ssoFinishedURL" + case .p_ssoButtonTitle_get: return "[get] .ssoButtonTitle" + case .p_oAuthClientId_get: return "[get] .oAuthClientId" + case .p_tokenType_get: return "[get] .tokenType" + case .p_feedbackEmail_get: return "[get] .feedbackEmail" + case .p_appStoreLink_get: return "[get] .appStoreLink" + case .p_faq_get: return "[get] .faq" + case .p_platformName_get: return "[get] .platformName" + case .p_agreement_get: return "[get] .agreement" + case .p_firebase_get: return "[get] .firebase" + case .p_facebook_get: return "[get] .facebook" + case .p_microsoft_get: return "[get] .microsoft" + case .p_google_get: return "[get] .google" + case .p_appleSignIn_get: return "[get] .appleSignIn" + case .p_features_get: return "[get] .features" + case .p_theme_get: return "[get] .theme" + case .p_uiComponents_get: return "[get] .uiComponents" + case .p_discovery_get: return "[get] .discovery" + case .p_dashboard_get: return "[get] .dashboard" + case .p_braze_get: return "[get] .braze" + case .p_branch_get: return "[get] .branch" + case .p_program_get: return "[get] .program" + case .p_URIScheme_get: return "[get] .URIScheme" } } } @@ -1279,42 +1659,118 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { super.init(products) } + public static func baseURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func baseSSOURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_baseSSOURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoFinishedURL(getter defaultValue: URL...) -> PropertyStub { + return Given(method: .p_ssoFinishedURL_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func ssoButtonTitle(getter defaultValue: [String: Any]...) -> PropertyStub { + return Given(method: .p_ssoButtonTitle_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func oAuthClientId(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_oAuthClientId_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func tokenType(getter defaultValue: TokenType...) -> PropertyStub { + return Given(method: .p_tokenType_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func feedbackEmail(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_feedbackEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appStoreLink(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_appStoreLink_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func faq(getter defaultValue: URL?...) -> PropertyStub { + return Given(method: .p_faq_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func platformName(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_platformName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func agreement(getter defaultValue: AgreementConfig...) -> PropertyStub { + return Given(method: .p_agreement_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func firebase(getter defaultValue: FirebaseConfig...) -> PropertyStub { + return Given(method: .p_firebase_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func facebook(getter defaultValue: FacebookConfig...) -> PropertyStub { + return Given(method: .p_facebook_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func microsoft(getter defaultValue: MicrosoftConfig...) -> PropertyStub { + return Given(method: .p_microsoft_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func google(getter defaultValue: GoogleConfig...) -> PropertyStub { + return Given(method: .p_google_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignIn(getter defaultValue: AppleSignInConfig...) -> PropertyStub { + return Given(method: .p_appleSignIn_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func features(getter defaultValue: FeaturesConfig...) -> PropertyStub { + return Given(method: .p_features_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func theme(getter defaultValue: ThemeConfig...) -> PropertyStub { + return Given(method: .p_theme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func uiComponents(getter defaultValue: UIComponentsConfig...) -> PropertyStub { + return Given(method: .p_uiComponents_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func discovery(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_discovery_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func dashboard(getter defaultValue: DashboardConfig...) -> PropertyStub { + return Given(method: .p_dashboard_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func braze(getter defaultValue: BrazeConfig...) -> PropertyStub { + return Given(method: .p_braze_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func branch(getter defaultValue: BranchConfig...) -> PropertyStub { + return Given(method: .p_branch_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func program(getter defaultValue: DiscoveryConfig...) -> PropertyStub { + return Given(method: .p_program_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func URIScheme(getter defaultValue: String...) -> PropertyStub { + return Given(method: .p_URIScheme_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } } public struct Verify { fileprivate var method: MethodType - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} - public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} - public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static var baseURL: Verify { return Verify(method: .p_baseURL_get) } + public static var baseSSOURL: Verify { return Verify(method: .p_baseSSOURL_get) } + public static var ssoFinishedURL: Verify { return Verify(method: .p_ssoFinishedURL_get) } + public static var ssoButtonTitle: Verify { return Verify(method: .p_ssoButtonTitle_get) } + public static var oAuthClientId: Verify { return Verify(method: .p_oAuthClientId_get) } + public static var tokenType: Verify { return Verify(method: .p_tokenType_get) } + public static var feedbackEmail: Verify { return Verify(method: .p_feedbackEmail_get) } + public static var appStoreLink: Verify { return Verify(method: .p_appStoreLink_get) } + public static var faq: Verify { return Verify(method: .p_faq_get) } + public static var platformName: Verify { return Verify(method: .p_platformName_get) } + public static var agreement: Verify { return Verify(method: .p_agreement_get) } + public static var firebase: Verify { return Verify(method: .p_firebase_get) } + public static var facebook: Verify { return Verify(method: .p_facebook_get) } + public static var microsoft: Verify { return Verify(method: .p_microsoft_get) } + public static var google: Verify { return Verify(method: .p_google_get) } + public static var appleSignIn: Verify { return Verify(method: .p_appleSignIn_get) } + public static var features: Verify { return Verify(method: .p_features_get) } + public static var theme: Verify { return Verify(method: .p_theme_get) } + public static var uiComponents: Verify { return Verify(method: .p_uiComponents_get) } + public static var discovery: Verify { return Verify(method: .p_discovery_get) } + public static var dashboard: Verify { return Verify(method: .p_dashboard_get) } + public static var braze: Verify { return Verify(method: .p_braze_get) } + public static var branch: Verify { return Verify(method: .p_branch_get) } + public static var program: Verify { return Verify(method: .p_program_get) } + public static var URIScheme: Verify { return Verify(method: .p_URIScheme_get) } } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) - } - public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) - } - public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { - return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) - } - public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { - return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) - } - public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { - return Perform(method: .m_trackEvent__event(`event`), performs: perform) - } - public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { - return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) - } } public func given(_ method: Given) { @@ -1390,9 +1846,9 @@ open class CoreAnalyticsMock: CoreAnalytics, Mock { } } -// MARK: - DownloadManagerProtocol +// MARK: - ConnectivityProtocol -open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { +open class ConnectivityProtocolMock: ConnectivityProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -1430,504 +1886,2738 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { if scopes.contains(.perform) { methodPerformValues = [] } } - public var currentDownloadTask: DownloadDataTask? { - get { invocations.append(.p_currentDownloadTask_get); return __p_currentDownloadTask ?? optionalGivenGetterValue(.p_currentDownloadTask_get, "DownloadManagerProtocolMock - stub value for currentDownloadTask was not defined") } + public var isInternetAvaliable: Bool { + get { invocations.append(.p_isInternetAvaliable_get); return __p_isInternetAvaliable ?? givenGetterValue(.p_isInternetAvaliable_get, "ConnectivityProtocolMock - stub value for isInternetAvaliable was not defined") } } - private var __p_currentDownloadTask: (DownloadDataTask)? + private var __p_isInternetAvaliable: (Bool)? + public var isMobileData: Bool { + get { invocations.append(.p_isMobileData_get); return __p_isMobileData ?? givenGetterValue(.p_isMobileData_get, "ConnectivityProtocolMock - stub value for isMobileData was not defined") } + } + private var __p_isMobileData: (Bool)? + public var internetReachableSubject: CurrentValueSubject { + get { invocations.append(.p_internetReachableSubject_get); return __p_internetReachableSubject ?? givenGetterValue(.p_internetReachableSubject_get, "ConnectivityProtocolMock - stub value for internetReachableSubject was not defined") } + } + private var __p_internetReachableSubject: (CurrentValueSubject)? - open func publisher() -> AnyPublisher { - addInvocation(.m_publisher) - let perform = methodPerformValue(.m_publisher) as? () -> Void - perform?() - var __value: AnyPublisher - do { - __value = try methodReturnValue(.m_publisher).casted() - } catch { - onFatalFailure("Stub return value not specified for publisher(). Use given") - Failure("Stub return value not specified for publisher(). Use given") - } - return __value - } - open func eventPublisher() -> AnyPublisher { - addInvocation(.m_eventPublisher) - let perform = methodPerformValue(.m_eventPublisher) as? () -> Void - perform?() - var __value: AnyPublisher - do { - __value = try methodReturnValue(.m_eventPublisher).casted() - } catch { - onFatalFailure("Stub return value not specified for eventPublisher(). Use given") - Failure("Stub return value not specified for eventPublisher(). Use given") - } - return __value - } - open func addToDownloadQueue(blocks: [CourseBlock]) throws { - addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) - do { - _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } - } - open func getDownloadTasks() -> [DownloadDataTask] { - addInvocation(.m_getDownloadTasks) - let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void - perform?() - var __value: [DownloadDataTask] - do { - __value = try methodReturnValue(.m_getDownloadTasks).casted() - } catch { - onFatalFailure("Stub return value not specified for getDownloadTasks(). Use given") - Failure("Stub return value not specified for getDownloadTasks(). Use given") - } - return __value + fileprivate enum MethodType { + case p_isInternetAvaliable_get + case p_isMobileData_get + case p_internetReachableSubject_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_isInternetAvaliable_get,.p_isInternetAvaliable_get): return Matcher.ComparisonResult.match + case (.p_isMobileData_get,.p_isMobileData_get): return Matcher.ComparisonResult.match + case (.p_internetReachableSubject_get,.p_internetReachableSubject_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_isInternetAvaliable_get: return 0 + case .p_isMobileData_get: return 0 + case .p_internetReachableSubject_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .p_isInternetAvaliable_get: return "[get] .isInternetAvaliable" + case .p_isMobileData_get: return "[get] .isMobileData" + case .p_internetReachableSubject_get: return "[get] .internetReachableSubject" + } + } } - open func getDownloadTasksForCourse(_ courseId: String) -> [DownloadDataTask] { - addInvocation(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) - let perform = methodPerformValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void - perform?(`courseId`) - var __value: [DownloadDataTask] - do { - __value = try methodReturnValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))).casted() - } catch { - onFatalFailure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") - Failure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") - } - return __value + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func isInternetAvaliable(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isInternetAvaliable_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func isMobileData(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_isMobileData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func internetReachableSubject(getter defaultValue: CurrentValueSubject...) -> PropertyStub { + return Given(method: .p_internetReachableSubject_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + } - open func cancelDownloading(courseId: String, blocks: [CourseBlock]) throws { - addInvocation(.m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter.value(`courseId`), Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter.value(`courseId`), Parameter<[CourseBlock]>.value(`blocks`))) as? (String, [CourseBlock]) -> Void - perform?(`courseId`, `blocks`) - do { - _ = try methodReturnValue(.m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter.value(`courseId`), Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } + public struct Verify { + fileprivate var method: MethodType + + public static var isInternetAvaliable: Verify { return Verify(method: .p_isInternetAvaliable_get) } + public static var isMobileData: Verify { return Verify(method: .p_isMobileData_get) } + public static var internetReachableSubject: Verify { return Verify(method: .p_internetReachableSubject_get) } } - open func cancelDownloading(task: DownloadDataTask) throws { - addInvocation(.m_cancelDownloading__task_task(Parameter.value(`task`))) - let perform = methodPerformValue(.m_cancelDownloading__task_task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void - perform?(`task`) - do { - _ = try methodReturnValue(.m_cancelDownloading__task_task(Parameter.value(`task`))).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } + public struct Perform { + fileprivate var method: MethodType + var performs: Any + } - open func cancelDownloading(courseId: String) throws { - addInvocation(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) - let perform = methodPerformValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) as? (String) -> Void - perform?(`courseId`) - do { - _ = try methodReturnValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreAnalytics + +open class CoreAnalyticsMock: CoreAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func trackEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventparameters_parameters(Parameter.value(`event`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, [String: Any]?) -> Void + perform?(`event`, `parameters`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue, parameters: [String: Any]?) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter.value(`event`), Parameter.value(`biValue`), Parameter<[String: Any]?>.value(`parameters`))) as? (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void + perform?(`event`, `biValue`, `parameters`) + } + + open func appreview(_ event: AnalyticsEvent, biValue: EventBIValue, action: String?, rating: Int?) { + addInvocation(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) + let perform = methodPerformValue(.m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter.value(`event`), Parameter.value(`biValue`), Parameter.value(`action`), Parameter.value(`rating`))) as? (AnalyticsEvent, EventBIValue, String?, Int?) -> Void + perform?(`event`, `biValue`, `action`, `rating`) + } + + open func videoQualityChanged(_ event: AnalyticsEvent, bivalue: EventBIValue, value: String, oldValue: String) { + addInvocation(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) + let perform = methodPerformValue(.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter.value(`event`), Parameter.value(`bivalue`), Parameter.value(`value`), Parameter.value(`oldValue`))) as? (AnalyticsEvent, EventBIValue, String, String) -> Void + perform?(`event`, `bivalue`, `value`, `oldValue`) + } + + open func trackEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent) { + addInvocation(.m_trackScreenEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_trackScreenEvent__event(Parameter.value(`event`))) as? (AnalyticsEvent) -> Void + perform?(`event`) + } + + open func trackScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_trackScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + + fileprivate enum MethodType { + case m_trackEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventparameters_parameters(Parameter, Parameter<[String: Any]?>) + case m_trackScreenEvent__eventbiValue_biValueparameters_parameters(Parameter, Parameter, Parameter<[String: Any]?>) + case m_appreview__eventbiValue_biValueaction_actionrating_rating(Parameter, Parameter, Parameter, Parameter) + case m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(Parameter, Parameter, Parameter, Parameter) + case m_trackEvent__event(Parameter) + case m_trackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_trackScreenEvent__event(Parameter) + case m_trackScreenEvent__eventbiValue_biValue(Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_trackEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventparameters_parameters(let lhsEvent, let lhsParameters), .m_trackScreenEvent__eventparameters_parameters(let rhsEvent, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let lhsEvent, let lhsBivalue, let lhsParameters), .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(let rhsEvent, let rhsBivalue, let rhsParameters)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + return Matcher.ComparisonResult(results) + + case (.m_appreview__eventbiValue_biValueaction_actionrating_rating(let lhsEvent, let lhsBivalue, let lhsAction, let lhsRating), .m_appreview__eventbiValue_biValueaction_actionrating_rating(let rhsEvent, let rhsBivalue, let rhsAction, let rhsRating)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsRating, rhs: rhsRating, with: matcher), lhsRating, rhsRating, "rating")) + return Matcher.ComparisonResult(results) + + case (.m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let lhsEvent, let lhsBivalue, let lhsValue, let lhsOldvalue), .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(let rhsEvent, let rhsBivalue, let rhsValue, let rhsOldvalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "bivalue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsValue, rhs: rhsValue, with: matcher), lhsValue, rhsValue, "value")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsOldvalue, rhs: rhsOldvalue, with: matcher), lhsOldvalue, rhsOldvalue, "oldValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__event(let lhsEvent), .m_trackEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__event(let lhsEvent), .m_trackScreenEvent__event(let rhsEvent)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + return Matcher.ComparisonResult(results) + + case (.m_trackScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_trackScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_trackEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_trackScreenEvent__eventparameters_parameters(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_appreview__eventbiValue_biValueaction_actionrating_rating(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(p0, p1, p2, p3): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + case let .m_trackEvent__event(p0): return p0.intValue + case let .m_trackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_trackScreenEvent__event(p0): return p0.intValue + case let .m_trackScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + } + } + func assertionName() -> String { + switch self { + case .m_trackEvent__eventparameters_parameters: return ".trackEvent(_:parameters:)" + case .m_trackEvent__eventbiValue_biValueparameters_parameters: return ".trackEvent(_:biValue:parameters:)" + case .m_trackScreenEvent__eventparameters_parameters: return ".trackScreenEvent(_:parameters:)" + case .m_trackScreenEvent__eventbiValue_biValueparameters_parameters: return ".trackScreenEvent(_:biValue:parameters:)" + case .m_appreview__eventbiValue_biValueaction_actionrating_rating: return ".appreview(_:biValue:action:rating:)" + case .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue: return ".videoQualityChanged(_:bivalue:value:oldValue:)" + case .m_trackEvent__event: return ".trackEvent(_:)" + case .m_trackEvent__eventbiValue_biValue: return ".trackEvent(_:biValue:)" + case .m_trackScreenEvent__event: return ".trackScreenEvent(_:)" + case .m_trackScreenEvent__eventbiValue_biValue: return ".trackScreenEvent(_:biValue:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`))} + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter) -> Verify { return Verify(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`))} + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter) -> Verify { return Verify(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`))} + public static func trackEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackEvent__event(`event`))} + public static func trackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func trackScreenEvent(_ event: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__event(`event`))} + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func trackEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventparameters_parameters(`event`, `parameters`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, parameters: Parameter<[String: Any]?>, perform: @escaping (AnalyticsEvent, EventBIValue, [String: Any]?) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValueparameters_parameters(`event`, `biValue`, `parameters`), performs: perform) + } + public static func appreview(_ event: Parameter, biValue: Parameter, action: Parameter, rating: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String?, Int?) -> Void) -> Perform { + return Perform(method: .m_appreview__eventbiValue_biValueaction_actionrating_rating(`event`, `biValue`, `action`, `rating`), performs: perform) + } + public static func videoQualityChanged(_ event: Parameter, bivalue: Parameter, value: Parameter, oldValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue, String, String) -> Void) -> Perform { + return Perform(method: .m_videoQualityChanged__eventbivalue_bivaluevalue_valueoldValue_oldValue(`event`, `bivalue`, `value`, `oldValue`), performs: perform) + } + public static func trackEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackEvent__event(`event`), performs: perform) + } + public static func trackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, perform: @escaping (AnalyticsEvent) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__event(`event`), performs: perform) + } + public static func trackScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_trackScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CorePersistenceProtocol + +open class CorePersistenceProtocolMock: CorePersistenceProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func set(userId: Int) { + addInvocation(.m_set__userId_userId(Parameter.value(`userId`))) + let perform = methodPerformValue(.m_set__userId_userId(Parameter.value(`userId`))) as? (Int) -> Void + perform?(`userId`) + } + + open func getUserID() -> Int? { + addInvocation(.m_getUserID) + let perform = methodPerformValue(.m_getUserID) as? () -> Void + perform?() + var __value: Int? = nil + do { + __value = try methodReturnValue(.m_getUserID).casted() + } catch { + // do nothing + } + return __value + } + + open func publisher() -> AnyPublisher { + addInvocation(.m_publisher) + let perform = methodPerformValue(.m_publisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_publisher).casted() + } catch { + onFatalFailure("Stub return value not specified for publisher(). Use given") + Failure("Stub return value not specified for publisher(). Use given") + } + return __value + } + + open func addToDownloadQueue(tasks: [DownloadDataTask]) { + addInvocation(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>.value(`tasks`))) as? ([DownloadDataTask]) -> Void + perform?(`tasks`) + } + + open func saveOfflineProgress(progress: OfflineProgress) { + addInvocation(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) + let perform = methodPerformValue(.m_saveOfflineProgress__progress_progress(Parameter.value(`progress`))) as? (OfflineProgress) -> Void + perform?(`progress`) + } + + open func loadProgress(for blockID: String) -> OfflineProgress? { + addInvocation(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + var __value: OfflineProgress? = nil + do { + __value = try methodReturnValue(.m_loadProgress__for_blockID(Parameter.value(`blockID`))).casted() + } catch { + // do nothing + } + return __value + } + + open func loadAllOfflineProgress() -> [OfflineProgress] { + addInvocation(.m_loadAllOfflineProgress) + let perform = methodPerformValue(.m_loadAllOfflineProgress) as? () -> Void + perform?() + var __value: [OfflineProgress] + do { + __value = try methodReturnValue(.m_loadAllOfflineProgress).casted() + } catch { + onFatalFailure("Stub return value not specified for loadAllOfflineProgress(). Use given") + Failure("Stub return value not specified for loadAllOfflineProgress(). Use given") + } + return __value + } + + open func deleteProgress(for blockID: String) { + addInvocation(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) + let perform = methodPerformValue(.m_deleteProgress__for_blockID(Parameter.value(`blockID`))) as? (String) -> Void + perform?(`blockID`) + } + + open func deleteAllProgress() { + addInvocation(.m_deleteAllProgress) + let perform = methodPerformValue(.m_deleteAllProgress) as? () -> Void + perform?() + } + + open func addToDownloadQueue(blocks: [CourseBlock], downloadQuality: DownloadQuality) { + addInvocation(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>.value(`blocks`), Parameter.value(`downloadQuality`))) as? ([CourseBlock], DownloadQuality) -> Void + perform?(`blocks`, `downloadQuality`) + } + + open func nextBlockForDownloading() -> DownloadDataTask? { + addInvocation(.m_nextBlockForDownloading) + let perform = methodPerformValue(.m_nextBlockForDownloading) as? () -> Void + perform?() + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_nextBlockForDownloading).casted() + } catch { + // do nothing + } + return __value + } + + open func updateDownloadState(id: String, state: DownloadState, resumeData: Data?) { + addInvocation(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) + let perform = methodPerformValue(.m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter.value(`id`), Parameter.value(`state`), Parameter.value(`resumeData`))) as? (String, DownloadState, Data?) -> Void + perform?(`id`, `state`, `resumeData`) + } + + open func deleteDownloadDataTask(id: String) throws { + addInvocation(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) + let perform = methodPerformValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))) as? (String) -> Void + perform?(`id`) + do { + _ = try methodReturnValue(.m_deleteDownloadDataTask__id_id(Parameter.value(`id`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func saveDownloadDataTask(_ task: DownloadDataTask) { + addInvocation(.m_saveDownloadDataTask__task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_saveDownloadDataTask__task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) + } + + open func downloadDataTask(for blockId: String) -> DownloadDataTask? { + addInvocation(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) + let perform = methodPerformValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void + perform?(`blockId`) + var __value: DownloadDataTask? = nil + do { + __value = try methodReturnValue(.m_downloadDataTask__for_blockId(Parameter.value(`blockId`))).casted() + } catch { + // do nothing + } + return __value + } + + open func getDownloadDataTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasks) + let perform = methodPerformValue(.m_getDownloadDataTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasks(). Use given") + Failure("Stub return value not specified for getDownloadDataTasks(). Use given") + } + return __value + } + + open func getDownloadDataTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadDataTasksForCourse__courseId(Parameter.value(`courseId`))).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadDataTasksForCourse(_ courseId: String). Use given") + } + return __value + } + + + fileprivate enum MethodType { + case m_set__userId_userId(Parameter) + case m_getUserID + case m_publisher + case m_addToDownloadQueue__tasks_tasks(Parameter<[DownloadDataTask]>) + case m_saveOfflineProgress__progress_progress(Parameter) + case m_loadProgress__for_blockID(Parameter) + case m_loadAllOfflineProgress + case m_deleteProgress__for_blockID(Parameter) + case m_deleteAllProgress + case m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(Parameter<[CourseBlock]>, Parameter) + case m_nextBlockForDownloading + case m_updateDownloadState__id_idstate_stateresumeData_resumeData(Parameter, Parameter, Parameter) + case m_deleteDownloadDataTask__id_id(Parameter) + case m_saveDownloadDataTask__task(Parameter) + case m_downloadDataTask__for_blockId(Parameter) + case m_getDownloadDataTasks + case m_getDownloadDataTasksForCourse__courseId(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_set__userId_userId(let lhsUserid), .m_set__userId_userId(let rhsUserid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUserid, rhs: rhsUserid, with: matcher), lhsUserid, rhsUserid, "userId")) + return Matcher.ComparisonResult(results) + + case (.m_getUserID, .m_getUserID): return .match + + case (.m_publisher, .m_publisher): return .match + + case (.m_addToDownloadQueue__tasks_tasks(let lhsTasks), .m_addToDownloadQueue__tasks_tasks(let rhsTasks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTasks, rhs: rhsTasks, with: matcher), lhsTasks, rhsTasks, "tasks")) + return Matcher.ComparisonResult(results) + + case (.m_saveOfflineProgress__progress_progress(let lhsProgress), .m_saveOfflineProgress__progress_progress(let rhsProgress)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsProgress, rhs: rhsProgress, with: matcher), lhsProgress, rhsProgress, "progress")) + return Matcher.ComparisonResult(results) + + case (.m_loadProgress__for_blockID(let lhsBlockid), .m_loadProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_loadAllOfflineProgress, .m_loadAllOfflineProgress): return .match + + case (.m_deleteProgress__for_blockID(let lhsBlockid), .m_deleteProgress__for_blockID(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockID")) + return Matcher.ComparisonResult(results) + + case (.m_deleteAllProgress, .m_deleteAllProgress): return .match + + case (.m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let lhsBlocks, let lhsDownloadquality), .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(let rhsBlocks, let rhsDownloadquality)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDownloadquality, rhs: rhsDownloadquality, with: matcher), lhsDownloadquality, rhsDownloadquality, "downloadQuality")) + return Matcher.ComparisonResult(results) + + case (.m_nextBlockForDownloading, .m_nextBlockForDownloading): return .match + + case (.m_updateDownloadState__id_idstate_stateresumeData_resumeData(let lhsId, let lhsState, let lhsResumedata), .m_updateDownloadState__id_idstate_stateresumeData_resumeData(let rhsId, let rhsState, let rhsResumedata)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsState, rhs: rhsState, with: matcher), lhsState, rhsState, "state")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsResumedata, rhs: rhsResumedata, with: matcher), lhsResumedata, rhsResumedata, "resumeData")) + return Matcher.ComparisonResult(results) + + case (.m_deleteDownloadDataTask__id_id(let lhsId), .m_deleteDownloadDataTask__id_id(let rhsId)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsId, rhs: rhsId, with: matcher), lhsId, rhsId, "id")) + return Matcher.ComparisonResult(results) + + case (.m_saveDownloadDataTask__task(let lhsTask), .m_saveDownloadDataTask__task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "_ task")) + return Matcher.ComparisonResult(results) + + case (.m_downloadDataTask__for_blockId(let lhsBlockid), .m_downloadDataTask__for_blockId(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) + return Matcher.ComparisonResult(results) + + case (.m_getDownloadDataTasks, .m_getDownloadDataTasks): return .match + + case (.m_getDownloadDataTasksForCourse__courseId(let lhsCourseid), .m_getDownloadDataTasksForCourse__courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) + return Matcher.ComparisonResult(results) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case let .m_set__userId_userId(p0): return p0.intValue + case .m_getUserID: return 0 + case .m_publisher: return 0 + case let .m_addToDownloadQueue__tasks_tasks(p0): return p0.intValue + case let .m_saveOfflineProgress__progress_progress(p0): return p0.intValue + case let .m_loadProgress__for_blockID(p0): return p0.intValue + case .m_loadAllOfflineProgress: return 0 + case let .m_deleteProgress__for_blockID(p0): return p0.intValue + case .m_deleteAllProgress: return 0 + case let .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(p0, p1): return p0.intValue + p1.intValue + case .m_nextBlockForDownloading: return 0 + case let .m_updateDownloadState__id_idstate_stateresumeData_resumeData(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + case let .m_deleteDownloadDataTask__id_id(p0): return p0.intValue + case let .m_saveDownloadDataTask__task(p0): return p0.intValue + case let .m_downloadDataTask__for_blockId(p0): return p0.intValue + case .m_getDownloadDataTasks: return 0 + case let .m_getDownloadDataTasksForCourse__courseId(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_set__userId_userId: return ".set(userId:)" + case .m_getUserID: return ".getUserID()" + case .m_publisher: return ".publisher()" + case .m_addToDownloadQueue__tasks_tasks: return ".addToDownloadQueue(tasks:)" + case .m_saveOfflineProgress__progress_progress: return ".saveOfflineProgress(progress:)" + case .m_loadProgress__for_blockID: return ".loadProgress(for:)" + case .m_loadAllOfflineProgress: return ".loadAllOfflineProgress()" + case .m_deleteProgress__for_blockID: return ".deleteProgress(for:)" + case .m_deleteAllProgress: return ".deleteAllProgress()" + case .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality: return ".addToDownloadQueue(blocks:downloadQuality:)" + case .m_nextBlockForDownloading: return ".nextBlockForDownloading()" + case .m_updateDownloadState__id_idstate_stateresumeData_resumeData: return ".updateDownloadState(id:state:resumeData:)" + case .m_deleteDownloadDataTask__id_id: return ".deleteDownloadDataTask(id:)" + case .m_saveDownloadDataTask__task: return ".saveDownloadDataTask(_:)" + case .m_downloadDataTask__for_blockId: return ".downloadDataTask(for:)" + case .m_getDownloadDataTasks: return ".getDownloadDataTasks()" + case .m_getDownloadDataTasksForCourse__courseId: return ".getDownloadDataTasksForCourse(_:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func getUserID(willReturn: Int?...) -> MethodStub { + return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func publisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadProgress(for blockID: Parameter, willReturn: OfflineProgress?...) -> MethodStub { + return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func loadAllOfflineProgress(willReturn: [OfflineProgress]...) -> MethodStub { + return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func nextBlockForDownloading(willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func downloadDataTask(for blockId: Parameter, willReturn: DownloadDataTask?...) -> MethodStub { + return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getUserID(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Int?] = [] + let given: Given = { return Given(method: .m_getUserID, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Int?).self) + willProduce(stubber) + return given + } + public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func loadProgress(for blockID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [OfflineProgress?] = [] + let given: Given = { return Given(method: .m_loadProgress__for_blockID(`blockID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (OfflineProgress?).self) + willProduce(stubber) + return given + } + public static func loadAllOfflineProgress(willProduce: (Stubber<[OfflineProgress]>) -> Void) -> MethodStub { + let willReturn: [[OfflineProgress]] = [] + let given: Given = { return Given(method: .m_loadAllOfflineProgress, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([OfflineProgress]).self) + willProduce(stubber) + return given + } + public static func nextBlockForDownloading(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_nextBlockForDownloading, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func downloadDataTask(for blockId: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [DownloadDataTask?] = [] + let given: Given = { return Given(method: .m_downloadDataTask__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (DownloadDataTask?).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func deleteDownloadDataTask(id: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func deleteDownloadDataTask(id: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_deleteDownloadDataTask__id_id(`id`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func set(userId: Parameter) -> Verify { return Verify(method: .m_set__userId_userId(`userId`))} + public static func getUserID() -> Verify { return Verify(method: .m_getUserID)} + public static func publisher() -> Verify { return Verify(method: .m_publisher)} + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>) -> Verify { return Verify(method: .m_addToDownloadQueue__tasks_tasks(`tasks`))} + public static func saveOfflineProgress(progress: Parameter) -> Verify { return Verify(method: .m_saveOfflineProgress__progress_progress(`progress`))} + public static func loadProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_loadProgress__for_blockID(`blockID`))} + public static func loadAllOfflineProgress() -> Verify { return Verify(method: .m_loadAllOfflineProgress)} + public static func deleteProgress(for blockID: Parameter) -> Verify { return Verify(method: .m_deleteProgress__for_blockID(`blockID`))} + public static func deleteAllProgress() -> Verify { return Verify(method: .m_deleteAllProgress)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`))} + public static func nextBlockForDownloading() -> Verify { return Verify(method: .m_nextBlockForDownloading)} + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter) -> Verify { return Verify(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`))} + public static func deleteDownloadDataTask(id: Parameter) -> Verify { return Verify(method: .m_deleteDownloadDataTask__id_id(`id`))} + public static func saveDownloadDataTask(_ task: Parameter) -> Verify { return Verify(method: .m_saveDownloadDataTask__task(`task`))} + public static func downloadDataTask(for blockId: Parameter) -> Verify { return Verify(method: .m_downloadDataTask__for_blockId(`blockId`))} + public static func getDownloadDataTasks() -> Verify { return Verify(method: .m_getDownloadDataTasks)} + public static func getDownloadDataTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func set(userId: Parameter, perform: @escaping (Int) -> Void) -> Perform { + return Perform(method: .m_set__userId_userId(`userId`), performs: perform) + } + public static func getUserID(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getUserID, performs: perform) + } + public static func publisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_publisher, performs: perform) + } + public static func addToDownloadQueue(tasks: Parameter<[DownloadDataTask]>, perform: @escaping ([DownloadDataTask]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__tasks_tasks(`tasks`), performs: perform) + } + public static func saveOfflineProgress(progress: Parameter, perform: @escaping (OfflineProgress) -> Void) -> Perform { + return Perform(method: .m_saveOfflineProgress__progress_progress(`progress`), performs: perform) + } + public static func loadProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_loadProgress__for_blockID(`blockID`), performs: perform) + } + public static func loadAllOfflineProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_loadAllOfflineProgress, performs: perform) + } + public static func deleteProgress(for blockID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteProgress__for_blockID(`blockID`), performs: perform) + } + public static func deleteAllProgress(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteAllProgress, performs: perform) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, downloadQuality: Parameter, perform: @escaping ([CourseBlock], DownloadQuality) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocksdownloadQuality_downloadQuality(`blocks`, `downloadQuality`), performs: perform) + } + public static func nextBlockForDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_nextBlockForDownloading, performs: perform) + } + public static func updateDownloadState(id: Parameter, state: Parameter, resumeData: Parameter, perform: @escaping (String, DownloadState, Data?) -> Void) -> Perform { + return Perform(method: .m_updateDownloadState__id_idstate_stateresumeData_resumeData(`id`, `state`, `resumeData`), performs: perform) + } + public static func deleteDownloadDataTask(id: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteDownloadDataTask__id_id(`id`), performs: perform) + } + public static func saveDownloadDataTask(_ task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_saveDownloadDataTask__task(`task`), performs: perform) + } + public static func downloadDataTask(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_downloadDataTask__for_blockId(`blockId`), performs: perform) + } + public static func getDownloadDataTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasks, performs: perform) + } + public static func getDownloadDataTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadDataTasksForCourse__courseId(`courseId`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - CoreStorage + +open class CoreStorageMock: CoreStorage, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var accessToken: String? { + get { invocations.append(.p_accessToken_get); return __p_accessToken ?? optionalGivenGetterValue(.p_accessToken_get, "CoreStorageMock - stub value for accessToken was not defined") } + set { invocations.append(.p_accessToken_set(.value(newValue))); __p_accessToken = newValue } + } + private var __p_accessToken: (String)? + + public var refreshToken: String? { + get { invocations.append(.p_refreshToken_get); return __p_refreshToken ?? optionalGivenGetterValue(.p_refreshToken_get, "CoreStorageMock - stub value for refreshToken was not defined") } + set { invocations.append(.p_refreshToken_set(.value(newValue))); __p_refreshToken = newValue } + } + private var __p_refreshToken: (String)? + + public var pushToken: String? { + get { invocations.append(.p_pushToken_get); return __p_pushToken ?? optionalGivenGetterValue(.p_pushToken_get, "CoreStorageMock - stub value for pushToken was not defined") } + set { invocations.append(.p_pushToken_set(.value(newValue))); __p_pushToken = newValue } + } + private var __p_pushToken: (String)? + + public var appleSignFullName: String? { + get { invocations.append(.p_appleSignFullName_get); return __p_appleSignFullName ?? optionalGivenGetterValue(.p_appleSignFullName_get, "CoreStorageMock - stub value for appleSignFullName was not defined") } + set { invocations.append(.p_appleSignFullName_set(.value(newValue))); __p_appleSignFullName = newValue } + } + private var __p_appleSignFullName: (String)? + + public var appleSignEmail: String? { + get { invocations.append(.p_appleSignEmail_get); return __p_appleSignEmail ?? optionalGivenGetterValue(.p_appleSignEmail_get, "CoreStorageMock - stub value for appleSignEmail was not defined") } + set { invocations.append(.p_appleSignEmail_set(.value(newValue))); __p_appleSignEmail = newValue } + } + private var __p_appleSignEmail: (String)? + + public var cookiesDate: Date? { + get { invocations.append(.p_cookiesDate_get); return __p_cookiesDate ?? optionalGivenGetterValue(.p_cookiesDate_get, "CoreStorageMock - stub value for cookiesDate was not defined") } + set { invocations.append(.p_cookiesDate_set(.value(newValue))); __p_cookiesDate = newValue } + } + private var __p_cookiesDate: (Date)? + + public var reviewLastShownVersion: String? { + get { invocations.append(.p_reviewLastShownVersion_get); return __p_reviewLastShownVersion ?? optionalGivenGetterValue(.p_reviewLastShownVersion_get, "CoreStorageMock - stub value for reviewLastShownVersion was not defined") } + set { invocations.append(.p_reviewLastShownVersion_set(.value(newValue))); __p_reviewLastShownVersion = newValue } + } + private var __p_reviewLastShownVersion: (String)? + + public var lastReviewDate: Date? { + get { invocations.append(.p_lastReviewDate_get); return __p_lastReviewDate ?? optionalGivenGetterValue(.p_lastReviewDate_get, "CoreStorageMock - stub value for lastReviewDate was not defined") } + set { invocations.append(.p_lastReviewDate_set(.value(newValue))); __p_lastReviewDate = newValue } + } + private var __p_lastReviewDate: (Date)? + + public var user: DataLayer.User? { + get { invocations.append(.p_user_get); return __p_user ?? optionalGivenGetterValue(.p_user_get, "CoreStorageMock - stub value for user was not defined") } + set { invocations.append(.p_user_set(.value(newValue))); __p_user = newValue } + } + private var __p_user: (DataLayer.User)? + + public var userSettings: UserSettings? { + get { invocations.append(.p_userSettings_get); return __p_userSettings ?? optionalGivenGetterValue(.p_userSettings_get, "CoreStorageMock - stub value for userSettings was not defined") } + set { invocations.append(.p_userSettings_set(.value(newValue))); __p_userSettings = newValue } + } + private var __p_userSettings: (UserSettings)? + + public var resetAppSupportDirectoryUserData: Bool? { + get { invocations.append(.p_resetAppSupportDirectoryUserData_get); return __p_resetAppSupportDirectoryUserData ?? optionalGivenGetterValue(.p_resetAppSupportDirectoryUserData_get, "CoreStorageMock - stub value for resetAppSupportDirectoryUserData was not defined") } + set { invocations.append(.p_resetAppSupportDirectoryUserData_set(.value(newValue))); __p_resetAppSupportDirectoryUserData = newValue } + } + private var __p_resetAppSupportDirectoryUserData: (Bool)? + + public var useRelativeDates: Bool { + get { invocations.append(.p_useRelativeDates_get); return __p_useRelativeDates ?? givenGetterValue(.p_useRelativeDates_get, "CoreStorageMock - stub value for useRelativeDates was not defined") } + set { invocations.append(.p_useRelativeDates_set(.value(newValue))); __p_useRelativeDates = newValue } + } + private var __p_useRelativeDates: (Bool)? + + + + + + open func clear() { + addInvocation(.m_clear) + let perform = methodPerformValue(.m_clear) as? () -> Void + perform?() + } + + + fileprivate enum MethodType { + case m_clear + case p_accessToken_get + case p_accessToken_set(Parameter) + case p_refreshToken_get + case p_refreshToken_set(Parameter) + case p_pushToken_get + case p_pushToken_set(Parameter) + case p_appleSignFullName_get + case p_appleSignFullName_set(Parameter) + case p_appleSignEmail_get + case p_appleSignEmail_set(Parameter) + case p_cookiesDate_get + case p_cookiesDate_set(Parameter) + case p_reviewLastShownVersion_get + case p_reviewLastShownVersion_set(Parameter) + case p_lastReviewDate_get + case p_lastReviewDate_set(Parameter) + case p_user_get + case p_user_set(Parameter) + case p_userSettings_get + case p_userSettings_set(Parameter) + case p_resetAppSupportDirectoryUserData_get + case p_resetAppSupportDirectoryUserData_set(Parameter) + case p_useRelativeDates_get + case p_useRelativeDates_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_clear, .m_clear): return .match + case (.p_accessToken_get,.p_accessToken_get): return Matcher.ComparisonResult.match + case (.p_accessToken_set(let left),.p_accessToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_refreshToken_get,.p_refreshToken_get): return Matcher.ComparisonResult.match + case (.p_refreshToken_set(let left),.p_refreshToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_pushToken_get,.p_pushToken_get): return Matcher.ComparisonResult.match + case (.p_pushToken_set(let left),.p_pushToken_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignFullName_get,.p_appleSignFullName_get): return Matcher.ComparisonResult.match + case (.p_appleSignFullName_set(let left),.p_appleSignFullName_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_appleSignEmail_get,.p_appleSignEmail_get): return Matcher.ComparisonResult.match + case (.p_appleSignEmail_set(let left),.p_appleSignEmail_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_cookiesDate_get,.p_cookiesDate_get): return Matcher.ComparisonResult.match + case (.p_cookiesDate_set(let left),.p_cookiesDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_reviewLastShownVersion_get,.p_reviewLastShownVersion_get): return Matcher.ComparisonResult.match + case (.p_reviewLastShownVersion_set(let left),.p_reviewLastShownVersion_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_lastReviewDate_get,.p_lastReviewDate_get): return Matcher.ComparisonResult.match + case (.p_lastReviewDate_set(let left),.p_lastReviewDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_user_get,.p_user_get): return Matcher.ComparisonResult.match + case (.p_user_set(let left),.p_user_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_userSettings_get,.p_userSettings_get): return Matcher.ComparisonResult.match + case (.p_userSettings_set(let left),.p_userSettings_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_resetAppSupportDirectoryUserData_get,.p_resetAppSupportDirectoryUserData_get): return Matcher.ComparisonResult.match + case (.p_resetAppSupportDirectoryUserData_set(let left),.p_resetAppSupportDirectoryUserData_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_useRelativeDates_get,.p_useRelativeDates_get): return Matcher.ComparisonResult.match + case (.p_useRelativeDates_set(let left),.p_useRelativeDates_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .m_clear: return 0 + case .p_accessToken_get: return 0 + case .p_accessToken_set(let newValue): return newValue.intValue + case .p_refreshToken_get: return 0 + case .p_refreshToken_set(let newValue): return newValue.intValue + case .p_pushToken_get: return 0 + case .p_pushToken_set(let newValue): return newValue.intValue + case .p_appleSignFullName_get: return 0 + case .p_appleSignFullName_set(let newValue): return newValue.intValue + case .p_appleSignEmail_get: return 0 + case .p_appleSignEmail_set(let newValue): return newValue.intValue + case .p_cookiesDate_get: return 0 + case .p_cookiesDate_set(let newValue): return newValue.intValue + case .p_reviewLastShownVersion_get: return 0 + case .p_reviewLastShownVersion_set(let newValue): return newValue.intValue + case .p_lastReviewDate_get: return 0 + case .p_lastReviewDate_set(let newValue): return newValue.intValue + case .p_user_get: return 0 + case .p_user_set(let newValue): return newValue.intValue + case .p_userSettings_get: return 0 + case .p_userSettings_set(let newValue): return newValue.intValue + case .p_resetAppSupportDirectoryUserData_get: return 0 + case .p_resetAppSupportDirectoryUserData_set(let newValue): return newValue.intValue + case .p_useRelativeDates_get: return 0 + case .p_useRelativeDates_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .m_clear: return ".clear()" + case .p_accessToken_get: return "[get] .accessToken" + case .p_accessToken_set: return "[set] .accessToken" + case .p_refreshToken_get: return "[get] .refreshToken" + case .p_refreshToken_set: return "[set] .refreshToken" + case .p_pushToken_get: return "[get] .pushToken" + case .p_pushToken_set: return "[set] .pushToken" + case .p_appleSignFullName_get: return "[get] .appleSignFullName" + case .p_appleSignFullName_set: return "[set] .appleSignFullName" + case .p_appleSignEmail_get: return "[get] .appleSignEmail" + case .p_appleSignEmail_set: return "[set] .appleSignEmail" + case .p_cookiesDate_get: return "[get] .cookiesDate" + case .p_cookiesDate_set: return "[set] .cookiesDate" + case .p_reviewLastShownVersion_get: return "[get] .reviewLastShownVersion" + case .p_reviewLastShownVersion_set: return "[set] .reviewLastShownVersion" + case .p_lastReviewDate_get: return "[get] .lastReviewDate" + case .p_lastReviewDate_set: return "[set] .lastReviewDate" + case .p_user_get: return "[get] .user" + case .p_user_set: return "[set] .user" + case .p_userSettings_get: return "[get] .userSettings" + case .p_userSettings_set: return "[set] .userSettings" + case .p_resetAppSupportDirectoryUserData_get: return "[get] .resetAppSupportDirectoryUserData" + case .p_resetAppSupportDirectoryUserData_set: return "[set] .resetAppSupportDirectoryUserData" + case .p_useRelativeDates_get: return "[get] .useRelativeDates" + case .p_useRelativeDates_set: return "[set] .useRelativeDates" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func accessToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_accessToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func refreshToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_refreshToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func pushToken(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_pushToken_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignFullName(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignFullName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func appleSignEmail(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_appleSignEmail_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func cookiesDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_cookiesDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func reviewLastShownVersion(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_reviewLastShownVersion_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func lastReviewDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_lastReviewDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func user(getter defaultValue: DataLayer.User?...) -> PropertyStub { + return Given(method: .p_user_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func userSettings(getter defaultValue: UserSettings?...) -> PropertyStub { + return Given(method: .p_userSettings_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func resetAppSupportDirectoryUserData(getter defaultValue: Bool?...) -> PropertyStub { + return Given(method: .p_resetAppSupportDirectoryUserData_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func useRelativeDates(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_useRelativeDates_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static func clear() -> Verify { return Verify(method: .m_clear)} + public static var accessToken: Verify { return Verify(method: .p_accessToken_get) } + public static func accessToken(set newValue: Parameter) -> Verify { return Verify(method: .p_accessToken_set(newValue)) } + public static var refreshToken: Verify { return Verify(method: .p_refreshToken_get) } + public static func refreshToken(set newValue: Parameter) -> Verify { return Verify(method: .p_refreshToken_set(newValue)) } + public static var pushToken: Verify { return Verify(method: .p_pushToken_get) } + public static func pushToken(set newValue: Parameter) -> Verify { return Verify(method: .p_pushToken_set(newValue)) } + public static var appleSignFullName: Verify { return Verify(method: .p_appleSignFullName_get) } + public static func appleSignFullName(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignFullName_set(newValue)) } + public static var appleSignEmail: Verify { return Verify(method: .p_appleSignEmail_get) } + public static func appleSignEmail(set newValue: Parameter) -> Verify { return Verify(method: .p_appleSignEmail_set(newValue)) } + public static var cookiesDate: Verify { return Verify(method: .p_cookiesDate_get) } + public static func cookiesDate(set newValue: Parameter) -> Verify { return Verify(method: .p_cookiesDate_set(newValue)) } + public static var reviewLastShownVersion: Verify { return Verify(method: .p_reviewLastShownVersion_get) } + public static func reviewLastShownVersion(set newValue: Parameter) -> Verify { return Verify(method: .p_reviewLastShownVersion_set(newValue)) } + public static var lastReviewDate: Verify { return Verify(method: .p_lastReviewDate_get) } + public static func lastReviewDate(set newValue: Parameter) -> Verify { return Verify(method: .p_lastReviewDate_set(newValue)) } + public static var user: Verify { return Verify(method: .p_user_get) } + public static func user(set newValue: Parameter) -> Verify { return Verify(method: .p_user_set(newValue)) } + public static var userSettings: Verify { return Verify(method: .p_userSettings_get) } + public static func userSettings(set newValue: Parameter) -> Verify { return Verify(method: .p_userSettings_set(newValue)) } + public static var resetAppSupportDirectoryUserData: Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_get) } + public static func resetAppSupportDirectoryUserData(set newValue: Parameter) -> Verify { return Verify(method: .p_resetAppSupportDirectoryUserData_set(newValue)) } + public static var useRelativeDates: Verify { return Verify(method: .p_useRelativeDates_get) } + public static func useRelativeDates(set newValue: Parameter) -> Verify { return Verify(method: .p_useRelativeDates_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func clear(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_clear, performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - DownloadManagerProtocol + +open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var currentDownloadTask: DownloadDataTask? { + get { invocations.append(.p_currentDownloadTask_get); return __p_currentDownloadTask ?? optionalGivenGetterValue(.p_currentDownloadTask_get, "DownloadManagerProtocolMock - stub value for currentDownloadTask was not defined") } + } + private var __p_currentDownloadTask: (DownloadDataTask)? + + + + + + open func publisher() -> AnyPublisher { + addInvocation(.m_publisher) + let perform = methodPerformValue(.m_publisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_publisher).casted() + } catch { + onFatalFailure("Stub return value not specified for publisher(). Use given") + Failure("Stub return value not specified for publisher(). Use given") + } + return __value + } + + open func eventPublisher() -> AnyPublisher { + addInvocation(.m_eventPublisher) + let perform = methodPerformValue(.m_eventPublisher) as? () -> Void + perform?() + var __value: AnyPublisher + do { + __value = try methodReturnValue(.m_eventPublisher).casted() + } catch { + onFatalFailure("Stub return value not specified for eventPublisher(). Use given") + Failure("Stub return value not specified for eventPublisher(). Use given") + } + return __value + } + + open func addToDownloadQueue(blocks: [CourseBlock]) throws { + addInvocation(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + do { + _ = try methodReturnValue(.m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func getDownloadTasks() -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasks) + let perform = methodPerformValue(.m_getDownloadTasks) as? () -> Void + perform?() + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadTasks).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadTasks(). Use given") + Failure("Stub return value not specified for getDownloadTasks(). Use given") + } + return __value + } + + open func getDownloadTasksForCourse(_ courseId: String) -> [DownloadDataTask] { + addInvocation(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + var __value: [DownloadDataTask] + do { + __value = try methodReturnValue(.m_getDownloadTasksForCourse__courseId(Parameter.value(`courseId`))).casted() + } catch { + onFatalFailure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") + Failure("Stub return value not specified for getDownloadTasksForCourse(_ courseId: String). Use given") + } + return __value + } + + open func cancelDownloading(courseId: String, blocks: [CourseBlock]) throws { + addInvocation(.m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter.value(`courseId`), Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter.value(`courseId`), Parameter<[CourseBlock]>.value(`blocks`))) as? (String, [CourseBlock]) -> Void + perform?(`courseId`, `blocks`) + do { + _ = try methodReturnValue(.m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter.value(`courseId`), Parameter<[CourseBlock]>.value(`blocks`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func cancelDownloading(task: DownloadDataTask) throws { + addInvocation(.m_cancelDownloading__task_task(Parameter.value(`task`))) + let perform = methodPerformValue(.m_cancelDownloading__task_task(Parameter.value(`task`))) as? (DownloadDataTask) -> Void + perform?(`task`) + do { + _ = try methodReturnValue(.m_cancelDownloading__task_task(Parameter.value(`task`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func cancelDownloading(courseId: String) throws { + addInvocation(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + do { + _ = try methodReturnValue(.m_cancelDownloading__courseId_courseId(Parameter.value(`courseId`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } } open func cancelAllDownloading() throws { addInvocation(.m_cancelAllDownloading) let perform = methodPerformValue(.m_cancelAllDownloading) as? () -> Void perform?() - do { - _ = try methodReturnValue(.m_cancelAllDownloading).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } + do { + _ = try methodReturnValue(.m_cancelAllDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func deleteFile(blocks: [CourseBlock]) { + addInvocation(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + } + + open func deleteAllFiles() { + addInvocation(.m_deleteAllFiles) + let perform = methodPerformValue(.m_deleteAllFiles) as? () -> Void + perform?() + } + + open func fileUrl(for blockId: String) -> URL? { + addInvocation(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) + let perform = methodPerformValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void + perform?(`blockId`) + var __value: URL? = nil + do { + __value = try methodReturnValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))).casted() + } catch { + // do nothing + } + return __value + } + + open func updateUnzippedFileSize(for sequentials: [CourseSequential]) -> [CourseSequential] { + addInvocation(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) + let perform = methodPerformValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))) as? ([CourseSequential]) -> Void + perform?(`sequentials`) + var __value: [CourseSequential] + do { + __value = try methodReturnValue(.m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>.value(`sequentials`))).casted() + } catch { + onFatalFailure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + Failure("Stub return value not specified for updateUnzippedFileSize(for sequentials: [CourseSequential]). Use given") + } + return __value + } + + open func resumeDownloading() throws { + addInvocation(.m_resumeDownloading) + let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + perform?() + do { + _ = try methodReturnValue(.m_resumeDownloading).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } + } + + open func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { + addInvocation(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) + let perform = methodPerformValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void + perform?(`blocks`) + var __value: Bool + do { + __value = try methodReturnValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() + } catch { + onFatalFailure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + Failure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") + } + return __value + } + + open func removeAppSupportDirectoryUnusedContent() { + addInvocation(.m_removeAppSupportDirectoryUnusedContent) + let perform = methodPerformValue(.m_removeAppSupportDirectoryUnusedContent) as? () -> Void + perform?() + } + + + fileprivate enum MethodType { + case m_publisher + case m_eventPublisher + case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) + case m_getDownloadTasks + case m_getDownloadTasksForCourse__courseId(Parameter) + case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) + case m_cancelDownloading__task_task(Parameter) + case m_cancelDownloading__courseId_courseId(Parameter) + case m_cancelAllDownloading + case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) + case m_deleteAllFiles + case m_fileUrl__for_blockId(Parameter) + case m_updateUnzippedFileSize__for_sequentials(Parameter<[CourseSequential]>) + case m_resumeDownloading + case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) + case m_removeAppSupportDirectoryUnusedContent + case p_currentDownloadTask_get + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_publisher, .m_publisher): return .match + + case (.m_eventPublisher, .m_eventPublisher): return .match + + case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_getDownloadTasks, .m_getDownloadTasks): return .match + + case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) + return Matcher.ComparisonResult(results) + + case (.m_cancelDownloading__courseId_courseIdblocks_blocks(let lhsCourseid, let lhsBlocks), .m_cancelDownloading__courseId_courseIdblocks_blocks(let rhsCourseid, let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_cancelDownloading__task_task(let lhsTask), .m_cancelDownloading__task_task(let rhsTask)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "task")) + return Matcher.ComparisonResult(results) + + case (.m_cancelDownloading__courseId_courseId(let lhsCourseid), .m_cancelDownloading__courseId_courseId(let rhsCourseid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + return Matcher.ComparisonResult(results) + + case (.m_cancelAllDownloading, .m_cancelAllDownloading): return .match + + case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_deleteAllFiles, .m_deleteAllFiles): return .match + + case (.m_fileUrl__for_blockId(let lhsBlockid), .m_fileUrl__for_blockId(let rhsBlockid)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) + return Matcher.ComparisonResult(results) + + case (.m_updateUnzippedFileSize__for_sequentials(let lhsSequentials), .m_updateUnzippedFileSize__for_sequentials(let rhsSequentials)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSequentials, rhs: rhsSequentials, with: matcher), lhsSequentials, rhsSequentials, "for sequentials")) + return Matcher.ComparisonResult(results) + + case (.m_resumeDownloading, .m_resumeDownloading): return .match + + case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + return Matcher.ComparisonResult(results) + + case (.m_removeAppSupportDirectoryUnusedContent, .m_removeAppSupportDirectoryUnusedContent): return .match + case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .m_publisher: return 0 + case .m_eventPublisher: return 0 + case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue + case .m_getDownloadTasks: return 0 + case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue + case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue + case let .m_cancelDownloading__task_task(p0): return p0.intValue + case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue + case .m_cancelAllDownloading: return 0 + case let .m_deleteFile__blocks_blocks(p0): return p0.intValue + case .m_deleteAllFiles: return 0 + case let .m_fileUrl__for_blockId(p0): return p0.intValue + case let .m_updateUnzippedFileSize__for_sequentials(p0): return p0.intValue + case .m_resumeDownloading: return 0 + case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue + case .m_removeAppSupportDirectoryUnusedContent: return 0 + case .p_currentDownloadTask_get: return 0 + } + } + func assertionName() -> String { + switch self { + case .m_publisher: return ".publisher()" + case .m_eventPublisher: return ".eventPublisher()" + case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" + case .m_getDownloadTasks: return ".getDownloadTasks()" + case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" + case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" + case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" + case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" + case .m_cancelAllDownloading: return ".cancelAllDownloading()" + case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" + case .m_deleteAllFiles: return ".deleteAllFiles()" + case .m_fileUrl__for_blockId: return ".fileUrl(for:)" + case .m_updateUnzippedFileSize__for_sequentials: return ".updateUnzippedFileSize(for:)" + case .m_resumeDownloading: return ".resumeDownloading()" + case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" + case .m_removeAppSupportDirectoryUnusedContent: return ".removeAppSupportDirectoryUnusedContent()" + case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func currentDownloadTask(getter defaultValue: DownloadDataTask?...) -> PropertyStub { + return Given(method: .p_currentDownloadTask_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + public static func publisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func eventPublisher(willReturn: AnyPublisher...) -> MethodStub { + return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasks(willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { + return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { + return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willReturn: [CourseSequential]...) -> MethodStub { + return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { + return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func eventPublisher(willProduce: (Stubber>) -> Void) -> MethodStub { + let willReturn: [AnyPublisher] = [] + let given: Given = { return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (AnyPublisher).self) + willProduce(stubber) + return given + } + public static func getDownloadTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { + let willReturn: [[DownloadDataTask]] = [] + let given: Given = { return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([DownloadDataTask]).self) + willProduce(stubber) + return given + } + public static func fileUrl(for blockId: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [URL?] = [] + let given: Given = { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (URL?).self) + willProduce(stubber) + return given + } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, willProduce: (Stubber<[CourseSequential]>) -> Void) -> MethodStub { + let willReturn: [[CourseSequential]] = [] + let given: Given = { return Given(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseSequential]).self) + willProduce(stubber) + return given + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [Bool] = [] + let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (Bool).self) + willProduce(stubber) + return given + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(task: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(task: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelDownloading(courseId: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelDownloading(courseId: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func cancelAllDownloading(willThrow: Error...) -> MethodStub { + return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func cancelAllDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func resumeDownloading(willThrow: Error...) -> MethodStub { + return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func resumeDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func publisher() -> Verify { return Verify(method: .m_publisher)} + public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} + public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} + public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} + public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} + public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} + public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} + public static func cancelAllDownloading() -> Verify { return Verify(method: .m_cancelAllDownloading)} + public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} + public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} + public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>) -> Verify { return Verify(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`))} + public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} + public static func removeAppSupportDirectoryUnusedContent() -> Verify { return Verify(method: .m_removeAppSupportDirectoryUnusedContent)} + public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func publisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_publisher, performs: perform) + } + public static func eventPublisher(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_eventPublisher, performs: perform) + } + public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + } + public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getDownloadTasks, performs: perform) + } + public static func getDownloadTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getDownloadTasksForCourse__courseId(`courseId`), performs: perform) + } + public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, perform: @escaping (String, [CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), performs: perform) + } + public static func cancelDownloading(task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__task_task(`task`), performs: perform) + } + public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) + } + public static func cancelAllDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_cancelAllDownloading, performs: perform) + } + public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) + } + public static func deleteAllFiles(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteAllFiles, performs: perform) + } + public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) + } + public static func updateUnzippedFileSize(for sequentials: Parameter<[CourseSequential]>, perform: @escaping ([CourseSequential]) -> Void) -> Perform { + return Perform(method: .m_updateUnzippedFileSize__for_sequentials(`sequentials`), performs: perform) + } + public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_resumeDownloading, performs: perform) + } + public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { + return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) + } + public static func removeAppSupportDirectoryUnusedContent(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeAppSupportDirectoryUnusedContent, performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - OfflineSyncInteractorProtocol + +open class OfflineSyncInteractorProtocolMock: OfflineSyncInteractorProtocol, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func submitOfflineProgress(courseID: String, blockID: String, data: String) throws -> Bool { + addInvocation(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) + let perform = methodPerformValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))) as? (String, String, String) -> Void + perform?(`courseID`, `blockID`, `data`) + var __value: Bool + do { + __value = try methodReturnValue(.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter.value(`courseID`), Parameter.value(`blockID`), Parameter.value(`data`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + Failure("Stub return value not specified for submitOfflineProgress(courseID: String, blockID: String, data: String). Use given") + } catch { + throw error + } + return __value + } + + + fileprivate enum MethodType { + case m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(Parameter, Parameter, Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { + case (.m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let lhsCourseid, let lhsBlockid, let lhsData), .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(let rhsCourseid, let rhsBlockid, let rhsData)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "blockID")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsData, rhs: rhsData, with: matcher), lhsData, rhsData, "data")) + return Matcher.ComparisonResult(results) + } + } + + func intValue() -> Int { + switch self { + case let .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue + } + } + func assertionName() -> String { + switch self { + case .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data: return ".submitOfflineProgress(courseID:blockID:data:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Bool).self) + willProduce(stubber) + return given + } + } + + public struct Verify { + fileprivate var method: MethodType + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter) -> Verify { return Verify(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`))} + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + public static func submitOfflineProgress(courseID: Parameter, blockID: Parameter, data: Parameter, perform: @escaping (String, String, String) -> Void) -> Perform { + return Perform(method: .m_submitOfflineProgress__courseID_courseIDblockID_blockIDdata_data(`courseID`, `blockID`, `data`), performs: perform) + } + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + +// MARK: - ProfileAnalytics + +open class ProfileAnalyticsMock: ProfileAnalytics, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + + + + + open func profileEditClicked() { + addInvocation(.m_profileEditClicked) + let perform = methodPerformValue(.m_profileEditClicked) as? () -> Void + perform?() } - open func deleteFile(blocks: [CourseBlock]) { - addInvocation(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) + open func profileSwitch(action: String) { + addInvocation(.m_profileSwitch__action_action(Parameter.value(`action`))) + let perform = methodPerformValue(.m_profileSwitch__action_action(Parameter.value(`action`))) as? (String) -> Void + perform?(`action`) } - open func deleteAllFiles() { - addInvocation(.m_deleteAllFiles) - let perform = methodPerformValue(.m_deleteAllFiles) as? () -> Void + open func profileEditDoneClicked() { + addInvocation(.m_profileEditDoneClicked) + let perform = methodPerformValue(.m_profileEditDoneClicked) as? () -> Void perform?() } - open func fileUrl(for blockId: String) -> URL? { - addInvocation(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) - let perform = methodPerformValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))) as? (String) -> Void - perform?(`blockId`) - var __value: URL? = nil - do { - __value = try methodReturnValue(.m_fileUrl__for_blockId(Parameter.value(`blockId`))).casted() - } catch { - // do nothing - } - return __value + open func profileDeleteAccountClicked() { + addInvocation(.m_profileDeleteAccountClicked) + let perform = methodPerformValue(.m_profileDeleteAccountClicked) as? () -> Void + perform?() } - open func resumeDownloading() throws { - addInvocation(.m_resumeDownloading) - let perform = methodPerformValue(.m_resumeDownloading) as? () -> Void + open func profileVideoSettingsClicked() { + addInvocation(.m_profileVideoSettingsClicked) + let perform = methodPerformValue(.m_profileVideoSettingsClicked) as? () -> Void perform?() - do { - _ = try methodReturnValue(.m_resumeDownloading).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } } - open func isLargeVideosSize(blocks: [CourseBlock]) -> Bool { - addInvocation(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) - let perform = methodPerformValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))) as? ([CourseBlock]) -> Void - perform?(`blocks`) - var __value: Bool - do { - __value = try methodReturnValue(.m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>.value(`blocks`))).casted() - } catch { - onFatalFailure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") - Failure("Stub return value not specified for isLargeVideosSize(blocks: [CourseBlock]). Use given") - } - return __value + open func privacyPolicyClicked() { + addInvocation(.m_privacyPolicyClicked) + let perform = methodPerformValue(.m_privacyPolicyClicked) as? () -> Void + perform?() + } + + open func cookiePolicyClicked() { + addInvocation(.m_cookiePolicyClicked) + let perform = methodPerformValue(.m_cookiePolicyClicked) as? () -> Void + perform?() + } + + open func emailSupportClicked() { + addInvocation(.m_emailSupportClicked) + let perform = methodPerformValue(.m_emailSupportClicked) as? () -> Void + perform?() + } + + open func faqClicked() { + addInvocation(.m_faqClicked) + let perform = methodPerformValue(.m_faqClicked) as? () -> Void + perform?() + } + + open func tosClicked() { + addInvocation(.m_tosClicked) + let perform = methodPerformValue(.m_tosClicked) as? () -> Void + perform?() + } + + open func dataSellClicked() { + addInvocation(.m_dataSellClicked) + let perform = methodPerformValue(.m_dataSellClicked) as? () -> Void + perform?() + } + + open func userLogout(force: Bool) { + addInvocation(.m_userLogout__force_force(Parameter.value(`force`))) + let perform = methodPerformValue(.m_userLogout__force_force(Parameter.value(`force`))) as? (Bool) -> Void + perform?(`force`) + } + + open func profileWifiToggle(action: String) { + addInvocation(.m_profileWifiToggle__action_action(Parameter.value(`action`))) + let perform = methodPerformValue(.m_profileWifiToggle__action_action(Parameter.value(`action`))) as? (String) -> Void + perform?(`action`) + } + + open func profileUserDeleteAccountClicked() { + addInvocation(.m_profileUserDeleteAccountClicked) + let perform = methodPerformValue(.m_profileUserDeleteAccountClicked) as? () -> Void + perform?() + } + + open func profileDeleteAccountSuccess(success: Bool) { + addInvocation(.m_profileDeleteAccountSuccess__success_success(Parameter.value(`success`))) + let perform = methodPerformValue(.m_profileDeleteAccountSuccess__success_success(Parameter.value(`success`))) as? (Bool) -> Void + perform?(`success`) + } + + open func profileTrackEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_profileTrackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_profileTrackEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) + } + + open func profileScreenEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { + addInvocation(.m_profileScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) + let perform = methodPerformValue(.m_profileScreenEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void + perform?(`event`, `biValue`) } fileprivate enum MethodType { - case m_publisher - case m_eventPublisher - case m_addToDownloadQueue__blocks_blocks(Parameter<[CourseBlock]>) - case m_getDownloadTasks - case m_getDownloadTasksForCourse__courseId(Parameter) - case m_cancelDownloading__courseId_courseIdblocks_blocks(Parameter, Parameter<[CourseBlock]>) - case m_cancelDownloading__task_task(Parameter) - case m_cancelDownloading__courseId_courseId(Parameter) - case m_cancelAllDownloading - case m_deleteFile__blocks_blocks(Parameter<[CourseBlock]>) - case m_deleteAllFiles - case m_fileUrl__for_blockId(Parameter) - case m_resumeDownloading - case m_isLargeVideosSize__blocks_blocks(Parameter<[CourseBlock]>) - case p_currentDownloadTask_get + case m_profileEditClicked + case m_profileSwitch__action_action(Parameter) + case m_profileEditDoneClicked + case m_profileDeleteAccountClicked + case m_profileVideoSettingsClicked + case m_privacyPolicyClicked + case m_cookiePolicyClicked + case m_emailSupportClicked + case m_faqClicked + case m_tosClicked + case m_dataSellClicked + case m_userLogout__force_force(Parameter) + case m_profileWifiToggle__action_action(Parameter) + case m_profileUserDeleteAccountClicked + case m_profileDeleteAccountSuccess__success_success(Parameter) + case m_profileTrackEvent__eventbiValue_biValue(Parameter, Parameter) + case m_profileScreenEvent__eventbiValue_biValue(Parameter, Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { - case (.m_publisher, .m_publisher): return .match - - case (.m_eventPublisher, .m_eventPublisher): return .match + case (.m_profileEditClicked, .m_profileEditClicked): return .match - case (.m_addToDownloadQueue__blocks_blocks(let lhsBlocks), .m_addToDownloadQueue__blocks_blocks(let rhsBlocks)): + case (.m_profileSwitch__action_action(let lhsAction), .m_profileSwitch__action_action(let rhsAction)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) return Matcher.ComparisonResult(results) - case (.m_getDownloadTasks, .m_getDownloadTasks): return .match + case (.m_profileEditDoneClicked, .m_profileEditDoneClicked): return .match - case (.m_getDownloadTasksForCourse__courseId(let lhsCourseid), .m_getDownloadTasksForCourse__courseId(let rhsCourseid)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "_ courseId")) - return Matcher.ComparisonResult(results) + case (.m_profileDeleteAccountClicked, .m_profileDeleteAccountClicked): return .match - case (.m_cancelDownloading__courseId_courseIdblocks_blocks(let lhsCourseid, let lhsBlocks), .m_cancelDownloading__courseId_courseIdblocks_blocks(let rhsCourseid, let rhsBlocks)): - var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) - return Matcher.ComparisonResult(results) + case (.m_profileVideoSettingsClicked, .m_profileVideoSettingsClicked): return .match - case (.m_cancelDownloading__task_task(let lhsTask), .m_cancelDownloading__task_task(let rhsTask)): + case (.m_privacyPolicyClicked, .m_privacyPolicyClicked): return .match + + case (.m_cookiePolicyClicked, .m_cookiePolicyClicked): return .match + + case (.m_emailSupportClicked, .m_emailSupportClicked): return .match + + case (.m_faqClicked, .m_faqClicked): return .match + + case (.m_tosClicked, .m_tosClicked): return .match + + case (.m_dataSellClicked, .m_dataSellClicked): return .match + + case (.m_userLogout__force_force(let lhsForce), .m_userLogout__force_force(let rhsForce)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTask, rhs: rhsTask, with: matcher), lhsTask, rhsTask, "task")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) return Matcher.ComparisonResult(results) - case (.m_cancelDownloading__courseId_courseId(let lhsCourseid), .m_cancelDownloading__courseId_courseId(let rhsCourseid)): + case (.m_profileWifiToggle__action_action(let lhsAction), .m_profileWifiToggle__action_action(let rhsAction)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) return Matcher.ComparisonResult(results) - case (.m_cancelAllDownloading, .m_cancelAllDownloading): return .match + case (.m_profileUserDeleteAccountClicked, .m_profileUserDeleteAccountClicked): return .match - case (.m_deleteFile__blocks_blocks(let lhsBlocks), .m_deleteFile__blocks_blocks(let rhsBlocks)): + case (.m_profileDeleteAccountSuccess__success_success(let lhsSuccess), .m_profileDeleteAccountSuccess__success_success(let rhsSuccess)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSuccess, rhs: rhsSuccess, with: matcher), lhsSuccess, rhsSuccess, "success")) return Matcher.ComparisonResult(results) - case (.m_deleteAllFiles, .m_deleteAllFiles): return .match - - case (.m_fileUrl__for_blockId(let lhsBlockid), .m_fileUrl__for_blockId(let rhsBlockid)): + case (.m_profileTrackEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_profileTrackEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlockid, rhs: rhsBlockid, with: matcher), lhsBlockid, rhsBlockid, "for blockId")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) return Matcher.ComparisonResult(results) - case (.m_resumeDownloading, .m_resumeDownloading): return .match - - case (.m_isLargeVideosSize__blocks_blocks(let lhsBlocks), .m_isLargeVideosSize__blocks_blocks(let rhsBlocks)): + case (.m_profileScreenEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_profileScreenEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBlocks, rhs: rhsBlocks, with: matcher), lhsBlocks, rhsBlocks, "blocks")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) return Matcher.ComparisonResult(results) - case (.p_currentDownloadTask_get,.p_currentDownloadTask_get): return Matcher.ComparisonResult.match default: return .none } } func intValue() -> Int { switch self { - case .m_publisher: return 0 - case .m_eventPublisher: return 0 - case let .m_addToDownloadQueue__blocks_blocks(p0): return p0.intValue - case .m_getDownloadTasks: return 0 - case let .m_getDownloadTasksForCourse__courseId(p0): return p0.intValue - case let .m_cancelDownloading__courseId_courseIdblocks_blocks(p0, p1): return p0.intValue + p1.intValue - case let .m_cancelDownloading__task_task(p0): return p0.intValue - case let .m_cancelDownloading__courseId_courseId(p0): return p0.intValue - case .m_cancelAllDownloading: return 0 - case let .m_deleteFile__blocks_blocks(p0): return p0.intValue - case .m_deleteAllFiles: return 0 - case let .m_fileUrl__for_blockId(p0): return p0.intValue - case .m_resumeDownloading: return 0 - case let .m_isLargeVideosSize__blocks_blocks(p0): return p0.intValue - case .p_currentDownloadTask_get: return 0 + case .m_profileEditClicked: return 0 + case let .m_profileSwitch__action_action(p0): return p0.intValue + case .m_profileEditDoneClicked: return 0 + case .m_profileDeleteAccountClicked: return 0 + case .m_profileVideoSettingsClicked: return 0 + case .m_privacyPolicyClicked: return 0 + case .m_cookiePolicyClicked: return 0 + case .m_emailSupportClicked: return 0 + case .m_faqClicked: return 0 + case .m_tosClicked: return 0 + case .m_dataSellClicked: return 0 + case let .m_userLogout__force_force(p0): return p0.intValue + case let .m_profileWifiToggle__action_action(p0): return p0.intValue + case .m_profileUserDeleteAccountClicked: return 0 + case let .m_profileDeleteAccountSuccess__success_success(p0): return p0.intValue + case let .m_profileTrackEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue + case let .m_profileScreenEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue } } func assertionName() -> String { switch self { - case .m_publisher: return ".publisher()" - case .m_eventPublisher: return ".eventPublisher()" - case .m_addToDownloadQueue__blocks_blocks: return ".addToDownloadQueue(blocks:)" - case .m_getDownloadTasks: return ".getDownloadTasks()" - case .m_getDownloadTasksForCourse__courseId: return ".getDownloadTasksForCourse(_:)" - case .m_cancelDownloading__courseId_courseIdblocks_blocks: return ".cancelDownloading(courseId:blocks:)" - case .m_cancelDownloading__task_task: return ".cancelDownloading(task:)" - case .m_cancelDownloading__courseId_courseId: return ".cancelDownloading(courseId:)" - case .m_cancelAllDownloading: return ".cancelAllDownloading()" - case .m_deleteFile__blocks_blocks: return ".deleteFile(blocks:)" - case .m_deleteAllFiles: return ".deleteAllFiles()" - case .m_fileUrl__for_blockId: return ".fileUrl(for:)" - case .m_resumeDownloading: return ".resumeDownloading()" - case .m_isLargeVideosSize__blocks_blocks: return ".isLargeVideosSize(blocks:)" - case .p_currentDownloadTask_get: return "[get] .currentDownloadTask" - } - } - } - - open class Given: StubbedMethod { - fileprivate var method: MethodType - - private init(method: MethodType, products: [StubProduct]) { - self.method = method - super.init(products) - } - - public static func currentDownloadTask(getter defaultValue: DownloadDataTask?...) -> PropertyStub { - return Given(method: .p_currentDownloadTask_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) - } - - public static func publisher(willReturn: AnyPublisher...) -> MethodStub { - return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func eventPublisher(willReturn: AnyPublisher...) -> MethodStub { - return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func getDownloadTasks(willReturn: [DownloadDataTask]...) -> MethodStub { - return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func getDownloadTasksForCourse(_ courseId: Parameter, willReturn: [DownloadDataTask]...) -> MethodStub { - return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func fileUrl(for blockId: Parameter, willReturn: URL?...) -> MethodStub { - return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willReturn: Bool...) -> MethodStub { - return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func publisher(willProduce: (Stubber>) -> Void) -> MethodStub { - let willReturn: [AnyPublisher] = [] - let given: Given = { return Given(method: .m_publisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: (AnyPublisher).self) - willProduce(stubber) - return given - } - public static func eventPublisher(willProduce: (Stubber>) -> Void) -> MethodStub { - let willReturn: [AnyPublisher] = [] - let given: Given = { return Given(method: .m_eventPublisher, products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: (AnyPublisher).self) - willProduce(stubber) - return given - } - public static func getDownloadTasks(willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { - let willReturn: [[DownloadDataTask]] = [] - let given: Given = { return Given(method: .m_getDownloadTasks, products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: ([DownloadDataTask]).self) - willProduce(stubber) - return given - } - public static func getDownloadTasksForCourse(_ courseId: Parameter, willProduce: (Stubber<[DownloadDataTask]>) -> Void) -> MethodStub { - let willReturn: [[DownloadDataTask]] = [] - let given: Given = { return Given(method: .m_getDownloadTasksForCourse__courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: ([DownloadDataTask]).self) - willProduce(stubber) - return given - } - public static func fileUrl(for blockId: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { - let willReturn: [URL?] = [] - let given: Given = { return Given(method: .m_fileUrl__for_blockId(`blockId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: (URL?).self) - willProduce(stubber) - return given - } - public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, willProduce: (Stubber) -> Void) -> MethodStub { - let willReturn: [Bool] = [] - let given: Given = { return Given(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: (Bool).self) - willProduce(stubber) - return given - } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) - willProduce(stubber) - return given - } - public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) - willProduce(stubber) - return given - } - public static func cancelDownloading(task: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func cancelDownloading(task: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_cancelDownloading__task_task(`task`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) - willProduce(stubber) - return given - } - public static func cancelDownloading(courseId: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func cancelDownloading(courseId: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_cancelDownloading__courseId_courseId(`courseId`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) - willProduce(stubber) - return given - } - public static func cancelAllDownloading(willThrow: Error...) -> MethodStub { - return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) - } - public static func cancelAllDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_cancelAllDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) - willProduce(stubber) - return given - } - public static func resumeDownloading(willThrow: Error...) -> MethodStub { - return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) + case .m_profileEditClicked: return ".profileEditClicked()" + case .m_profileSwitch__action_action: return ".profileSwitch(action:)" + case .m_profileEditDoneClicked: return ".profileEditDoneClicked()" + case .m_profileDeleteAccountClicked: return ".profileDeleteAccountClicked()" + case .m_profileVideoSettingsClicked: return ".profileVideoSettingsClicked()" + case .m_privacyPolicyClicked: return ".privacyPolicyClicked()" + case .m_cookiePolicyClicked: return ".cookiePolicyClicked()" + case .m_emailSupportClicked: return ".emailSupportClicked()" + case .m_faqClicked: return ".faqClicked()" + case .m_tosClicked: return ".tosClicked()" + case .m_dataSellClicked: return ".dataSellClicked()" + case .m_userLogout__force_force: return ".userLogout(force:)" + case .m_profileWifiToggle__action_action: return ".profileWifiToggle(action:)" + case .m_profileUserDeleteAccountClicked: return ".profileUserDeleteAccountClicked()" + case .m_profileDeleteAccountSuccess__success_success: return ".profileDeleteAccountSuccess(success:)" + case .m_profileTrackEvent__eventbiValue_biValue: return ".profileTrackEvent(_:biValue:)" + case .m_profileScreenEvent__eventbiValue_biValue: return ".profileScreenEvent(_:biValue:)" + } } - public static func resumeDownloading(willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_resumeDownloading, products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) - willProduce(stubber) - return given + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) } + + } public struct Verify { fileprivate var method: MethodType - public static func publisher() -> Verify { return Verify(method: .m_publisher)} - public static func eventPublisher() -> Verify { return Verify(method: .m_eventPublisher)} - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_addToDownloadQueue__blocks_blocks(`blocks`))} - public static func getDownloadTasks() -> Verify { return Verify(method: .m_getDownloadTasks)} - public static func getDownloadTasksForCourse(_ courseId: Parameter) -> Verify { return Verify(method: .m_getDownloadTasksForCourse__courseId(`courseId`))} - public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`))} - public static func cancelDownloading(task: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__task_task(`task`))} - public static func cancelDownloading(courseId: Parameter) -> Verify { return Verify(method: .m_cancelDownloading__courseId_courseId(`courseId`))} - public static func cancelAllDownloading() -> Verify { return Verify(method: .m_cancelAllDownloading)} - public static func deleteFile(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_deleteFile__blocks_blocks(`blocks`))} - public static func deleteAllFiles() -> Verify { return Verify(method: .m_deleteAllFiles)} - public static func fileUrl(for blockId: Parameter) -> Verify { return Verify(method: .m_fileUrl__for_blockId(`blockId`))} - public static func resumeDownloading() -> Verify { return Verify(method: .m_resumeDownloading)} - public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>) -> Verify { return Verify(method: .m_isLargeVideosSize__blocks_blocks(`blocks`))} - public static var currentDownloadTask: Verify { return Verify(method: .p_currentDownloadTask_get) } + public static func profileEditClicked() -> Verify { return Verify(method: .m_profileEditClicked)} + public static func profileSwitch(action: Parameter) -> Verify { return Verify(method: .m_profileSwitch__action_action(`action`))} + public static func profileEditDoneClicked() -> Verify { return Verify(method: .m_profileEditDoneClicked)} + public static func profileDeleteAccountClicked() -> Verify { return Verify(method: .m_profileDeleteAccountClicked)} + public static func profileVideoSettingsClicked() -> Verify { return Verify(method: .m_profileVideoSettingsClicked)} + public static func privacyPolicyClicked() -> Verify { return Verify(method: .m_privacyPolicyClicked)} + public static func cookiePolicyClicked() -> Verify { return Verify(method: .m_cookiePolicyClicked)} + public static func emailSupportClicked() -> Verify { return Verify(method: .m_emailSupportClicked)} + public static func faqClicked() -> Verify { return Verify(method: .m_faqClicked)} + public static func tosClicked() -> Verify { return Verify(method: .m_tosClicked)} + public static func dataSellClicked() -> Verify { return Verify(method: .m_dataSellClicked)} + public static func userLogout(force: Parameter) -> Verify { return Verify(method: .m_userLogout__force_force(`force`))} + public static func profileWifiToggle(action: Parameter) -> Verify { return Verify(method: .m_profileWifiToggle__action_action(`action`))} + public static func profileUserDeleteAccountClicked() -> Verify { return Verify(method: .m_profileUserDeleteAccountClicked)} + public static func profileDeleteAccountSuccess(success: Parameter) -> Verify { return Verify(method: .m_profileDeleteAccountSuccess__success_success(`success`))} + public static func profileTrackEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_profileTrackEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func profileScreenEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_profileScreenEvent__eventbiValue_biValue(`event`, `biValue`))} } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func publisher(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_publisher, performs: perform) + public static func profileEditClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_profileEditClicked, performs: perform) } - public static func eventPublisher(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_eventPublisher, performs: perform) + public static func profileSwitch(action: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_profileSwitch__action_action(`action`), performs: perform) } - public static func addToDownloadQueue(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_addToDownloadQueue__blocks_blocks(`blocks`), performs: perform) + public static func profileEditDoneClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_profileEditDoneClicked, performs: perform) } - public static func getDownloadTasks(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_getDownloadTasks, performs: perform) + public static func profileDeleteAccountClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_profileDeleteAccountClicked, performs: perform) } - public static func getDownloadTasksForCourse(_ courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getDownloadTasksForCourse__courseId(`courseId`), performs: perform) + public static func profileVideoSettingsClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_profileVideoSettingsClicked, performs: perform) } - public static func cancelDownloading(courseId: Parameter, blocks: Parameter<[CourseBlock]>, perform: @escaping (String, [CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_cancelDownloading__courseId_courseIdblocks_blocks(`courseId`, `blocks`), performs: perform) + public static func privacyPolicyClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_privacyPolicyClicked, performs: perform) } - public static func cancelDownloading(task: Parameter, perform: @escaping (DownloadDataTask) -> Void) -> Perform { - return Perform(method: .m_cancelDownloading__task_task(`task`), performs: perform) + public static func cookiePolicyClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_cookiePolicyClicked, performs: perform) } - public static func cancelDownloading(courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_cancelDownloading__courseId_courseId(`courseId`), performs: perform) + public static func emailSupportClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_emailSupportClicked, performs: perform) } - public static func cancelAllDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_cancelAllDownloading, performs: perform) + public static func faqClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_faqClicked, performs: perform) } - public static func deleteFile(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_deleteFile__blocks_blocks(`blocks`), performs: perform) + public static func tosClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_tosClicked, performs: perform) } - public static func deleteAllFiles(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_deleteAllFiles, performs: perform) + public static func dataSellClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_dataSellClicked, performs: perform) } - public static func fileUrl(for blockId: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_fileUrl__for_blockId(`blockId`), performs: perform) + public static func userLogout(force: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_userLogout__force_force(`force`), performs: perform) } - public static func resumeDownloading(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_resumeDownloading, performs: perform) + public static func profileWifiToggle(action: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_profileWifiToggle__action_action(`action`), performs: perform) } - public static func isLargeVideosSize(blocks: Parameter<[CourseBlock]>, perform: @escaping ([CourseBlock]) -> Void) -> Perform { - return Perform(method: .m_isLargeVideosSize__blocks_blocks(`blocks`), performs: perform) + public static func profileUserDeleteAccountClicked(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_profileUserDeleteAccountClicked, performs: perform) + } + public static func profileDeleteAccountSuccess(success: Parameter, perform: @escaping (Bool) -> Void) -> Perform { + return Perform(method: .m_profileDeleteAccountSuccess__success_success(`success`), performs: perform) + } + public static func profileTrackEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_profileTrackEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + } + public static func profileScreenEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { + return Perform(method: .m_profileScreenEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) } } @@ -2004,9 +4694,9 @@ open class DownloadManagerProtocolMock: DownloadManagerProtocol, Mock { } } -// MARK: - ProfileAnalytics +// MARK: - ProfileInteractorProtocol -open class ProfileAnalyticsMock: ProfileAnalytics, Mock { +open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -2048,299 +4738,537 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { - open func profileEditClicked() { - addInvocation(.m_profileEditClicked) - let perform = methodPerformValue(.m_profileEditClicked) as? () -> Void - perform?() - } - - open func profileSwitch(action: String) { - addInvocation(.m_profileSwitch__action_action(Parameter.value(`action`))) - let perform = methodPerformValue(.m_profileSwitch__action_action(Parameter.value(`action`))) as? (String) -> Void - perform?(`action`) + open func getUserProfile(username: String) throws -> UserProfile { + addInvocation(.m_getUserProfile__username_username(Parameter.value(`username`))) + let perform = methodPerformValue(.m_getUserProfile__username_username(Parameter.value(`username`))) as? (String) -> Void + perform?(`username`) + var __value: UserProfile + do { + __value = try methodReturnValue(.m_getUserProfile__username_username(Parameter.value(`username`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getUserProfile(username: String). Use given") + Failure("Stub return value not specified for getUserProfile(username: String). Use given") + } catch { + throw error + } + return __value } - open func profileEditDoneClicked() { - addInvocation(.m_profileEditDoneClicked) - let perform = methodPerformValue(.m_profileEditDoneClicked) as? () -> Void + open func getMyProfile() throws -> UserProfile { + addInvocation(.m_getMyProfile) + let perform = methodPerformValue(.m_getMyProfile) as? () -> Void perform?() + var __value: UserProfile + do { + __value = try methodReturnValue(.m_getMyProfile).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getMyProfile(). Use given") + Failure("Stub return value not specified for getMyProfile(). Use given") + } catch { + throw error + } + return __value } - open func profileDeleteAccountClicked() { - addInvocation(.m_profileDeleteAccountClicked) - let perform = methodPerformValue(.m_profileDeleteAccountClicked) as? () -> Void + open func getMyProfileOffline() -> UserProfile? { + addInvocation(.m_getMyProfileOffline) + let perform = methodPerformValue(.m_getMyProfileOffline) as? () -> Void perform?() + var __value: UserProfile? = nil + do { + __value = try methodReturnValue(.m_getMyProfileOffline).casted() + } catch { + // do nothing + } + return __value } - open func profileVideoSettingsClicked() { - addInvocation(.m_profileVideoSettingsClicked) - let perform = methodPerformValue(.m_profileVideoSettingsClicked) as? () -> Void + open func logOut() throws { + addInvocation(.m_logOut) + let perform = methodPerformValue(.m_logOut) as? () -> Void perform?() + do { + _ = try methodReturnValue(.m_logOut).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } } - open func privacyPolicyClicked() { - addInvocation(.m_privacyPolicyClicked) - let perform = methodPerformValue(.m_privacyPolicyClicked) as? () -> Void + open func getSpokenLanguages() -> [PickerFields.Option] { + addInvocation(.m_getSpokenLanguages) + let perform = methodPerformValue(.m_getSpokenLanguages) as? () -> Void perform?() + var __value: [PickerFields.Option] + do { + __value = try methodReturnValue(.m_getSpokenLanguages).casted() + } catch { + onFatalFailure("Stub return value not specified for getSpokenLanguages(). Use given") + Failure("Stub return value not specified for getSpokenLanguages(). Use given") + } + return __value } - open func cookiePolicyClicked() { - addInvocation(.m_cookiePolicyClicked) - let perform = methodPerformValue(.m_cookiePolicyClicked) as? () -> Void + open func getCountries() -> [PickerFields.Option] { + addInvocation(.m_getCountries) + let perform = methodPerformValue(.m_getCountries) as? () -> Void perform?() + var __value: [PickerFields.Option] + do { + __value = try methodReturnValue(.m_getCountries).casted() + } catch { + onFatalFailure("Stub return value not specified for getCountries(). Use given") + Failure("Stub return value not specified for getCountries(). Use given") + } + return __value } - open func emailSupportClicked() { - addInvocation(.m_emailSupportClicked) - let perform = methodPerformValue(.m_emailSupportClicked) as? () -> Void - perform?() + open func uploadProfilePicture(pictureData: Data) throws { + addInvocation(.m_uploadProfilePicture__pictureData_pictureData(Parameter.value(`pictureData`))) + let perform = methodPerformValue(.m_uploadProfilePicture__pictureData_pictureData(Parameter.value(`pictureData`))) as? (Data) -> Void + perform?(`pictureData`) + do { + _ = try methodReturnValue(.m_uploadProfilePicture__pictureData_pictureData(Parameter.value(`pictureData`))).casted() as Void + } catch MockError.notStubed { + // do nothing + } catch { + throw error + } } - open func faqClicked() { - addInvocation(.m_faqClicked) - let perform = methodPerformValue(.m_faqClicked) as? () -> Void + open func deleteProfilePicture() throws -> Bool { + addInvocation(.m_deleteProfilePicture) + let perform = methodPerformValue(.m_deleteProfilePicture) as? () -> Void perform?() + var __value: Bool + do { + __value = try methodReturnValue(.m_deleteProfilePicture).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for deleteProfilePicture(). Use given") + Failure("Stub return value not specified for deleteProfilePicture(). Use given") + } catch { + throw error + } + return __value } - open func tosClicked() { - addInvocation(.m_tosClicked) - let perform = methodPerformValue(.m_tosClicked) as? () -> Void - perform?() + open func updateUserProfile(parameters: [String: Any]) throws -> UserProfile { + addInvocation(.m_updateUserProfile__parameters_parameters(Parameter<[String: Any]>.value(`parameters`))) + let perform = methodPerformValue(.m_updateUserProfile__parameters_parameters(Parameter<[String: Any]>.value(`parameters`))) as? ([String: Any]) -> Void + perform?(`parameters`) + var __value: UserProfile + do { + __value = try methodReturnValue(.m_updateUserProfile__parameters_parameters(Parameter<[String: Any]>.value(`parameters`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for updateUserProfile(parameters: [String: Any]). Use given") + Failure("Stub return value not specified for updateUserProfile(parameters: [String: Any]). Use given") + } catch { + throw error + } + return __value } - open func dataSellClicked() { - addInvocation(.m_dataSellClicked) - let perform = methodPerformValue(.m_dataSellClicked) as? () -> Void - perform?() + open func deleteAccount(password: String) throws -> Bool { + addInvocation(.m_deleteAccount__password_password(Parameter.value(`password`))) + let perform = methodPerformValue(.m_deleteAccount__password_password(Parameter.value(`password`))) as? (String) -> Void + perform?(`password`) + var __value: Bool + do { + __value = try methodReturnValue(.m_deleteAccount__password_password(Parameter.value(`password`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for deleteAccount(password: String). Use given") + Failure("Stub return value not specified for deleteAccount(password: String). Use given") + } catch { + throw error + } + return __value } - open func userLogout(force: Bool) { - addInvocation(.m_userLogout__force_force(Parameter.value(`force`))) - let perform = methodPerformValue(.m_userLogout__force_force(Parameter.value(`force`))) as? (Bool) -> Void - perform?(`force`) + open func getSettings() -> UserSettings { + addInvocation(.m_getSettings) + let perform = methodPerformValue(.m_getSettings) as? () -> Void + perform?() + var __value: UserSettings + do { + __value = try methodReturnValue(.m_getSettings).casted() + } catch { + onFatalFailure("Stub return value not specified for getSettings(). Use given") + Failure("Stub return value not specified for getSettings(). Use given") + } + return __value } - open func profileWifiToggle(action: String) { - addInvocation(.m_profileWifiToggle__action_action(Parameter.value(`action`))) - let perform = methodPerformValue(.m_profileWifiToggle__action_action(Parameter.value(`action`))) as? (String) -> Void - perform?(`action`) + open func saveSettings(_ settings: UserSettings) { + addInvocation(.m_saveSettings__settings(Parameter.value(`settings`))) + let perform = methodPerformValue(.m_saveSettings__settings(Parameter.value(`settings`))) as? (UserSettings) -> Void + perform?(`settings`) } - open func profileUserDeleteAccountClicked() { - addInvocation(.m_profileUserDeleteAccountClicked) - let perform = methodPerformValue(.m_profileUserDeleteAccountClicked) as? () -> Void + open func enrollmentsStatus() throws -> [CourseForSync] { + addInvocation(.m_enrollmentsStatus) + let perform = methodPerformValue(.m_enrollmentsStatus) as? () -> Void perform?() + var __value: [CourseForSync] + do { + __value = try methodReturnValue(.m_enrollmentsStatus).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for enrollmentsStatus(). Use given") + Failure("Stub return value not specified for enrollmentsStatus(). Use given") + } catch { + throw error + } + return __value } - open func profileDeleteAccountSuccess(success: Bool) { - addInvocation(.m_profileDeleteAccountSuccess__success_success(Parameter.value(`success`))) - let perform = methodPerformValue(.m_profileDeleteAccountSuccess__success_success(Parameter.value(`success`))) as? (Bool) -> Void - perform?(`success`) - } - - open func profileEvent(_ event: AnalyticsEvent, biValue: EventBIValue) { - addInvocation(.m_profileEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) - let perform = methodPerformValue(.m_profileEvent__eventbiValue_biValue(Parameter.value(`event`), Parameter.value(`biValue`))) as? (AnalyticsEvent, EventBIValue) -> Void - perform?(`event`, `biValue`) + open func getCourseDates(courseID: String) throws -> CourseDates { + addInvocation(.m_getCourseDates__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_getCourseDates__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: CourseDates + do { + __value = try methodReturnValue(.m_getCourseDates__courseID_courseID(Parameter.value(`courseID`))).casted() + } catch MockError.notStubed { + onFatalFailure("Stub return value not specified for getCourseDates(courseID: String). Use given") + Failure("Stub return value not specified for getCourseDates(courseID: String). Use given") + } catch { + throw error + } + return __value } fileprivate enum MethodType { - case m_profileEditClicked - case m_profileSwitch__action_action(Parameter) - case m_profileEditDoneClicked - case m_profileDeleteAccountClicked - case m_profileVideoSettingsClicked - case m_privacyPolicyClicked - case m_cookiePolicyClicked - case m_emailSupportClicked - case m_faqClicked - case m_tosClicked - case m_dataSellClicked - case m_userLogout__force_force(Parameter) - case m_profileWifiToggle__action_action(Parameter) - case m_profileUserDeleteAccountClicked - case m_profileDeleteAccountSuccess__success_success(Parameter) - case m_profileEvent__eventbiValue_biValue(Parameter, Parameter) + case m_getUserProfile__username_username(Parameter) + case m_getMyProfile + case m_getMyProfileOffline + case m_logOut + case m_getSpokenLanguages + case m_getCountries + case m_uploadProfilePicture__pictureData_pictureData(Parameter) + case m_deleteProfilePicture + case m_updateUserProfile__parameters_parameters(Parameter<[String: Any]>) + case m_deleteAccount__password_password(Parameter) + case m_getSettings + case m_saveSettings__settings(Parameter) + case m_enrollmentsStatus + case m_getCourseDates__courseID_courseID(Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { - case (.m_profileEditClicked, .m_profileEditClicked): return .match - - case (.m_profileSwitch__action_action(let lhsAction), .m_profileSwitch__action_action(let rhsAction)): + case (.m_getUserProfile__username_username(let lhsUsername), .m_getUserProfile__username_username(let rhsUsername)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUsername, rhs: rhsUsername, with: matcher), lhsUsername, rhsUsername, "username")) return Matcher.ComparisonResult(results) - case (.m_profileEditDoneClicked, .m_profileEditDoneClicked): return .match - - case (.m_profileDeleteAccountClicked, .m_profileDeleteAccountClicked): return .match - - case (.m_profileVideoSettingsClicked, .m_profileVideoSettingsClicked): return .match + case (.m_getMyProfile, .m_getMyProfile): return .match - case (.m_privacyPolicyClicked, .m_privacyPolicyClicked): return .match + case (.m_getMyProfileOffline, .m_getMyProfileOffline): return .match - case (.m_cookiePolicyClicked, .m_cookiePolicyClicked): return .match + case (.m_logOut, .m_logOut): return .match - case (.m_emailSupportClicked, .m_emailSupportClicked): return .match + case (.m_getSpokenLanguages, .m_getSpokenLanguages): return .match - case (.m_faqClicked, .m_faqClicked): return .match + case (.m_getCountries, .m_getCountries): return .match - case (.m_tosClicked, .m_tosClicked): return .match + case (.m_uploadProfilePicture__pictureData_pictureData(let lhsPicturedata), .m_uploadProfilePicture__pictureData_pictureData(let rhsPicturedata)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPicturedata, rhs: rhsPicturedata, with: matcher), lhsPicturedata, rhsPicturedata, "pictureData")) + return Matcher.ComparisonResult(results) - case (.m_dataSellClicked, .m_dataSellClicked): return .match + case (.m_deleteProfilePicture, .m_deleteProfilePicture): return .match - case (.m_userLogout__force_force(let lhsForce), .m_userLogout__force_force(let rhsForce)): + case (.m_updateUserProfile__parameters_parameters(let lhsParameters), .m_updateUserProfile__parameters_parameters(let rhsParameters)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsForce, rhs: rhsForce, with: matcher), lhsForce, rhsForce, "force")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) return Matcher.ComparisonResult(results) - case (.m_profileWifiToggle__action_action(let lhsAction), .m_profileWifiToggle__action_action(let rhsAction)): + case (.m_deleteAccount__password_password(let lhsPassword), .m_deleteAccount__password_password(let rhsPassword)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAction, rhs: rhsAction, with: matcher), lhsAction, rhsAction, "action")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) return Matcher.ComparisonResult(results) - case (.m_profileUserDeleteAccountClicked, .m_profileUserDeleteAccountClicked): return .match + case (.m_getSettings, .m_getSettings): return .match - case (.m_profileDeleteAccountSuccess__success_success(let lhsSuccess), .m_profileDeleteAccountSuccess__success_success(let rhsSuccess)): + case (.m_saveSettings__settings(let lhsSettings), .m_saveSettings__settings(let rhsSettings)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSuccess, rhs: rhsSuccess, with: matcher), lhsSuccess, rhsSuccess, "success")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSettings, rhs: rhsSettings, with: matcher), lhsSettings, rhsSettings, "_ settings")) return Matcher.ComparisonResult(results) - case (.m_profileEvent__eventbiValue_biValue(let lhsEvent, let lhsBivalue), .m_profileEvent__eventbiValue_biValue(let rhsEvent, let rhsBivalue)): + case (.m_enrollmentsStatus, .m_enrollmentsStatus): return .match + + case (.m_getCourseDates__courseID_courseID(let lhsCourseid), .m_getCourseDates__courseID_courseID(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsBivalue, rhs: rhsBivalue, with: matcher), lhsBivalue, rhsBivalue, "biValue")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) return Matcher.ComparisonResult(results) default: return .none } } - - func intValue() -> Int { - switch self { - case .m_profileEditClicked: return 0 - case let .m_profileSwitch__action_action(p0): return p0.intValue - case .m_profileEditDoneClicked: return 0 - case .m_profileDeleteAccountClicked: return 0 - case .m_profileVideoSettingsClicked: return 0 - case .m_privacyPolicyClicked: return 0 - case .m_cookiePolicyClicked: return 0 - case .m_emailSupportClicked: return 0 - case .m_faqClicked: return 0 - case .m_tosClicked: return 0 - case .m_dataSellClicked: return 0 - case let .m_userLogout__force_force(p0): return p0.intValue - case let .m_profileWifiToggle__action_action(p0): return p0.intValue - case .m_profileUserDeleteAccountClicked: return 0 - case let .m_profileDeleteAccountSuccess__success_success(p0): return p0.intValue - case let .m_profileEvent__eventbiValue_biValue(p0, p1): return p0.intValue + p1.intValue - } + + func intValue() -> Int { + switch self { + case let .m_getUserProfile__username_username(p0): return p0.intValue + case .m_getMyProfile: return 0 + case .m_getMyProfileOffline: return 0 + case .m_logOut: return 0 + case .m_getSpokenLanguages: return 0 + case .m_getCountries: return 0 + case let .m_uploadProfilePicture__pictureData_pictureData(p0): return p0.intValue + case .m_deleteProfilePicture: return 0 + case let .m_updateUserProfile__parameters_parameters(p0): return p0.intValue + case let .m_deleteAccount__password_password(p0): return p0.intValue + case .m_getSettings: return 0 + case let .m_saveSettings__settings(p0): return p0.intValue + case .m_enrollmentsStatus: return 0 + case let .m_getCourseDates__courseID_courseID(p0): return p0.intValue + } + } + func assertionName() -> String { + switch self { + case .m_getUserProfile__username_username: return ".getUserProfile(username:)" + case .m_getMyProfile: return ".getMyProfile()" + case .m_getMyProfileOffline: return ".getMyProfileOffline()" + case .m_logOut: return ".logOut()" + case .m_getSpokenLanguages: return ".getSpokenLanguages()" + case .m_getCountries: return ".getCountries()" + case .m_uploadProfilePicture__pictureData_pictureData: return ".uploadProfilePicture(pictureData:)" + case .m_deleteProfilePicture: return ".deleteProfilePicture()" + case .m_updateUserProfile__parameters_parameters: return ".updateUserProfile(parameters:)" + case .m_deleteAccount__password_password: return ".deleteAccount(password:)" + case .m_getSettings: return ".getSettings()" + case .m_saveSettings__settings: return ".saveSettings(_:)" + case .m_enrollmentsStatus: return ".enrollmentsStatus()" + case .m_getCourseDates__courseID_courseID: return ".getCourseDates(courseID:)" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + + public static func getUserProfile(username: Parameter, willReturn: UserProfile...) -> MethodStub { + return Given(method: .m_getUserProfile__username_username(`username`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getMyProfile(willReturn: UserProfile...) -> MethodStub { + return Given(method: .m_getMyProfile, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getMyProfileOffline(willReturn: UserProfile?...) -> MethodStub { + return Given(method: .m_getMyProfileOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getSpokenLanguages(willReturn: [PickerFields.Option]...) -> MethodStub { + return Given(method: .m_getSpokenLanguages, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getCountries(willReturn: [PickerFields.Option]...) -> MethodStub { + return Given(method: .m_getCountries, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func deleteProfilePicture(willReturn: Bool...) -> MethodStub { + return Given(method: .m_deleteProfilePicture, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func updateUserProfile(parameters: Parameter<[String: Any]>, willReturn: UserProfile...) -> MethodStub { + return Given(method: .m_updateUserProfile__parameters_parameters(`parameters`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func deleteAccount(password: Parameter, willReturn: Bool...) -> MethodStub { + return Given(method: .m_deleteAccount__password_password(`password`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getSettings(willReturn: UserSettings...) -> MethodStub { + return Given(method: .m_getSettings, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func enrollmentsStatus(willReturn: [CourseForSync]...) -> MethodStub { + return Given(method: .m_enrollmentsStatus, products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getCourseDates(courseID: Parameter, willReturn: CourseDates...) -> MethodStub { + return Given(method: .m_getCourseDates__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) + } + public static func getMyProfileOffline(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [UserProfile?] = [] + let given: Given = { return Given(method: .m_getMyProfileOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (UserProfile?).self) + willProduce(stubber) + return given + } + public static func getSpokenLanguages(willProduce: (Stubber<[PickerFields.Option]>) -> Void) -> MethodStub { + let willReturn: [[PickerFields.Option]] = [] + let given: Given = { return Given(method: .m_getSpokenLanguages, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([PickerFields.Option]).self) + willProduce(stubber) + return given + } + public static func getCountries(willProduce: (Stubber<[PickerFields.Option]>) -> Void) -> MethodStub { + let willReturn: [[PickerFields.Option]] = [] + let given: Given = { return Given(method: .m_getCountries, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([PickerFields.Option]).self) + willProduce(stubber) + return given + } + public static func getSettings(willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [UserSettings] = [] + let given: Given = { return Given(method: .m_getSettings, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (UserSettings).self) + willProduce(stubber) + return given + } + public static func getUserProfile(username: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getUserProfile__username_username(`username`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getUserProfile(username: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getUserProfile__username_username(`username`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (UserProfile).self) + willProduce(stubber) + return given + } + public static func getMyProfile(willThrow: Error...) -> MethodStub { + return Given(method: .m_getMyProfile, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getMyProfile(willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getMyProfile, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (UserProfile).self) + willProduce(stubber) + return given + } + public static func logOut(willThrow: Error...) -> MethodStub { + return Given(method: .m_logOut, products: willThrow.map({ StubProduct.throw($0) })) } - func assertionName() -> String { - switch self { - case .m_profileEditClicked: return ".profileEditClicked()" - case .m_profileSwitch__action_action: return ".profileSwitch(action:)" - case .m_profileEditDoneClicked: return ".profileEditDoneClicked()" - case .m_profileDeleteAccountClicked: return ".profileDeleteAccountClicked()" - case .m_profileVideoSettingsClicked: return ".profileVideoSettingsClicked()" - case .m_privacyPolicyClicked: return ".privacyPolicyClicked()" - case .m_cookiePolicyClicked: return ".cookiePolicyClicked()" - case .m_emailSupportClicked: return ".emailSupportClicked()" - case .m_faqClicked: return ".faqClicked()" - case .m_tosClicked: return ".tosClicked()" - case .m_dataSellClicked: return ".dataSellClicked()" - case .m_userLogout__force_force: return ".userLogout(force:)" - case .m_profileWifiToggle__action_action: return ".profileWifiToggle(action:)" - case .m_profileUserDeleteAccountClicked: return ".profileUserDeleteAccountClicked()" - case .m_profileDeleteAccountSuccess__success_success: return ".profileDeleteAccountSuccess(success:)" - case .m_profileEvent__eventbiValue_biValue: return ".profileEvent(_:biValue:)" - } + public static func logOut(willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_logOut, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given } - } - - open class Given: StubbedMethod { - fileprivate var method: MethodType - - private init(method: MethodType, products: [StubProduct]) { - self.method = method - super.init(products) + public static func uploadProfilePicture(pictureData: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_uploadProfilePicture__pictureData_pictureData(`pictureData`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func uploadProfilePicture(pictureData: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_uploadProfilePicture__pictureData_pictureData(`pictureData`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Void).self) + willProduce(stubber) + return given + } + public static func deleteProfilePicture(willThrow: Error...) -> MethodStub { + return Given(method: .m_deleteProfilePicture, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func deleteProfilePicture(willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_deleteProfilePicture, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Bool).self) + willProduce(stubber) + return given + } + public static func updateUserProfile(parameters: Parameter<[String: Any]>, willThrow: Error...) -> MethodStub { + return Given(method: .m_updateUserProfile__parameters_parameters(`parameters`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func updateUserProfile(parameters: Parameter<[String: Any]>, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_updateUserProfile__parameters_parameters(`parameters`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (UserProfile).self) + willProduce(stubber) + return given + } + public static func deleteAccount(password: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_deleteAccount__password_password(`password`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func deleteAccount(password: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_deleteAccount__password_password(`password`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (Bool).self) + willProduce(stubber) + return given + } + public static func enrollmentsStatus(willThrow: Error...) -> MethodStub { + return Given(method: .m_enrollmentsStatus, products: willThrow.map({ StubProduct.throw($0) })) + } + public static func enrollmentsStatus(willProduce: (StubberThrows<[CourseForSync]>) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_enrollmentsStatus, products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: ([CourseForSync]).self) + willProduce(stubber) + return given + } + public static func getCourseDates(courseID: Parameter, willThrow: Error...) -> MethodStub { + return Given(method: .m_getCourseDates__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) + } + public static func getCourseDates(courseID: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { + let willThrow: [Error] = [] + let given: Given = { return Given(method: .m_getCourseDates__courseID_courseID(`courseID`), products: willThrow.map({ StubProduct.throw($0) })) }() + let stubber = given.stubThrows(for: (CourseDates).self) + willProduce(stubber) + return given } - - } public struct Verify { fileprivate var method: MethodType - public static func profileEditClicked() -> Verify { return Verify(method: .m_profileEditClicked)} - public static func profileSwitch(action: Parameter) -> Verify { return Verify(method: .m_profileSwitch__action_action(`action`))} - public static func profileEditDoneClicked() -> Verify { return Verify(method: .m_profileEditDoneClicked)} - public static func profileDeleteAccountClicked() -> Verify { return Verify(method: .m_profileDeleteAccountClicked)} - public static func profileVideoSettingsClicked() -> Verify { return Verify(method: .m_profileVideoSettingsClicked)} - public static func privacyPolicyClicked() -> Verify { return Verify(method: .m_privacyPolicyClicked)} - public static func cookiePolicyClicked() -> Verify { return Verify(method: .m_cookiePolicyClicked)} - public static func emailSupportClicked() -> Verify { return Verify(method: .m_emailSupportClicked)} - public static func faqClicked() -> Verify { return Verify(method: .m_faqClicked)} - public static func tosClicked() -> Verify { return Verify(method: .m_tosClicked)} - public static func dataSellClicked() -> Verify { return Verify(method: .m_dataSellClicked)} - public static func userLogout(force: Parameter) -> Verify { return Verify(method: .m_userLogout__force_force(`force`))} - public static func profileWifiToggle(action: Parameter) -> Verify { return Verify(method: .m_profileWifiToggle__action_action(`action`))} - public static func profileUserDeleteAccountClicked() -> Verify { return Verify(method: .m_profileUserDeleteAccountClicked)} - public static func profileDeleteAccountSuccess(success: Parameter) -> Verify { return Verify(method: .m_profileDeleteAccountSuccess__success_success(`success`))} - public static func profileEvent(_ event: Parameter, biValue: Parameter) -> Verify { return Verify(method: .m_profileEvent__eventbiValue_biValue(`event`, `biValue`))} + public static func getUserProfile(username: Parameter) -> Verify { return Verify(method: .m_getUserProfile__username_username(`username`))} + public static func getMyProfile() -> Verify { return Verify(method: .m_getMyProfile)} + public static func getMyProfileOffline() -> Verify { return Verify(method: .m_getMyProfileOffline)} + public static func logOut() -> Verify { return Verify(method: .m_logOut)} + public static func getSpokenLanguages() -> Verify { return Verify(method: .m_getSpokenLanguages)} + public static func getCountries() -> Verify { return Verify(method: .m_getCountries)} + public static func uploadProfilePicture(pictureData: Parameter) -> Verify { return Verify(method: .m_uploadProfilePicture__pictureData_pictureData(`pictureData`))} + public static func deleteProfilePicture() -> Verify { return Verify(method: .m_deleteProfilePicture)} + public static func updateUserProfile(parameters: Parameter<[String: Any]>) -> Verify { return Verify(method: .m_updateUserProfile__parameters_parameters(`parameters`))} + public static func deleteAccount(password: Parameter) -> Verify { return Verify(method: .m_deleteAccount__password_password(`password`))} + public static func getSettings() -> Verify { return Verify(method: .m_getSettings)} + public static func saveSettings(_ settings: Parameter) -> Verify { return Verify(method: .m_saveSettings__settings(`settings`))} + public static func enrollmentsStatus() -> Verify { return Verify(method: .m_enrollmentsStatus)} + public static func getCourseDates(courseID: Parameter) -> Verify { return Verify(method: .m_getCourseDates__courseID_courseID(`courseID`))} } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func profileEditClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_profileEditClicked, performs: perform) - } - public static func profileSwitch(action: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_profileSwitch__action_action(`action`), performs: perform) - } - public static func profileEditDoneClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_profileEditDoneClicked, performs: perform) + public static func getUserProfile(username: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getUserProfile__username_username(`username`), performs: perform) } - public static func profileDeleteAccountClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_profileDeleteAccountClicked, performs: perform) + public static func getMyProfile(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getMyProfile, performs: perform) } - public static func profileVideoSettingsClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_profileVideoSettingsClicked, performs: perform) + public static func getMyProfileOffline(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getMyProfileOffline, performs: perform) } - public static func privacyPolicyClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_privacyPolicyClicked, performs: perform) + public static func logOut(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_logOut, performs: perform) } - public static func cookiePolicyClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_cookiePolicyClicked, performs: perform) + public static func getSpokenLanguages(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getSpokenLanguages, performs: perform) } - public static func emailSupportClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_emailSupportClicked, performs: perform) + public static func getCountries(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getCountries, performs: perform) } - public static func faqClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_faqClicked, performs: perform) + public static func uploadProfilePicture(pictureData: Parameter, perform: @escaping (Data) -> Void) -> Perform { + return Perform(method: .m_uploadProfilePicture__pictureData_pictureData(`pictureData`), performs: perform) } - public static func tosClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_tosClicked, performs: perform) + public static func deleteProfilePicture(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteProfilePicture, performs: perform) } - public static func dataSellClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_dataSellClicked, performs: perform) + public static func updateUserProfile(parameters: Parameter<[String: Any]>, perform: @escaping ([String: Any]) -> Void) -> Perform { + return Perform(method: .m_updateUserProfile__parameters_parameters(`parameters`), performs: perform) } - public static func userLogout(force: Parameter, perform: @escaping (Bool) -> Void) -> Perform { - return Perform(method: .m_userLogout__force_force(`force`), performs: perform) + public static func deleteAccount(password: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_deleteAccount__password_password(`password`), performs: perform) } - public static func profileWifiToggle(action: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_profileWifiToggle__action_action(`action`), performs: perform) + public static func getSettings(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getSettings, performs: perform) } - public static func profileUserDeleteAccountClicked(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_profileUserDeleteAccountClicked, performs: perform) + public static func saveSettings(_ settings: Parameter, perform: @escaping (UserSettings) -> Void) -> Perform { + return Perform(method: .m_saveSettings__settings(`settings`), performs: perform) } - public static func profileDeleteAccountSuccess(success: Parameter, perform: @escaping (Bool) -> Void) -> Perform { - return Perform(method: .m_profileDeleteAccountSuccess__success_success(`success`), performs: perform) + public static func enrollmentsStatus(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_enrollmentsStatus, performs: perform) } - public static func profileEvent(_ event: Parameter, biValue: Parameter, perform: @escaping (AnalyticsEvent, EventBIValue) -> Void) -> Perform { - return Perform(method: .m_profileEvent__eventbiValue_biValue(`event`, `biValue`), performs: perform) + public static func getCourseDates(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getCourseDates__courseID_courseID(`courseID`), performs: perform) } } @@ -2417,9 +5345,9 @@ open class ProfileAnalyticsMock: ProfileAnalytics, Mock { } } -// MARK: - ProfileInteractorProtocol +// MARK: - ProfilePersistenceProtocol -open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { +open class ProfilePersistenceProtocolMock: ProfilePersistenceProtocol, Mock { public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { SwiftyMockyTestObserver.setup() self.sequencingPolicy = sequencingPolicy @@ -2458,230 +5386,134 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { } - - - - open func getUserProfile(username: String) throws -> UserProfile { - addInvocation(.m_getUserProfile__username_username(Parameter.value(`username`))) - let perform = methodPerformValue(.m_getUserProfile__username_username(Parameter.value(`username`))) as? (String) -> Void - perform?(`username`) - var __value: UserProfile - do { - __value = try methodReturnValue(.m_getUserProfile__username_username(Parameter.value(`username`))).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getUserProfile(username: String). Use given") - Failure("Stub return value not specified for getUserProfile(username: String). Use given") - } catch { - throw error - } - return __value - } - - open func getMyProfile() throws -> UserProfile { - addInvocation(.m_getMyProfile) - let perform = methodPerformValue(.m_getMyProfile) as? () -> Void - perform?() - var __value: UserProfile - do { - __value = try methodReturnValue(.m_getMyProfile).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for getMyProfile(). Use given") - Failure("Stub return value not specified for getMyProfile(). Use given") - } catch { - throw error - } - return __value - } - - open func getMyProfileOffline() -> UserProfile? { - addInvocation(.m_getMyProfileOffline) - let perform = methodPerformValue(.m_getMyProfileOffline) as? () -> Void - perform?() - var __value: UserProfile? = nil - do { - __value = try methodReturnValue(.m_getMyProfileOffline).casted() - } catch { - // do nothing - } - return __value - } - - open func logOut() throws { - addInvocation(.m_logOut) - let perform = methodPerformValue(.m_logOut) as? () -> Void - perform?() - do { - _ = try methodReturnValue(.m_logOut).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } - } - - open func getSpokenLanguages() -> [PickerFields.Option] { - addInvocation(.m_getSpokenLanguages) - let perform = methodPerformValue(.m_getSpokenLanguages) as? () -> Void - perform?() - var __value: [PickerFields.Option] + + + + open func getCourseState(courseID: String) -> CourseCalendarState? { + addInvocation(.m_getCourseState__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_getCourseState__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + var __value: CourseCalendarState? = nil do { - __value = try methodReturnValue(.m_getSpokenLanguages).casted() + __value = try methodReturnValue(.m_getCourseState__courseID_courseID(Parameter.value(`courseID`))).casted() } catch { - onFatalFailure("Stub return value not specified for getSpokenLanguages(). Use given") - Failure("Stub return value not specified for getSpokenLanguages(). Use given") + // do nothing } return __value } - open func getCountries() -> [PickerFields.Option] { - addInvocation(.m_getCountries) - let perform = methodPerformValue(.m_getCountries) as? () -> Void + open func getAllCourseStates() -> [CourseCalendarState] { + addInvocation(.m_getAllCourseStates) + let perform = methodPerformValue(.m_getAllCourseStates) as? () -> Void perform?() - var __value: [PickerFields.Option] + var __value: [CourseCalendarState] do { - __value = try methodReturnValue(.m_getCountries).casted() + __value = try methodReturnValue(.m_getAllCourseStates).casted() } catch { - onFatalFailure("Stub return value not specified for getCountries(). Use given") - Failure("Stub return value not specified for getCountries(). Use given") + onFatalFailure("Stub return value not specified for getAllCourseStates(). Use given") + Failure("Stub return value not specified for getAllCourseStates(). Use given") } return __value } - open func uploadProfilePicture(pictureData: Data) throws { - addInvocation(.m_uploadProfilePicture__pictureData_pictureData(Parameter.value(`pictureData`))) - let perform = methodPerformValue(.m_uploadProfilePicture__pictureData_pictureData(Parameter.value(`pictureData`))) as? (Data) -> Void - perform?(`pictureData`) - do { - _ = try methodReturnValue(.m_uploadProfilePicture__pictureData_pictureData(Parameter.value(`pictureData`))).casted() as Void - } catch MockError.notStubed { - // do nothing - } catch { - throw error - } + open func saveCourseState(state: CourseCalendarState) { + addInvocation(.m_saveCourseState__state_state(Parameter.value(`state`))) + let perform = methodPerformValue(.m_saveCourseState__state_state(Parameter.value(`state`))) as? (CourseCalendarState) -> Void + perform?(`state`) } - open func deleteProfilePicture() throws -> Bool { - addInvocation(.m_deleteProfilePicture) - let perform = methodPerformValue(.m_deleteProfilePicture) as? () -> Void + open func removeCourseState(courseID: String) { + addInvocation(.m_removeCourseState__courseID_courseID(Parameter.value(`courseID`))) + let perform = methodPerformValue(.m_removeCourseState__courseID_courseID(Parameter.value(`courseID`))) as? (String) -> Void + perform?(`courseID`) + } + + open func deleteAllCourseStatesAndEvents() { + addInvocation(.m_deleteAllCourseStatesAndEvents) + let perform = methodPerformValue(.m_deleteAllCourseStatesAndEvents) as? () -> Void perform?() - var __value: Bool - do { - __value = try methodReturnValue(.m_deleteProfilePicture).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for deleteProfilePicture(). Use given") - Failure("Stub return value not specified for deleteProfilePicture(). Use given") - } catch { - throw error - } - return __value } - open func updateUserProfile(parameters: [String: Any]) throws -> UserProfile { - addInvocation(.m_updateUserProfile__parameters_parameters(Parameter<[String: Any]>.value(`parameters`))) - let perform = methodPerformValue(.m_updateUserProfile__parameters_parameters(Parameter<[String: Any]>.value(`parameters`))) as? ([String: Any]) -> Void - perform?(`parameters`) - var __value: UserProfile - do { - __value = try methodReturnValue(.m_updateUserProfile__parameters_parameters(Parameter<[String: Any]>.value(`parameters`))).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for updateUserProfile(parameters: [String: Any]). Use given") - Failure("Stub return value not specified for updateUserProfile(parameters: [String: Any]). Use given") - } catch { - throw error - } - return __value + open func saveCourseCalendarEvent(_ event: CourseCalendarEvent) { + addInvocation(.m_saveCourseCalendarEvent__event(Parameter.value(`event`))) + let perform = methodPerformValue(.m_saveCourseCalendarEvent__event(Parameter.value(`event`))) as? (CourseCalendarEvent) -> Void + perform?(`event`) } - open func deleteAccount(password: String) throws -> Bool { - addInvocation(.m_deleteAccount__password_password(Parameter.value(`password`))) - let perform = methodPerformValue(.m_deleteAccount__password_password(Parameter.value(`password`))) as? (String) -> Void - perform?(`password`) - var __value: Bool - do { - __value = try methodReturnValue(.m_deleteAccount__password_password(Parameter.value(`password`))).casted() - } catch MockError.notStubed { - onFatalFailure("Stub return value not specified for deleteAccount(password: String). Use given") - Failure("Stub return value not specified for deleteAccount(password: String). Use given") - } catch { - throw error - } - return __value + open func removeCourseCalendarEvents(for courseId: String) { + addInvocation(.m_removeCourseCalendarEvents__for_courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_removeCourseCalendarEvents__for_courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) } - open func getSettings() -> UserSettings { - addInvocation(.m_getSettings) - let perform = methodPerformValue(.m_getSettings) as? () -> Void + open func removeAllCourseCalendarEvents() { + addInvocation(.m_removeAllCourseCalendarEvents) + let perform = methodPerformValue(.m_removeAllCourseCalendarEvents) as? () -> Void perform?() - var __value: UserSettings + } + + open func getCourseCalendarEvents(for courseId: String) -> [CourseCalendarEvent] { + addInvocation(.m_getCourseCalendarEvents__for_courseId(Parameter.value(`courseId`))) + let perform = methodPerformValue(.m_getCourseCalendarEvents__for_courseId(Parameter.value(`courseId`))) as? (String) -> Void + perform?(`courseId`) + var __value: [CourseCalendarEvent] do { - __value = try methodReturnValue(.m_getSettings).casted() + __value = try methodReturnValue(.m_getCourseCalendarEvents__for_courseId(Parameter.value(`courseId`))).casted() } catch { - onFatalFailure("Stub return value not specified for getSettings(). Use given") - Failure("Stub return value not specified for getSettings(). Use given") + onFatalFailure("Stub return value not specified for getCourseCalendarEvents(for courseId: String). Use given") + Failure("Stub return value not specified for getCourseCalendarEvents(for courseId: String). Use given") } return __value } - open func saveSettings(_ settings: UserSettings) { - addInvocation(.m_saveSettings__settings(Parameter.value(`settings`))) - let perform = methodPerformValue(.m_saveSettings__settings(Parameter.value(`settings`))) as? (UserSettings) -> Void - perform?(`settings`) - } - fileprivate enum MethodType { - case m_getUserProfile__username_username(Parameter) - case m_getMyProfile - case m_getMyProfileOffline - case m_logOut - case m_getSpokenLanguages - case m_getCountries - case m_uploadProfilePicture__pictureData_pictureData(Parameter) - case m_deleteProfilePicture - case m_updateUserProfile__parameters_parameters(Parameter<[String: Any]>) - case m_deleteAccount__password_password(Parameter) - case m_getSettings - case m_saveSettings__settings(Parameter) + case m_getCourseState__courseID_courseID(Parameter) + case m_getAllCourseStates + case m_saveCourseState__state_state(Parameter) + case m_removeCourseState__courseID_courseID(Parameter) + case m_deleteAllCourseStatesAndEvents + case m_saveCourseCalendarEvent__event(Parameter) + case m_removeCourseCalendarEvents__for_courseId(Parameter) + case m_removeAllCourseCalendarEvents + case m_getCourseCalendarEvents__for_courseId(Parameter) static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { switch (lhs, rhs) { - case (.m_getUserProfile__username_username(let lhsUsername), .m_getUserProfile__username_username(let rhsUsername)): + case (.m_getCourseState__courseID_courseID(let lhsCourseid), .m_getCourseState__courseID_courseID(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUsername, rhs: rhsUsername, with: matcher), lhsUsername, rhsUsername, "username")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) return Matcher.ComparisonResult(results) - case (.m_getMyProfile, .m_getMyProfile): return .match - - case (.m_getMyProfileOffline, .m_getMyProfileOffline): return .match - - case (.m_logOut, .m_logOut): return .match - - case (.m_getSpokenLanguages, .m_getSpokenLanguages): return .match + case (.m_getAllCourseStates, .m_getAllCourseStates): return .match - case (.m_getCountries, .m_getCountries): return .match + case (.m_saveCourseState__state_state(let lhsState), .m_saveCourseState__state_state(let rhsState)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsState, rhs: rhsState, with: matcher), lhsState, rhsState, "state")) + return Matcher.ComparisonResult(results) - case (.m_uploadProfilePicture__pictureData_pictureData(let lhsPicturedata), .m_uploadProfilePicture__pictureData_pictureData(let rhsPicturedata)): + case (.m_removeCourseState__courseID_courseID(let lhsCourseid), .m_removeCourseState__courseID_courseID(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPicturedata, rhs: rhsPicturedata, with: matcher), lhsPicturedata, rhsPicturedata, "pictureData")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "courseID")) return Matcher.ComparisonResult(results) - case (.m_deleteProfilePicture, .m_deleteProfilePicture): return .match + case (.m_deleteAllCourseStatesAndEvents, .m_deleteAllCourseStatesAndEvents): return .match - case (.m_updateUserProfile__parameters_parameters(let lhsParameters), .m_updateUserProfile__parameters_parameters(let rhsParameters)): + case (.m_saveCourseCalendarEvent__event(let lhsEvent), .m_saveCourseCalendarEvent__event(let rhsEvent)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsParameters, rhs: rhsParameters, with: matcher), lhsParameters, rhsParameters, "parameters")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsEvent, rhs: rhsEvent, with: matcher), lhsEvent, rhsEvent, "_ event")) return Matcher.ComparisonResult(results) - case (.m_deleteAccount__password_password(let lhsPassword), .m_deleteAccount__password_password(let rhsPassword)): + case (.m_removeCourseCalendarEvents__for_courseId(let lhsCourseid), .m_removeCourseCalendarEvents__for_courseId(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsPassword, rhs: rhsPassword, with: matcher), lhsPassword, rhsPassword, "password")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "for courseId")) return Matcher.ComparisonResult(results) - case (.m_getSettings, .m_getSettings): return .match + case (.m_removeAllCourseCalendarEvents, .m_removeAllCourseCalendarEvents): return .match - case (.m_saveSettings__settings(let lhsSettings), .m_saveSettings__settings(let rhsSettings)): + case (.m_getCourseCalendarEvents__for_courseId(let lhsCourseid), .m_getCourseCalendarEvents__for_courseId(let rhsCourseid)): var results: [Matcher.ParameterComparisonResult] = [] - results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsSettings, rhs: rhsSettings, with: matcher), lhsSettings, rhsSettings, "_ settings")) + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsCourseid, rhs: rhsCourseid, with: matcher), lhsCourseid, rhsCourseid, "for courseId")) return Matcher.ComparisonResult(results) default: return .none } @@ -2689,34 +5521,28 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { func intValue() -> Int { switch self { - case let .m_getUserProfile__username_username(p0): return p0.intValue - case .m_getMyProfile: return 0 - case .m_getMyProfileOffline: return 0 - case .m_logOut: return 0 - case .m_getSpokenLanguages: return 0 - case .m_getCountries: return 0 - case let .m_uploadProfilePicture__pictureData_pictureData(p0): return p0.intValue - case .m_deleteProfilePicture: return 0 - case let .m_updateUserProfile__parameters_parameters(p0): return p0.intValue - case let .m_deleteAccount__password_password(p0): return p0.intValue - case .m_getSettings: return 0 - case let .m_saveSettings__settings(p0): return p0.intValue + case let .m_getCourseState__courseID_courseID(p0): return p0.intValue + case .m_getAllCourseStates: return 0 + case let .m_saveCourseState__state_state(p0): return p0.intValue + case let .m_removeCourseState__courseID_courseID(p0): return p0.intValue + case .m_deleteAllCourseStatesAndEvents: return 0 + case let .m_saveCourseCalendarEvent__event(p0): return p0.intValue + case let .m_removeCourseCalendarEvents__for_courseId(p0): return p0.intValue + case .m_removeAllCourseCalendarEvents: return 0 + case let .m_getCourseCalendarEvents__for_courseId(p0): return p0.intValue } } func assertionName() -> String { switch self { - case .m_getUserProfile__username_username: return ".getUserProfile(username:)" - case .m_getMyProfile: return ".getMyProfile()" - case .m_getMyProfileOffline: return ".getMyProfileOffline()" - case .m_logOut: return ".logOut()" - case .m_getSpokenLanguages: return ".getSpokenLanguages()" - case .m_getCountries: return ".getCountries()" - case .m_uploadProfilePicture__pictureData_pictureData: return ".uploadProfilePicture(pictureData:)" - case .m_deleteProfilePicture: return ".deleteProfilePicture()" - case .m_updateUserProfile__parameters_parameters: return ".updateUserProfile(parameters:)" - case .m_deleteAccount__password_password: return ".deleteAccount(password:)" - case .m_getSettings: return ".getSettings()" - case .m_saveSettings__settings: return ".saveSettings(_:)" + case .m_getCourseState__courseID_courseID: return ".getCourseState(courseID:)" + case .m_getAllCourseStates: return ".getAllCourseStates()" + case .m_saveCourseState__state_state: return ".saveCourseState(state:)" + case .m_removeCourseState__courseID_courseID: return ".removeCourseState(courseID:)" + case .m_deleteAllCourseStatesAndEvents: return ".deleteAllCourseStatesAndEvents()" + case .m_saveCourseCalendarEvent__event: return ".saveCourseCalendarEvent(_:)" + case .m_removeCourseCalendarEvents__for_courseId: return ".removeCourseCalendarEvents(for:)" + case .m_removeAllCourseCalendarEvents: return ".removeAllCourseCalendarEvents()" + case .m_getCourseCalendarEvents__for_courseId: return ".getCourseCalendarEvents(for:)" } } } @@ -2730,128 +5556,33 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { } - public static func getUserProfile(username: Parameter, willReturn: UserProfile...) -> MethodStub { - return Given(method: .m_getUserProfile__username_username(`username`), products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func getMyProfile(willReturn: UserProfile...) -> MethodStub { - return Given(method: .m_getMyProfile, products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func getMyProfileOffline(willReturn: UserProfile?...) -> MethodStub { - return Given(method: .m_getMyProfileOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func getSpokenLanguages(willReturn: [PickerFields.Option]...) -> MethodStub { - return Given(method: .m_getSpokenLanguages, products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func getCountries(willReturn: [PickerFields.Option]...) -> MethodStub { - return Given(method: .m_getCountries, products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func deleteProfilePicture(willReturn: Bool...) -> MethodStub { - return Given(method: .m_deleteProfilePicture, products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func updateUserProfile(parameters: Parameter<[String: Any]>, willReturn: UserProfile...) -> MethodStub { - return Given(method: .m_updateUserProfile__parameters_parameters(`parameters`), products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func deleteAccount(password: Parameter, willReturn: Bool...) -> MethodStub { - return Given(method: .m_deleteAccount__password_password(`password`), products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func getSettings(willReturn: UserSettings...) -> MethodStub { - return Given(method: .m_getSettings, products: willReturn.map({ StubProduct.return($0 as Any) })) - } - public static func getMyProfileOffline(willProduce: (Stubber) -> Void) -> MethodStub { - let willReturn: [UserProfile?] = [] - let given: Given = { return Given(method: .m_getMyProfileOffline, products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: (UserProfile?).self) - willProduce(stubber) - return given - } - public static func getSpokenLanguages(willProduce: (Stubber<[PickerFields.Option]>) -> Void) -> MethodStub { - let willReturn: [[PickerFields.Option]] = [] - let given: Given = { return Given(method: .m_getSpokenLanguages, products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: ([PickerFields.Option]).self) - willProduce(stubber) - return given - } - public static func getCountries(willProduce: (Stubber<[PickerFields.Option]>) -> Void) -> MethodStub { - let willReturn: [[PickerFields.Option]] = [] - let given: Given = { return Given(method: .m_getCountries, products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: ([PickerFields.Option]).self) - willProduce(stubber) - return given + public static func getCourseState(courseID: Parameter, willReturn: CourseCalendarState?...) -> MethodStub { + return Given(method: .m_getCourseState__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func getSettings(willProduce: (Stubber) -> Void) -> MethodStub { - let willReturn: [UserSettings] = [] - let given: Given = { return Given(method: .m_getSettings, products: willReturn.map({ StubProduct.return($0 as Any) })) }() - let stubber = given.stub(for: (UserSettings).self) - willProduce(stubber) - return given - } - public static func getUserProfile(username: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_getUserProfile__username_username(`username`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func getUserProfile(username: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getUserProfile__username_username(`username`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (UserProfile).self) - willProduce(stubber) - return given - } - public static func getMyProfile(willThrow: Error...) -> MethodStub { - return Given(method: .m_getMyProfile, products: willThrow.map({ StubProduct.throw($0) })) - } - public static func getMyProfile(willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_getMyProfile, products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (UserProfile).self) - willProduce(stubber) - return given - } - public static func logOut(willThrow: Error...) -> MethodStub { - return Given(method: .m_logOut, products: willThrow.map({ StubProduct.throw($0) })) - } - public static func logOut(willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_logOut, products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) - willProduce(stubber) - return given - } - public static func uploadProfilePicture(pictureData: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_uploadProfilePicture__pictureData_pictureData(`pictureData`), products: willThrow.map({ StubProduct.throw($0) })) - } - public static func uploadProfilePicture(pictureData: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_uploadProfilePicture__pictureData_pictureData(`pictureData`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Void).self) - willProduce(stubber) - return given - } - public static func deleteProfilePicture(willThrow: Error...) -> MethodStub { - return Given(method: .m_deleteProfilePicture, products: willThrow.map({ StubProduct.throw($0) })) - } - public static func deleteProfilePicture(willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_deleteProfilePicture, products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Bool).self) - willProduce(stubber) - return given + public static func getAllCourseStates(willReturn: [CourseCalendarState]...) -> MethodStub { + return Given(method: .m_getAllCourseStates, products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func updateUserProfile(parameters: Parameter<[String: Any]>, willThrow: Error...) -> MethodStub { - return Given(method: .m_updateUserProfile__parameters_parameters(`parameters`), products: willThrow.map({ StubProduct.throw($0) })) + public static func getCourseCalendarEvents(for courseId: Parameter, willReturn: [CourseCalendarEvent]...) -> MethodStub { + return Given(method: .m_getCourseCalendarEvents__for_courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) } - public static func updateUserProfile(parameters: Parameter<[String: Any]>, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_updateUserProfile__parameters_parameters(`parameters`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (UserProfile).self) + public static func getCourseState(courseID: Parameter, willProduce: (Stubber) -> Void) -> MethodStub { + let willReturn: [CourseCalendarState?] = [] + let given: Given = { return Given(method: .m_getCourseState__courseID_courseID(`courseID`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: (CourseCalendarState?).self) willProduce(stubber) return given } - public static func deleteAccount(password: Parameter, willThrow: Error...) -> MethodStub { - return Given(method: .m_deleteAccount__password_password(`password`), products: willThrow.map({ StubProduct.throw($0) })) + public static func getAllCourseStates(willProduce: (Stubber<[CourseCalendarState]>) -> Void) -> MethodStub { + let willReturn: [[CourseCalendarState]] = [] + let given: Given = { return Given(method: .m_getAllCourseStates, products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseCalendarState]).self) + willProduce(stubber) + return given } - public static func deleteAccount(password: Parameter, willProduce: (StubberThrows) -> Void) -> MethodStub { - let willThrow: [Error] = [] - let given: Given = { return Given(method: .m_deleteAccount__password_password(`password`), products: willThrow.map({ StubProduct.throw($0) })) }() - let stubber = given.stubThrows(for: (Bool).self) + public static func getCourseCalendarEvents(for courseId: Parameter, willProduce: (Stubber<[CourseCalendarEvent]>) -> Void) -> MethodStub { + let willReturn: [[CourseCalendarEvent]] = [] + let given: Given = { return Given(method: .m_getCourseCalendarEvents__for_courseId(`courseId`), products: willReturn.map({ StubProduct.return($0 as Any) })) }() + let stubber = given.stub(for: ([CourseCalendarEvent]).self) willProduce(stubber) return given } @@ -2860,59 +5591,47 @@ open class ProfileInteractorProtocolMock: ProfileInteractorProtocol, Mock { public struct Verify { fileprivate var method: MethodType - public static func getUserProfile(username: Parameter) -> Verify { return Verify(method: .m_getUserProfile__username_username(`username`))} - public static func getMyProfile() -> Verify { return Verify(method: .m_getMyProfile)} - public static func getMyProfileOffline() -> Verify { return Verify(method: .m_getMyProfileOffline)} - public static func logOut() -> Verify { return Verify(method: .m_logOut)} - public static func getSpokenLanguages() -> Verify { return Verify(method: .m_getSpokenLanguages)} - public static func getCountries() -> Verify { return Verify(method: .m_getCountries)} - public static func uploadProfilePicture(pictureData: Parameter) -> Verify { return Verify(method: .m_uploadProfilePicture__pictureData_pictureData(`pictureData`))} - public static func deleteProfilePicture() -> Verify { return Verify(method: .m_deleteProfilePicture)} - public static func updateUserProfile(parameters: Parameter<[String: Any]>) -> Verify { return Verify(method: .m_updateUserProfile__parameters_parameters(`parameters`))} - public static func deleteAccount(password: Parameter) -> Verify { return Verify(method: .m_deleteAccount__password_password(`password`))} - public static func getSettings() -> Verify { return Verify(method: .m_getSettings)} - public static func saveSettings(_ settings: Parameter) -> Verify { return Verify(method: .m_saveSettings__settings(`settings`))} + public static func getCourseState(courseID: Parameter) -> Verify { return Verify(method: .m_getCourseState__courseID_courseID(`courseID`))} + public static func getAllCourseStates() -> Verify { return Verify(method: .m_getAllCourseStates)} + public static func saveCourseState(state: Parameter) -> Verify { return Verify(method: .m_saveCourseState__state_state(`state`))} + public static func removeCourseState(courseID: Parameter) -> Verify { return Verify(method: .m_removeCourseState__courseID_courseID(`courseID`))} + public static func deleteAllCourseStatesAndEvents() -> Verify { return Verify(method: .m_deleteAllCourseStatesAndEvents)} + public static func saveCourseCalendarEvent(_ event: Parameter) -> Verify { return Verify(method: .m_saveCourseCalendarEvent__event(`event`))} + public static func removeCourseCalendarEvents(for courseId: Parameter) -> Verify { return Verify(method: .m_removeCourseCalendarEvents__for_courseId(`courseId`))} + public static func removeAllCourseCalendarEvents() -> Verify { return Verify(method: .m_removeAllCourseCalendarEvents)} + public static func getCourseCalendarEvents(for courseId: Parameter) -> Verify { return Verify(method: .m_getCourseCalendarEvents__for_courseId(`courseId`))} } public struct Perform { fileprivate var method: MethodType var performs: Any - public static func getUserProfile(username: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_getUserProfile__username_username(`username`), performs: perform) - } - public static func getMyProfile(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_getMyProfile, performs: perform) - } - public static func getMyProfileOffline(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_getMyProfileOffline, performs: perform) - } - public static func logOut(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_logOut, performs: perform) + public static func getCourseState(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getCourseState__courseID_courseID(`courseID`), performs: perform) } - public static func getSpokenLanguages(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_getSpokenLanguages, performs: perform) + public static func getAllCourseStates(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_getAllCourseStates, performs: perform) } - public static func getCountries(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_getCountries, performs: perform) + public static func saveCourseState(state: Parameter, perform: @escaping (CourseCalendarState) -> Void) -> Perform { + return Perform(method: .m_saveCourseState__state_state(`state`), performs: perform) } - public static func uploadProfilePicture(pictureData: Parameter, perform: @escaping (Data) -> Void) -> Perform { - return Perform(method: .m_uploadProfilePicture__pictureData_pictureData(`pictureData`), performs: perform) + public static func removeCourseState(courseID: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_removeCourseState__courseID_courseID(`courseID`), performs: perform) } - public static func deleteProfilePicture(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_deleteProfilePicture, performs: perform) + public static func deleteAllCourseStatesAndEvents(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_deleteAllCourseStatesAndEvents, performs: perform) } - public static func updateUserProfile(parameters: Parameter<[String: Any]>, perform: @escaping ([String: Any]) -> Void) -> Perform { - return Perform(method: .m_updateUserProfile__parameters_parameters(`parameters`), performs: perform) + public static func saveCourseCalendarEvent(_ event: Parameter, perform: @escaping (CourseCalendarEvent) -> Void) -> Perform { + return Perform(method: .m_saveCourseCalendarEvent__event(`event`), performs: perform) } - public static func deleteAccount(password: Parameter, perform: @escaping (String) -> Void) -> Perform { - return Perform(method: .m_deleteAccount__password_password(`password`), performs: perform) + public static func removeCourseCalendarEvents(for courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_removeCourseCalendarEvents__for_courseId(`courseId`), performs: perform) } - public static func getSettings(perform: @escaping () -> Void) -> Perform { - return Perform(method: .m_getSettings, performs: perform) + public static func removeAllCourseCalendarEvents(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_removeAllCourseCalendarEvents, performs: perform) } - public static func saveSettings(_ settings: Parameter, perform: @escaping (UserSettings) -> Void) -> Perform { - return Perform(method: .m_saveSettings__settings(`settings`), performs: perform) + public static func getCourseCalendarEvents(for courseId: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_getCourseCalendarEvents__for_courseId(`courseId`), performs: perform) } } @@ -3045,6 +5764,36 @@ open class ProfileRouterMock: ProfileRouter, Mock { perform?() } + open func showVideoSettings() { + addInvocation(.m_showVideoSettings) + let perform = methodPerformValue(.m_showVideoSettings) as? () -> Void + perform?() + } + + open func showManageAccount() { + addInvocation(.m_showManageAccount) + let perform = methodPerformValue(.m_showManageAccount) as? () -> Void + perform?() + } + + open func showDatesAndCalendar() { + addInvocation(.m_showDatesAndCalendar) + let perform = methodPerformValue(.m_showDatesAndCalendar) as? () -> Void + perform?() + } + + open func showSyncCalendarOptions() { + addInvocation(.m_showSyncCalendarOptions) + let perform = methodPerformValue(.m_showSyncCalendarOptions) as? () -> Void + perform?() + } + + open func showCoursesToSync() { + addInvocation(.m_showCoursesToSync) + let perform = methodPerformValue(.m_showCoursesToSync) as? () -> Void + perform?() + } + open func showVideoQualityView(viewModel: SettingsViewModel) { addInvocation(.m_showVideoQualityView__viewModel_viewModel(Parameter.value(`viewModel`))) let perform = methodPerformValue(.m_showVideoQualityView__viewModel_viewModel(Parameter.value(`viewModel`))) as? (SettingsViewModel) -> Void @@ -3135,6 +5884,12 @@ open class ProfileRouterMock: ProfileRouter, Mock { perform?(`title`, `url`) } + open func showSSOWebBrowser(title: String) { + addInvocation(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) + let perform = methodPerformValue(.m_showSSOWebBrowser__title_title(Parameter.value(`title`))) as? (String) -> Void + perform?(`title`) + } + open func presentAlert(alertTitle: String, alertMessage: String, positiveAction: String, onCloseTapped: @escaping () -> Void, okTapped: @escaping () -> Void, type: AlertViewType) { addInvocation(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) let perform = methodPerformValue(.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter.value(`alertTitle`), Parameter.value(`alertMessage`), Parameter.value(`positiveAction`), Parameter<() -> Void>.value(`onCloseTapped`), Parameter<() -> Void>.value(`okTapped`), Parameter.value(`type`))) as? (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void @@ -3163,6 +5918,11 @@ open class ProfileRouterMock: ProfileRouter, Mock { fileprivate enum MethodType { case m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit(Parameter, Parameter, Parameter<((UserProfile?, UIImage?)) -> Void>) case m_showSettings + case m_showVideoSettings + case m_showManageAccount + case m_showDatesAndCalendar + case m_showSyncCalendarOptions + case m_showCoursesToSync case m_showVideoQualityView__viewModel_viewModel(Parameter) case m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelectanalytics_analytics(Parameter, Parameter<((DownloadQuality) -> Void)?>, Parameter) case m_showDeleteProfileView @@ -3178,6 +5938,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { case m_showForgotPasswordScreen case m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(Parameter, Parameter) case m_showWebBrowser__title_titleurl_url(Parameter, Parameter) + case m_showSSOWebBrowser__title_title(Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter) case m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(Parameter, Parameter, Parameter, Parameter, Parameter, Parameter<() -> Void>, Parameter<() -> Void>, Parameter<() -> Void>) case m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(Parameter, Parameter, Parameter<(() -> Void)?>) @@ -3194,6 +5955,16 @@ open class ProfileRouterMock: ProfileRouter, Mock { case (.m_showSettings, .m_showSettings): return .match + case (.m_showVideoSettings, .m_showVideoSettings): return .match + + case (.m_showManageAccount, .m_showManageAccount): return .match + + case (.m_showDatesAndCalendar, .m_showDatesAndCalendar): return .match + + case (.m_showSyncCalendarOptions, .m_showSyncCalendarOptions): return .match + + case (.m_showCoursesToSync, .m_showCoursesToSync): return .match + case (.m_showVideoQualityView__viewModel_viewModel(let lhsViewmodel), .m_showVideoQualityView__viewModel_viewModel(let rhsViewmodel)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsViewmodel, rhs: rhsViewmodel, with: matcher), lhsViewmodel, rhsViewmodel, "viewModel")) @@ -3261,6 +6032,11 @@ open class ProfileRouterMock: ProfileRouter, Mock { results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsUrl, rhs: rhsUrl, with: matcher), lhsUrl, rhsUrl, "url")) return Matcher.ComparisonResult(results) + case (.m_showSSOWebBrowser__title_title(let lhsTitle), .m_showSSOWebBrowser__title_title(let rhsTitle)): + var results: [Matcher.ParameterComparisonResult] = [] + results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsTitle, rhs: rhsTitle, with: matcher), lhsTitle, rhsTitle, "title")) + return Matcher.ComparisonResult(results) + case (.m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let lhsAlerttitle, let lhsAlertmessage, let lhsPositiveaction, let lhsOnclosetapped, let lhsOktapped, let lhsType), .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(let rhsAlerttitle, let rhsAlertmessage, let rhsPositiveaction, let rhsOnclosetapped, let rhsOktapped, let rhsType)): var results: [Matcher.ParameterComparisonResult] = [] results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsAlerttitle, rhs: rhsAlerttitle, with: matcher), lhsAlerttitle, rhsAlerttitle, "alertTitle")) @@ -3304,6 +6080,11 @@ open class ProfileRouterMock: ProfileRouter, Mock { switch self { case let .m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case .m_showSettings: return 0 + case .m_showVideoSettings: return 0 + case .m_showManageAccount: return 0 + case .m_showDatesAndCalendar: return 0 + case .m_showSyncCalendarOptions: return 0 + case .m_showCoursesToSync: return 0 case let .m_showVideoQualityView__viewModel_viewModel(p0): return p0.intValue case let .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelectanalytics_analytics(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue case .m_showDeleteProfileView: return 0 @@ -3319,6 +6100,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { case .m_showForgotPasswordScreen: return 0 case let .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(p0, p1): return p0.intValue + p1.intValue case let .m_showWebBrowser__title_titleurl_url(p0, p1): return p0.intValue + p1.intValue + case let .m_showSSOWebBrowser__title_title(p0): return p0.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(p0, p1, p2, p3, p4, p5): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue case let .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(p0, p1, p2, p3, p4, p5, p6, p7): return p0.intValue + p1.intValue + p2.intValue + p3.intValue + p4.intValue + p5.intValue + p6.intValue + p7.intValue case let .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(p0, p1, p2): return p0.intValue + p1.intValue + p2.intValue @@ -3329,6 +6111,11 @@ open class ProfileRouterMock: ProfileRouter, Mock { switch self { case .m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit: return ".showEditProfile(userModel:avatar:profileDidEdit:)" case .m_showSettings: return ".showSettings()" + case .m_showVideoSettings: return ".showVideoSettings()" + case .m_showManageAccount: return ".showManageAccount()" + case .m_showDatesAndCalendar: return ".showDatesAndCalendar()" + case .m_showSyncCalendarOptions: return ".showSyncCalendarOptions()" + case .m_showCoursesToSync: return ".showCoursesToSync()" case .m_showVideoQualityView__viewModel_viewModel: return ".showVideoQualityView(viewModel:)" case .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelectanalytics_analytics: return ".showVideoDownloadQualityView(downloadQuality:didSelect:analytics:)" case .m_showDeleteProfileView: return ".showDeleteProfileView()" @@ -3344,6 +6131,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { case .m_showForgotPasswordScreen: return ".showForgotPasswordScreen()" case .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen: return ".showDiscoveryScreen(searchQuery:sourceScreen:)" case .m_showWebBrowser__title_titleurl_url: return ".showWebBrowser(title:url:)" + case .m_showSSOWebBrowser__title_title: return ".showSSOWebBrowser(title:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type: return ".presentAlert(alertTitle:alertMessage:positiveAction:onCloseTapped:okTapped:type:)" case .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped: return ".presentAlert(alertTitle:alertMessage:nextSectionName:action:image:onCloseTapped:okTapped:nextSectionTapped:)" case .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion: return ".presentView(transitionStyle:view:completion:)" @@ -3368,6 +6156,11 @@ open class ProfileRouterMock: ProfileRouter, Mock { public static func showEditProfile(userModel: Parameter, avatar: Parameter, profileDidEdit: Parameter<((UserProfile?, UIImage?)) -> Void>) -> Verify { return Verify(method: .m_showEditProfile__userModel_userModelavatar_avatarprofileDidEdit_profileDidEdit(`userModel`, `avatar`, `profileDidEdit`))} public static func showSettings() -> Verify { return Verify(method: .m_showSettings)} + public static func showVideoSettings() -> Verify { return Verify(method: .m_showVideoSettings)} + public static func showManageAccount() -> Verify { return Verify(method: .m_showManageAccount)} + public static func showDatesAndCalendar() -> Verify { return Verify(method: .m_showDatesAndCalendar)} + public static func showSyncCalendarOptions() -> Verify { return Verify(method: .m_showSyncCalendarOptions)} + public static func showCoursesToSync() -> Verify { return Verify(method: .m_showCoursesToSync)} public static func showVideoQualityView(viewModel: Parameter) -> Verify { return Verify(method: .m_showVideoQualityView__viewModel_viewModel(`viewModel`))} public static func showVideoDownloadQualityView(downloadQuality: Parameter, didSelect: Parameter<((DownloadQuality) -> Void)?>, analytics: Parameter) -> Verify { return Verify(method: .m_showVideoDownloadQualityView__downloadQuality_downloadQualitydidSelect_didSelectanalytics_analytics(`downloadQuality`, `didSelect`, `analytics`))} public static func showDeleteProfileView() -> Verify { return Verify(method: .m_showDeleteProfileView)} @@ -3383,6 +6176,7 @@ open class ProfileRouterMock: ProfileRouter, Mock { public static func showForgotPasswordScreen() -> Verify { return Verify(method: .m_showForgotPasswordScreen)} public static func showDiscoveryScreen(searchQuery: Parameter, sourceScreen: Parameter) -> Verify { return Verify(method: .m_showDiscoveryScreen__searchQuery_searchQuerysourceScreen_sourceScreen(`searchQuery`, `sourceScreen`))} public static func showWebBrowser(title: Parameter, url: Parameter) -> Verify { return Verify(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`))} + public static func showSSOWebBrowser(title: Parameter) -> Verify { return Verify(method: .m_showSSOWebBrowser__title_title(`title`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`))} public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, nextSectionName: Parameter, action: Parameter, image: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, nextSectionTapped: Parameter<() -> Void>) -> Verify { return Verify(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagenextSectionName_nextSectionNameaction_actionimage_imageonCloseTapped_onCloseTappedokTapped_okTappednextSectionTapped_nextSectionTapped(`alertTitle`, `alertMessage`, `nextSectionName`, `action`, `image`, `onCloseTapped`, `okTapped`, `nextSectionTapped`))} public static func presentView(transitionStyle: Parameter, view: Parameter, completion: Parameter<(() -> Void)?>) -> Verify { return Verify(method: .m_presentView__transitionStyle_transitionStyleview_viewcompletion_completion(`transitionStyle`, `view`, `completion`))} @@ -3399,6 +6193,21 @@ open class ProfileRouterMock: ProfileRouter, Mock { public static func showSettings(perform: @escaping () -> Void) -> Perform { return Perform(method: .m_showSettings, performs: perform) } + public static func showVideoSettings(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showVideoSettings, performs: perform) + } + public static func showManageAccount(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showManageAccount, performs: perform) + } + public static func showDatesAndCalendar(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showDatesAndCalendar, performs: perform) + } + public static func showSyncCalendarOptions(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showSyncCalendarOptions, performs: perform) + } + public static func showCoursesToSync(perform: @escaping () -> Void) -> Perform { + return Perform(method: .m_showCoursesToSync, performs: perform) + } public static func showVideoQualityView(viewModel: Parameter, perform: @escaping (SettingsViewModel) -> Void) -> Perform { return Perform(method: .m_showVideoQualityView__viewModel_viewModel(`viewModel`), performs: perform) } @@ -3444,6 +6253,9 @@ open class ProfileRouterMock: ProfileRouter, Mock { public static func showWebBrowser(title: Parameter, url: Parameter, perform: @escaping (String, URL) -> Void) -> Perform { return Perform(method: .m_showWebBrowser__title_titleurl_url(`title`, `url`), performs: perform) } + public static func showSSOWebBrowser(title: Parameter, perform: @escaping (String) -> Void) -> Perform { + return Perform(method: .m_showSSOWebBrowser__title_title(`title`), performs: perform) + } public static func presentAlert(alertTitle: Parameter, alertMessage: Parameter, positiveAction: Parameter, onCloseTapped: Parameter<() -> Void>, okTapped: Parameter<() -> Void>, type: Parameter, perform: @escaping (String, String, String, @escaping () -> Void, @escaping () -> Void, AlertViewType) -> Void) -> Perform { return Perform(method: .m_presentAlert__alertTitle_alertTitlealertMessage_alertMessagepositiveAction_positiveActiononCloseTapped_onCloseTappedokTapped_okTappedtype_type(`alertTitle`, `alertMessage`, `positiveAction`, `onCloseTapped`, `okTapped`, `type`), performs: perform) } @@ -3531,6 +6343,315 @@ open class ProfileRouterMock: ProfileRouter, Mock { } } +// MARK: - ProfileStorage + +open class ProfileStorageMock: ProfileStorage, Mock { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + + public var userProfile: DataLayer.UserProfile? { + get { invocations.append(.p_userProfile_get); return __p_userProfile ?? optionalGivenGetterValue(.p_userProfile_get, "ProfileStorageMock - stub value for userProfile was not defined") } + set { invocations.append(.p_userProfile_set(.value(newValue))); __p_userProfile = newValue } + } + private var __p_userProfile: (DataLayer.UserProfile)? + + public var useRelativeDates: Bool { + get { invocations.append(.p_useRelativeDates_get); return __p_useRelativeDates ?? givenGetterValue(.p_useRelativeDates_get, "ProfileStorageMock - stub value for useRelativeDates was not defined") } + set { invocations.append(.p_useRelativeDates_set(.value(newValue))); __p_useRelativeDates = newValue } + } + private var __p_useRelativeDates: (Bool)? + + public var calendarSettings: CalendarSettings? { + get { invocations.append(.p_calendarSettings_get); return __p_calendarSettings ?? optionalGivenGetterValue(.p_calendarSettings_get, "ProfileStorageMock - stub value for calendarSettings was not defined") } + set { invocations.append(.p_calendarSettings_set(.value(newValue))); __p_calendarSettings = newValue } + } + private var __p_calendarSettings: (CalendarSettings)? + + public var hideInactiveCourses: Bool? { + get { invocations.append(.p_hideInactiveCourses_get); return __p_hideInactiveCourses ?? optionalGivenGetterValue(.p_hideInactiveCourses_get, "ProfileStorageMock - stub value for hideInactiveCourses was not defined") } + set { invocations.append(.p_hideInactiveCourses_set(.value(newValue))); __p_hideInactiveCourses = newValue } + } + private var __p_hideInactiveCourses: (Bool)? + + public var lastLoginUsername: String? { + get { invocations.append(.p_lastLoginUsername_get); return __p_lastLoginUsername ?? optionalGivenGetterValue(.p_lastLoginUsername_get, "ProfileStorageMock - stub value for lastLoginUsername was not defined") } + set { invocations.append(.p_lastLoginUsername_set(.value(newValue))); __p_lastLoginUsername = newValue } + } + private var __p_lastLoginUsername: (String)? + + public var lastCalendarName: String? { + get { invocations.append(.p_lastCalendarName_get); return __p_lastCalendarName ?? optionalGivenGetterValue(.p_lastCalendarName_get, "ProfileStorageMock - stub value for lastCalendarName was not defined") } + set { invocations.append(.p_lastCalendarName_set(.value(newValue))); __p_lastCalendarName = newValue } + } + private var __p_lastCalendarName: (String)? + + public var lastCalendarUpdateDate: Date? { + get { invocations.append(.p_lastCalendarUpdateDate_get); return __p_lastCalendarUpdateDate ?? optionalGivenGetterValue(.p_lastCalendarUpdateDate_get, "ProfileStorageMock - stub value for lastCalendarUpdateDate was not defined") } + set { invocations.append(.p_lastCalendarUpdateDate_set(.value(newValue))); __p_lastCalendarUpdateDate = newValue } + } + private var __p_lastCalendarUpdateDate: (Date)? + + public var firstCalendarUpdate: Bool? { + get { invocations.append(.p_firstCalendarUpdate_get); return __p_firstCalendarUpdate ?? optionalGivenGetterValue(.p_firstCalendarUpdate_get, "ProfileStorageMock - stub value for firstCalendarUpdate was not defined") } + set { invocations.append(.p_firstCalendarUpdate_set(.value(newValue))); __p_firstCalendarUpdate = newValue } + } + private var __p_firstCalendarUpdate: (Bool)? + + + + + + + fileprivate enum MethodType { + case p_userProfile_get + case p_userProfile_set(Parameter) + case p_useRelativeDates_get + case p_useRelativeDates_set(Parameter) + case p_calendarSettings_get + case p_calendarSettings_set(Parameter) + case p_hideInactiveCourses_get + case p_hideInactiveCourses_set(Parameter) + case p_lastLoginUsername_get + case p_lastLoginUsername_set(Parameter) + case p_lastCalendarName_get + case p_lastCalendarName_set(Parameter) + case p_lastCalendarUpdateDate_get + case p_lastCalendarUpdateDate_set(Parameter) + case p_firstCalendarUpdate_get + case p_firstCalendarUpdate_set(Parameter) + + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { case (.p_userProfile_get,.p_userProfile_get): return Matcher.ComparisonResult.match + case (.p_userProfile_set(let left),.p_userProfile_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_useRelativeDates_get,.p_useRelativeDates_get): return Matcher.ComparisonResult.match + case (.p_useRelativeDates_set(let left),.p_useRelativeDates_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_calendarSettings_get,.p_calendarSettings_get): return Matcher.ComparisonResult.match + case (.p_calendarSettings_set(let left),.p_calendarSettings_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_hideInactiveCourses_get,.p_hideInactiveCourses_get): return Matcher.ComparisonResult.match + case (.p_hideInactiveCourses_set(let left),.p_hideInactiveCourses_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_lastLoginUsername_get,.p_lastLoginUsername_get): return Matcher.ComparisonResult.match + case (.p_lastLoginUsername_set(let left),.p_lastLoginUsername_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_lastCalendarName_get,.p_lastCalendarName_get): return Matcher.ComparisonResult.match + case (.p_lastCalendarName_set(let left),.p_lastCalendarName_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_lastCalendarUpdateDate_get,.p_lastCalendarUpdateDate_get): return Matcher.ComparisonResult.match + case (.p_lastCalendarUpdateDate_set(let left),.p_lastCalendarUpdateDate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + case (.p_firstCalendarUpdate_get,.p_firstCalendarUpdate_get): return Matcher.ComparisonResult.match + case (.p_firstCalendarUpdate_set(let left),.p_firstCalendarUpdate_set(let right)): return Matcher.ComparisonResult([Matcher.ParameterComparisonResult(Parameter.compare(lhs: left, rhs: right, with: matcher), left, right, "newValue")]) + default: return .none + } + } + + func intValue() -> Int { + switch self { + case .p_userProfile_get: return 0 + case .p_userProfile_set(let newValue): return newValue.intValue + case .p_useRelativeDates_get: return 0 + case .p_useRelativeDates_set(let newValue): return newValue.intValue + case .p_calendarSettings_get: return 0 + case .p_calendarSettings_set(let newValue): return newValue.intValue + case .p_hideInactiveCourses_get: return 0 + case .p_hideInactiveCourses_set(let newValue): return newValue.intValue + case .p_lastLoginUsername_get: return 0 + case .p_lastLoginUsername_set(let newValue): return newValue.intValue + case .p_lastCalendarName_get: return 0 + case .p_lastCalendarName_set(let newValue): return newValue.intValue + case .p_lastCalendarUpdateDate_get: return 0 + case .p_lastCalendarUpdateDate_set(let newValue): return newValue.intValue + case .p_firstCalendarUpdate_get: return 0 + case .p_firstCalendarUpdate_set(let newValue): return newValue.intValue + } + } + func assertionName() -> String { + switch self { + case .p_userProfile_get: return "[get] .userProfile" + case .p_userProfile_set: return "[set] .userProfile" + case .p_useRelativeDates_get: return "[get] .useRelativeDates" + case .p_useRelativeDates_set: return "[set] .useRelativeDates" + case .p_calendarSettings_get: return "[get] .calendarSettings" + case .p_calendarSettings_set: return "[set] .calendarSettings" + case .p_hideInactiveCourses_get: return "[get] .hideInactiveCourses" + case .p_hideInactiveCourses_set: return "[set] .hideInactiveCourses" + case .p_lastLoginUsername_get: return "[get] .lastLoginUsername" + case .p_lastLoginUsername_set: return "[set] .lastLoginUsername" + case .p_lastCalendarName_get: return "[get] .lastCalendarName" + case .p_lastCalendarName_set: return "[set] .lastCalendarName" + case .p_lastCalendarUpdateDate_get: return "[get] .lastCalendarUpdateDate" + case .p_lastCalendarUpdateDate_set: return "[set] .lastCalendarUpdateDate" + case .p_firstCalendarUpdate_get: return "[get] .firstCalendarUpdate" + case .p_firstCalendarUpdate_set: return "[set] .firstCalendarUpdate" + } + } + } + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + public static func userProfile(getter defaultValue: DataLayer.UserProfile?...) -> PropertyStub { + return Given(method: .p_userProfile_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func useRelativeDates(getter defaultValue: Bool...) -> PropertyStub { + return Given(method: .p_useRelativeDates_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func calendarSettings(getter defaultValue: CalendarSettings?...) -> PropertyStub { + return Given(method: .p_calendarSettings_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func hideInactiveCourses(getter defaultValue: Bool?...) -> PropertyStub { + return Given(method: .p_hideInactiveCourses_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func lastLoginUsername(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_lastLoginUsername_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func lastCalendarName(getter defaultValue: String?...) -> PropertyStub { + return Given(method: .p_lastCalendarName_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func lastCalendarUpdateDate(getter defaultValue: Date?...) -> PropertyStub { + return Given(method: .p_lastCalendarUpdateDate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + public static func firstCalendarUpdate(getter defaultValue: Bool?...) -> PropertyStub { + return Given(method: .p_firstCalendarUpdate_get, products: defaultValue.map({ StubProduct.return($0 as Any) })) + } + + } + + public struct Verify { + fileprivate var method: MethodType + + public static var userProfile: Verify { return Verify(method: .p_userProfile_get) } + public static func userProfile(set newValue: Parameter) -> Verify { return Verify(method: .p_userProfile_set(newValue)) } + public static var useRelativeDates: Verify { return Verify(method: .p_useRelativeDates_get) } + public static func useRelativeDates(set newValue: Parameter) -> Verify { return Verify(method: .p_useRelativeDates_set(newValue)) } + public static var calendarSettings: Verify { return Verify(method: .p_calendarSettings_get) } + public static func calendarSettings(set newValue: Parameter) -> Verify { return Verify(method: .p_calendarSettings_set(newValue)) } + public static var hideInactiveCourses: Verify { return Verify(method: .p_hideInactiveCourses_get) } + public static func hideInactiveCourses(set newValue: Parameter) -> Verify { return Verify(method: .p_hideInactiveCourses_set(newValue)) } + public static var lastLoginUsername: Verify { return Verify(method: .p_lastLoginUsername_get) } + public static func lastLoginUsername(set newValue: Parameter) -> Verify { return Verify(method: .p_lastLoginUsername_set(newValue)) } + public static var lastCalendarName: Verify { return Verify(method: .p_lastCalendarName_get) } + public static func lastCalendarName(set newValue: Parameter) -> Verify { return Verify(method: .p_lastCalendarName_set(newValue)) } + public static var lastCalendarUpdateDate: Verify { return Verify(method: .p_lastCalendarUpdateDate_get) } + public static func lastCalendarUpdateDate(set newValue: Parameter) -> Verify { return Verify(method: .p_lastCalendarUpdateDate_set(newValue)) } + public static var firstCalendarUpdate: Verify { return Verify(method: .p_firstCalendarUpdate_get) } + public static func firstCalendarUpdate(set newValue: Parameter) -> Verify { return Verify(method: .p_firstCalendarUpdate_set(newValue)) } + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + } + + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } +} + // MARK: - WebviewCookiesUpdateProtocol open class WebviewCookiesUpdateProtocolMock: WebviewCookiesUpdateProtocol, Mock { diff --git a/README.md b/README.md index 828a971a4..01addfd08 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,54 @@ Modern vision of the mobile application for the Open edX platform from Raccoon G 6. Click the **Run** button. +## Translations +### Getting translations for the app +Translations aren't included in the source code of this repository as of [OEP-58](https://docs.openedx.org/en/latest/developers/concepts/oep58.html). Therefore, they need to be pulled before testing or publishing to App Store. + +Before retrieving the translations for the app, we need to install the requirements listed in the requirements.txt file located in the i18n_scripts directory. This can be done easily by running the following make command: +```bash +make translation_requirements +``` + +Then, to get the latest translations for all languages use the following command: +```bash +make pull_translations +``` + +This command runs [`atlas pull`](https://github.com/openedx/openedx-atlas) to download the latest translations files from the [openedx/openedx-translations](https://github.com/openedx/openedx-translations) repository. These files contain the latest translations for all languages. In the [openedx/openedx-translations](https://github.com/openedx/openedx-translations) repository each language's translations are saved as a single file e.g. `I18N/I18N/uk.lproj/Localization.strings` ([example](https://github.com/openedx/openedx-translations/blob/6448167e9695a921f003ff6bd8f40f006a2d6743/translations/openedx-app-ios/I18N/I18N/uk.lproj/Localizable.strings)). After these are pulled, each language's translation file is split into the App's modules e.g. `Discovery/Discovery/uk.lproj/Localization.strings`. + + After this command is run the application can load the translations by changing the device (or the emulator) language in the settings. + +**Note:** This command modifies the XCode project files which fails the build so it's required to clean the translations files before committing using the following command: + +``` +make clean_translations +``` + +### Using custom translations + +By default, the command `make pull_translations` runs [`atlas pull`](https://github.com/openedx/openedx-atlas) with no arguments which pulls transaltions from the [openedx-translations repository](https://github.com/openedx/openedx-translations). + +You can use custom translations on your fork of the openedx-translations repository by setting the following configuration parameters: + +- `--revision` (default: `"main"`): Branch or git tag to pull translations from. +- `--repository` (default: `"openedx/openedx-translations"`): GitHub repository slug. There's a feature request to [support GitLab and other providers](https://github.com/openedx/openedx-atlas/issues/20). + +Arguments can be passed via the `ATLAS_OPTIONS` environment variable as shown below: +``` bash +make ATLAS_OPTIONS='--repository=/ --revision=' pull_translations +``` +Additional arguments can be passed to `atlas pull`. Refer to the [atlas documentations ](https://github.com/openedx/openedx-atlas) for more information. + +### How to translate the app + +Translations are managed in the [open-edx/openedx-translations](https://app.transifex.com/open-edx/openedx-translations/dashboard/) Transifex project. + +To translate the app join the [Transifex project](https://app.transifex.com/open-edx/openedx-translations/dashboard/) and add your translations to the [`openedx-app-ios`](https://app.transifex.com/open-edx/openedx-translations/openedx-app-ios/) resource. + +Once the resource is both 100% translated and reviewed the [Transifex integration](https://github.com/apps/transifex-integration) will automatically push it to the [openedx-translations](https://github.com/openedx/openedx-translations) repository and developers can use the translations in their app. + + ## API This project targets on the latest Open edX release and rely on the relevant mobile APIs. diff --git a/Theme/Theme.xcodeproj/project.pbxproj b/Theme/Theme.xcodeproj/project.pbxproj index e67d7eb3d..6178a2a2a 100644 --- a/Theme/Theme.xcodeproj/project.pbxproj +++ b/Theme/Theme.xcodeproj/project.pbxproj @@ -502,14 +502,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -595,14 +595,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -693,14 +693,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -786,14 +786,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -884,14 +884,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -977,14 +977,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1133,14 +1133,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1168,14 +1168,14 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json index 00d59cb46..bf1a96417 100644 --- a/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" + "blue" : "0xF8", + "green" : "0x78", + "red" : "0x53" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json index 00d59cb46..bf1a96417 100644 --- a/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" + "blue" : "0xF8", + "green" : "0x78", + "red" : "0x53" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json index 8fef18d07..df1a5f141 100644 --- a/Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.184", - "green" : "0.129", - "red" : "0.098" + "blue" : "0x2E", + "green" : "0x20", + "red" : "0x18" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json index 164b36790..44092279d 100644 --- a/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewBackground.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "display-p3", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json index 0a5fa0807..d31f2bcff 100644 --- a/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/CardView/CardViewStroke.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.878", - "green" : "0.831", - "red" : "0.800" + "blue" : "0xDF", + "green" : "0xD3", + "red" : "0xCC" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json index 00cf4a827..e9a7a3504 100644 --- a/Theme/Theme/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/CommentCellBackground.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.961", - "green" : "0.961", - "red" : "0.961" + "blue" : "0xF5", + "green" : "0xF5", + "red" : "0xF5" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/SecondaryButtonTextColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CourseCardBackground.colorset/Contents.json similarity index 80% rename from Theme/Theme/Assets.xcassets/Colors/SecondaryButtonTextColor.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/CourseCardBackground.colorset/Contents.json index 00d59cb46..8fef18d07 100644 --- a/Theme/Theme/Assets.xcassets/Colors/SecondaryButtonTextColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/CourseCardBackground.colorset/Contents.json @@ -6,8 +6,8 @@ "components" : { "alpha" : "1.000", "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "green" : "1.000", + "red" : "1.000" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" + "blue" : "0.184", + "green" : "0.129", + "red" : "0.098" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/CourseCardShadow.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CourseCardShadow.colorset/Contents.json new file mode 100644 index 000000000..e7c5c162d --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/CourseCardShadow.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.150", + "blue" : "0x2F", + "green" : "0x21", + "red" : "0x19" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.400", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/CourseDates/NextWeekTimelineColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CourseDates/NextWeekTimelineColor.colorset/Contents.json index 5cd29db93..d7638b312 100644 --- a/Theme/Theme/Assets.xcassets/Colors/CourseDates/NextWeekTimelineColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/CourseDates/NextWeekTimelineColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.733", - "green" : "0.647", - "red" : "0.592" + "blue" : "0xBA", + "green" : "0xA4", + "red" : "0x96" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/CourseDates/UpcomingTimelineColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/CourseDates/UpcomingTimelineColor.colorset/Contents.json index 2af3cc3c3..34275d32c 100644 --- a/Theme/Theme/Assets.xcassets/Colors/CourseDates/UpcomingTimelineColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/CourseDates/UpcomingTimelineColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.878", - "green" : "0.831", - "red" : "0.800" + "blue" : "0xDF", + "green" : "0xD3", + "red" : "0xCC" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBGColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBGColor.colorset/Contents.json new file mode 100644 index 000000000..8fef18d07 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBGColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.184", + "green" : "0.129", + "red" : "0.098" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset copy/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBorderColor.colorset/Contents.json similarity index 100% rename from Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset copy/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBorderColor.colorset/Contents.json diff --git a/Theme/Theme/Assets.xcassets/Colors/SecondaryButtonBorderColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonTextColor.colorset/Contents.json similarity index 100% rename from Theme/Theme/Assets.xcassets/Colors/SecondaryButtonBorderColor.colorset/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonTextColor.colorset/Contents.json diff --git a/Theme/Theme/Assets.xcassets/Colors/IrreversibleAlert.colorset copy/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingSelectedTextColor.colorset/Contents.json similarity index 83% rename from Theme/Theme/Assets.xcassets/Colors/IrreversibleAlert.colorset copy/Contents.json rename to Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingSelectedTextColor.colorset/Contents.json index 14e0c379b..22c4bb0a8 100644 --- a/Theme/Theme/Assets.xcassets/Colors/IrreversibleAlert.colorset copy/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingSelectedTextColor.colorset/Contents.json @@ -5,8 +5,8 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.443", - "green" : "0.239", + "blue" : "1.000", + "green" : "1.000", "red" : "1.000" } }, @@ -23,8 +23,8 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.443", - "green" : "0.239", + "blue" : "1.000", + "green" : "1.000", "red" : "1.000" } }, diff --git a/Theme/Theme/Assets.xcassets/Colors/StyledButton/disabledButton.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/StyledButton/disabledButton.colorset/Contents.json new file mode 100644 index 000000000..1d2b5d427 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/StyledButton/disabledButton.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE0", + "green" : "0xD4", + "red" : "0xCC" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xAE", + "green" : "0x9B", + "red" : "0x8E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/StyledButton/disabledButtonText.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/StyledButton/disabledButtonText.colorset/Contents.json new file mode 100644 index 000000000..3c2cd067c --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/StyledButton/disabledButtonText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x64", + "green" : "0x49", + "red" : "0x3D" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2F", + "green" : "0x21", + "red" : "0x19" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Colors/TabbarColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TabbarColor.colorset/Contents.json index 2d9b9cd70..7e4772ec9 100644 --- a/Theme/Theme/Assets.xcassets/Colors/TabbarColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/TabbarColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "display-p3", "components" : { "alpha" : "1.000", - "blue" : "0.984", - "green" : "0.980", - "red" : "0.976" + "blue" : "0xFA", + "green" : "0xF9", + "red" : "0xF8" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputUnfocusedStroke.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputUnfocusedStroke.colorset/Contents.json index 432fab345..b0ae672b6 100644 --- a/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputUnfocusedStroke.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputUnfocusedStroke.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.878", - "green" : "0.831", - "red" : "0.800" + "blue" : "0xDF", + "green" : "0xD3", + "red" : "0xCC" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/shade.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/shade.colorset/Contents.json new file mode 100644 index 000000000..0e22f05c2 --- /dev/null +++ b/Theme/Theme/Assets.xcassets/Colors/shade.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFB", + "green" : "0xFA", + "red" : "0xF9" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2E", + "green" : "0x20", + "red" : "0x18" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Contents.json b/Theme/Theme/Assets.xcassets/headerBackground.imageset/Contents.json similarity index 100% rename from Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Contents.json rename to Theme/Theme/Assets.xcassets/headerBackground.imageset/Contents.json diff --git a/Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle-2.png b/Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle-2.png similarity index 100% rename from Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle-2.png rename to Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle-2.png diff --git a/Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle.png b/Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle.png similarity index 100% rename from Theme/Theme/Assets.xcassets/Auth/authBackground.imageset/Rectangle.png rename to Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle.png diff --git a/Theme/Theme/SwiftGen/ThemeAssets.swift b/Theme/Theme/SwiftGen/ThemeAssets.swift index 733a02635..c7c49e9b2 100644 --- a/Theme/Theme/SwiftGen/ThemeAssets.swift +++ b/Theme/Theme/SwiftGen/ThemeAssets.swift @@ -24,7 +24,6 @@ public typealias AssetImageTypeAlias = ImageAsset.Image // swiftlint:disable identifier_name line_length nesting type_body_length type_name public enum ThemeAssets { - public static let authBackground = ImageAsset(name: "authBackground") public static let accentButtonColor = ColorAsset(name: "AccentButtonColor") public static let accentColor = ColorAsset(name: "AccentColor") public static let accentXColor = ColorAsset(name: "AccentXColor") @@ -36,6 +35,8 @@ public enum ThemeAssets { public static let cardViewStroke = ColorAsset(name: "CardViewStroke") public static let certificateForeground = ColorAsset(name: "CertificateForeground") public static let commentCellBackground = ColorAsset(name: "CommentCellBackground") + public static let courseCardBackground = ColorAsset(name: "CourseCardBackground") + public static let courseCardShadow = ColorAsset(name: "CourseCardShadow") public static let datesSectionBackground = ColorAsset(name: "DatesSectionBackground") public static let datesSectionStroke = ColorAsset(name: "DatesSectionStroke") public static let nextWeekTimelineColor = ColorAsset(name: "NextWeekTimelineColor") @@ -54,9 +55,11 @@ public enum ThemeAssets { public static let progressDone = ColorAsset(name: "ProgressDone") public static let progressSkip = ColorAsset(name: "ProgressSkip") public static let selectedAndDone = ColorAsset(name: "SelectedAndDone") + public static let secondaryButtonBGColor = ColorAsset(name: "SecondaryButtonBGColor") public static let secondaryButtonBorderColor = ColorAsset(name: "SecondaryButtonBorderColor") public static let secondaryButtonTextColor = ColorAsset(name: "SecondaryButtonTextColor") public static let shadowColor = ColorAsset(name: "ShadowColor") + public static let slidingSelectedTextColor = ColorAsset(name: "slidingSelectedTextColor") public static let slidingStrokeColor = ColorAsset(name: "slidingStrokeColor") public static let slidingTextColor = ColorAsset(name: "slidingTextColor") public static let snackbarErrorColor = ColorAsset(name: "SnackbarErrorColor") @@ -64,6 +67,8 @@ public enum ThemeAssets { public static let snackbarTextColor = ColorAsset(name: "SnackbarTextColor") public static let snackbarWarningColor = ColorAsset(name: "SnackbarWarningColor") public static let styledButtonText = ColorAsset(name: "StyledButtonText") + public static let disabledButton = ColorAsset(name: "disabledButton") + public static let disabledButtonText = ColorAsset(name: "disabledButtonText") public static let success = ColorAsset(name: "Success") public static let tabbarColor = ColorAsset(name: "TabbarColor") public static let textPrimary = ColorAsset(name: "TextPrimary") @@ -77,10 +82,12 @@ public enum ThemeAssets { public static let textInputUnfocusedStroke = ColorAsset(name: "TextInputUnfocusedStroke") public static let toggleSwitchColor = ColorAsset(name: "ToggleSwitchColor") public static let navigationBarTintColor = ColorAsset(name: "navigationBarTintColor") + public static let shade = ColorAsset(name: "shade") public static let warning = ColorAsset(name: "warning") public static let warningText = ColorAsset(name: "warningText") public static let white = ColorAsset(name: "white") public static let appLogo = ImageAsset(name: "appLogo") + public static let headerBackground = ImageAsset(name: "headerBackground") } // swiftlint:enable identifier_name line_length nesting type_body_length type_name diff --git a/Theme/Theme/Theme.swift b/Theme/Theme/Theme.swift index 0fad87eb9..ddff37c66 100644 --- a/Theme/Theme/Theme.swift +++ b/Theme/Theme/Theme.swift @@ -12,6 +12,7 @@ private var fontsParser = FontParser() public struct Theme { + // swiftlint:disable line_length public struct Colors { public private(set) static var accentColor = ThemeAssets.accentColor.swiftUIColor public private(set) static var accentXColor = ThemeAssets.accentXColor.swiftUIColor @@ -36,6 +37,8 @@ public struct Theme { public private(set) static var snackbarInfoColor = ThemeAssets.snackbarInfoColor.swiftUIColor public private(set) static var snackbarTextColor = ThemeAssets.snackbarTextColor.swiftUIColor public private(set) static var styledButtonText = ThemeAssets.styledButtonText.swiftUIColor + public private(set) static var disabledButton = ThemeAssets.disabledButton.swiftUIColor + public private(set) static var disabledButtonText = ThemeAssets.disabledButtonText.swiftUIColor public private(set) static var textPrimary = ThemeAssets.textPrimary.swiftUIColor public private(set) static var textSecondary = ThemeAssets.textSecondary.swiftUIColor public private(set) static var textSecondaryLight = ThemeAssets.textSecondaryLight.swiftUIColor @@ -56,6 +59,7 @@ public struct Theme { public private(set) static var navigationBarTintColor = ThemeAssets.navigationBarTintColor.swiftUIColor public private(set) static var secondaryButtonBorderColor = ThemeAssets.secondaryButtonBorderColor.swiftUIColor public private(set) static var secondaryButtonTextColor = ThemeAssets.secondaryButtonTextColor.swiftUIColor + public private(set) static var secondaryButtonBGColor = ThemeAssets.secondaryButtonBGColor.swiftUIColor public private(set) static var success = ThemeAssets.success.swiftUIColor public private(set) static var tabbarColor = ThemeAssets.tabbarColor.swiftUIColor public private(set) static var primaryButtonTextColor = ThemeAssets.primaryButtonTextColor.swiftUIColor @@ -65,9 +69,13 @@ public struct Theme { public private(set) static var infoColor = ThemeAssets.infoColor.swiftUIColor public private(set) static var irreversibleAlert = ThemeAssets.irreversibleAlert.swiftUIColor public private(set) static var slidingTextColor = ThemeAssets.slidingTextColor.swiftUIColor + public private(set) static var slidingSelectedTextColor = ThemeAssets.slidingSelectedTextColor.swiftUIColor public private(set) static var slidingStrokeColor = ThemeAssets.slidingStrokeColor.swiftUIColor public private(set) static var primaryHeaderColor = ThemeAssets.primaryHeaderColor.swiftUIColor public private(set) static var secondaryHeaderColor = ThemeAssets.secondaryHeaderColor.swiftUIColor + public private(set) static var courseCardShadow = ThemeAssets.courseCardShadow.swiftUIColor + public private(set) static var shade = ThemeAssets.shade.swiftUIColor + public private(set) static var courseCardBackground = ThemeAssets.courseCardBackground.swiftUIColor public static func update( accentColor: Color = ThemeAssets.accentColor.swiftUIColor, @@ -163,6 +171,7 @@ public struct Theme { self.irreversibleAlert = irreversibleAlert } } + // swiftlint:enable line_length // Use this structure where the computed Color.uiColor() extension is not appropriate. public struct UIColors { diff --git a/WhatsNew/Mockfile b/WhatsNew/Mockfile index 3fee3de2b..6107db3ed 100644 --- a/WhatsNew/Mockfile +++ b/WhatsNew/Mockfile @@ -14,4 +14,5 @@ unit.tests.mock: - WhatsNew - Foundation - SwiftUI - - Combine \ No newline at end of file + - Combine + - OEXFoundation \ No newline at end of file diff --git a/WhatsNew/WhatsNew.xcodeproj/project.pbxproj b/WhatsNew/WhatsNew.xcodeproj/project.pbxproj index b78d64a7d..86fca97de 100644 --- a/WhatsNew/WhatsNew.xcodeproj/project.pbxproj +++ b/WhatsNew/WhatsNew.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 02EC90AC2AE90C64007DE1E0 /* WhatsNewPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EC90AB2AE90C64007DE1E0 /* WhatsNewPage.swift */; }; 14769D3E2B99713800AB36D4 /* WhatsNewAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14769D3D2B99713800AB36D4 /* WhatsNewAnalytics.swift */; }; B3BB9B06B226989A619C6440 /* Pods_App_WhatsNew.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05AC45C7050E30F8394E0C76 /* Pods_App_WhatsNew.framework */; }; + CE7CAF352CC1560900E0AC9D /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CE7CAF342CC1560900E0AC9D /* OEXFoundation */; }; + CEB1E2672CC14E6400921517 /* OEXFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CEB1E2662CC14E6400921517 /* OEXFoundation */; }; EF5CA11A55CB49F2DA030D25 /* Pods_App_WhatsNew_WhatsNewTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8D1A5DF016EC4630637336C /* Pods_App_WhatsNew_WhatsNewTests.framework */; }; /* End PBXBuildFile section */ @@ -39,6 +41,19 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + CE7CAF372CC1560900E0AC9D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 020A7B5E2AE131A9000BAF70 /* WhatsNewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewModel.swift; sourceTree = ""; }; 020A7B602AE136D2000BAF70 /* WhatsNew.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = WhatsNew.json; path = WhatsNew/Data/WhatsNew.json; sourceTree = SOURCE_ROOT; }; @@ -52,7 +67,6 @@ 02B54E102AE061C100C56962 /* WhatsNewRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewRouter.swift; sourceTree = ""; }; 02E640782ADFF5920079AEDA /* WhatsNewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewView.swift; sourceTree = ""; }; 02E6407D2ADFF6250079AEDA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - 02E6407F2ADFF6270079AEDA /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 02E640802ADFFE440079AEDA /* WhatsNewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewViewModel.swift; sourceTree = ""; }; 02E640852ADFFF380079AEDA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 02E640892AE004300079AEDA /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; @@ -87,6 +101,7 @@ buildActionMask = 2147483647; files = ( 028A373A2ADFF425008CA604 /* Core.framework in Frameworks */, + CEB1E2672CC14E6400921517 /* OEXFoundation in Frameworks */, B3BB9B06B226989A619C6440 /* Pods_App_WhatsNew.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -96,6 +111,7 @@ buildActionMask = 2147483647; files = ( 028A37262ADFF3F8008CA604 /* WhatsNew.framework in Frameworks */, + CE7CAF352CC1560900E0AC9D /* OEXFoundation in Frameworks */, EF5CA11A55CB49F2DA030D25 /* Pods_App_WhatsNew_WhatsNewTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -278,6 +294,7 @@ 028A37222ADFF3F7008CA604 /* Frameworks */, 028A37232ADFF3F7008CA604 /* Resources */, 8A74692D666D8FF13F7BA64F /* [CP] Copy Pods Resources */, + CE7CAF372CC1560900E0AC9D /* Embed Frameworks */, ); buildRules = ( ); @@ -318,6 +335,9 @@ uk, ); mainGroup = 028A37132ADFF3F7008CA604; + packageReferences = ( + CEB1E2652CC14E6400921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */, + ); productRefGroup = 028A371E2ADFF3F7008CA604 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -473,7 +493,6 @@ isa = PBXVariantGroup; children = ( 02E6407D2ADFF6250079AEDA /* en */, - 02E6407F2ADFF6270079AEDA /* uk */, ); name = Localizable.strings; sourceTree = ""; @@ -614,7 +633,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -624,7 +643,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -653,7 +672,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -663,7 +682,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -692,6 +711,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -712,6 +732,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -797,7 +818,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -807,7 +828,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -837,6 +858,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -922,7 +944,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -932,7 +954,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -962,6 +984,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1047,7 +1070,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1057,7 +1080,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1087,6 +1110,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1165,7 +1189,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1175,7 +1199,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1204,6 +1228,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1282,7 +1307,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1292,7 +1317,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1321,6 +1346,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1399,7 +1425,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = L8PG7LC3Y3; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1409,7 +1435,7 @@ INFOPLIST_FILE = WhatsNew/Info.plist; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1438,6 +1464,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = L8PG7LC3Y3; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.WhatsNewTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1498,6 +1525,30 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + CEB1E2652CC14E6400921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openedx/openedx-app-foundation-ios/"; + requirement = { + kind = exactVersion; + version = 1.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + CE7CAF342CC1560900E0AC9D /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E2652CC14E6400921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; + CEB1E2662CC14E6400921517 /* OEXFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEB1E2652CC14E6400921517 /* XCRemoteSwiftPackageReference "openedx-app-foundation-ios" */; + productName = OEXFoundation; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 028A37142ADFF3F7008CA604 /* Project object */; } diff --git a/WhatsNew/WhatsNew/Info.plist b/WhatsNew/WhatsNew/Info.plist index f72a0f657..0c67376eb 100644 --- a/WhatsNew/WhatsNew/Info.plist +++ b/WhatsNew/WhatsNew/Info.plist @@ -1,12 +1,5 @@ - diff --git a/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift b/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift index 640aa5545..d439058ba 100644 --- a/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift +++ b/WhatsNew/WhatsNew/Presentation/Elements/WhatsNewNavigationButton.swift @@ -50,7 +50,7 @@ struct WhatsNewNavigationButton: View { Theme.Shapes.buttonShape .fill( type == .previous - ? Theme.Colors.background + ? Theme.Colors.secondaryButtonBGColor : Theme.Colors.accentButtonColor ) ) diff --git a/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift b/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift index 4fbd03c4e..9a6ec09ce 100644 --- a/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift +++ b/WhatsNew/WhatsNew/Presentation/WhatsNewView.swift @@ -16,7 +16,7 @@ public struct WhatsNewView: View { @ObservedObject private var viewModel: WhatsNewViewModel - @Environment (\.isHorizontal) + @Environment(\.isHorizontal) private var isHorizontal @State var index = 0 diff --git a/WhatsNew/WhatsNew/uk.lproj/Localizable.strings b/WhatsNew/WhatsNew/uk.lproj/Localizable.strings deleted file mode 100644 index a0194425c..000000000 --- a/WhatsNew/WhatsNew/uk.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* - Localizable.strings - WhatsNew - - Created by  Stepanok Ivan on 18.10.2023. - -*/ - -"TITLE" = "Що нового"; -"BUTTON_PREVIOUS" = "Назад"; -"BUTTON_NEXT" = "Далі"; -"BUTTON_DONE" = "Завершити"; diff --git a/WhatsNew/WhatsNewTests/WhatsNewMock.generated.swift b/WhatsNew/WhatsNewTests/WhatsNewMock.generated.swift index dca08a91e..24726cef8 100644 --- a/WhatsNew/WhatsNewTests/WhatsNewMock.generated.swift +++ b/WhatsNew/WhatsNewTests/WhatsNewMock.generated.swift @@ -13,6 +13,7 @@ import WhatsNew import Foundation import SwiftUI import Combine +import OEXFoundation // MARK: - WhatsNewAnalytics diff --git a/config_script/whitelabel.py b/config_script/whitelabel.py index 00eab9f92..13bdc1c1c 100644 --- a/config_script/whitelabel.py +++ b/config_script/whitelabel.py @@ -9,6 +9,7 @@ from PIL import Image import re from textwrap import dedent +from process_config import PlistManager # type: ignore class WhitelabelApp: EXAMPLE_CONFIG_FILE = dedent(""" @@ -46,9 +47,11 @@ class WhitelabelApp: config1: # build configuration name in project app_bundle_id: "bundle.id.app.new1" # bundle ID which should be set product_name: "Mobile App Name1" # app name which should be set + env_config: 'prod' # env name for this configuration. possible values: prod/dev/stage (values which config_settings.yaml defines) config2: # build configuration name in project app_bundle_id: "bundle.id.app.new2" # bundle ID which should be set product_name: "Mobile App Name2" # app name which should be set + env_config: 'dev' # env name for this configuration. possible values: prod/dev/stage (values which config_settings.yaml defines) font: font_import_file_path: 'path/to/importing/Font_file.ttf' # path to ttf font file what should be imported to project project_font_file_path: 'path/to/font/file/in/project/font.ttf' # path to existing ttf font file in project @@ -90,6 +93,7 @@ def whitelabel(self): self.copy_project_files() if self.project_config: self.set_app_project_config() + self.set_flags_from_mobile_config() def copy_assets(self): if self.assets: @@ -326,12 +330,12 @@ def replace_parameter_for_build_config(self, config_file_string, config_name, ne # replace existing parameter value with new value config_string_out = config_string.replace(parameter_string, new_param_string) else: - errors_texts.append("project_config->configurations->"+config_name+": Check regex please. Can't find place in project file where insert '"+new_param_string+"'") + errors_texts.append(config_name+": Check regex please. Can't find place in project file where place '"+new_param_string+"'") # if something found if config_string != config_string_out: config_file_string = config_file_string.replace(config_string, config_string_out) else: - errors_texts.append("project_config->configurations->"+config_name+": not found in project file") + errors_texts.append(config_name+": not found in project file") return config_file_string def regex_string_for_build_config(self, build_config): @@ -507,6 +511,96 @@ def copy_project_files(self): else: logging.debug("Project's Files for copying not found in config") + # params from MOBILE CONFIG + CONFIG_SETTINGS_YAML_FILENAME = 'config_settings.yaml' + DEFAULT_CONFIG_PATH = './default_config/' + CONFIG_SETTINGS_YAML_FILENAME + CONFIG_DIRECTORY_NAME = 'config_directory' + CONFIG_MAPPINGS = 'config_mapping' + MAPPINGS_FILENAME = 'file_mappings.yaml' + + def parse_yaml(self, file_path): + try: + with open(file_path, 'r') as file: + return yaml.safe_load(file) + except Exception as e: + logging.error(f"Unable to open or read the file '{file_path}': {e}") + return None + + def get_mobile_config(self, config_directory, config_folder, errors_texts): + # get path to mappings file + path = os.path.join(config_directory, config_folder) + mappings_path = os.path.join(path, self.MAPPINGS_FILENAME) + # read mappings file + data = self.parse_yaml(mappings_path) + if data: + # get config for ios described in mappings file + ios_files = data.get('ios', {}).get('files', []) + # re-use PlistManager class from process_config.py script + plist_manager = PlistManager(path, ios_files) + config = plist_manager.load_config() + if config: + return config + else: + errors_texts.append("Unable to parse config for "+config_folder) + else: + errors_texts.append("Files mappings for "+config_folder+" not found") + return None + + def set_flags_from_mobile_config(self): + # get path to mobile config + config_settings = self.parse_yaml(self.CONFIG_SETTINGS_YAML_FILENAME) + if not config_settings: + config_settings = self.parse_yaml(self.DEFAULT_CONFIG_PATH) + config_directory = config_settings.get(self.CONFIG_DIRECTORY_NAME) + # check if we found config directory + if config_directory: + # check if configurations exist + if "configurations" in self.project_config: + configurations = self.project_config["configurations"] + # read project file + with open(self.config_project_path, 'r') as openfile: + project_file_string = openfile.read() + errors_texts = [] + # iterate for all configurations + for name, config in configurations.items(): + if 'env_config' in config: + # get folder name for mobile config for current configuration by env_config + config_folder = config_settings.get(self.CONFIG_MAPPINGS, {}).get(config['env_config']) + if config_folder: + # example of usage + # project_file_string = self.replace_fullstory_flag(project_file_string, config_directory, name, config_folder, errors_texts) + else: + logging.error("Config folder for '"+config['env_config']+"' is not defined in config_settings.yaml->config_mapping") + else: + logging.error("'env_config' is not defined for "+name) + # write to project file + with open(self.config_project_path, 'w') as openfile: + openfile.write(project_file_string) + # print success message or errors if are presented + if len(errors_texts) == 0: + logging.debug("Mobile config user-defined flags were successfully changed") + else: + for error in errors_texts: + logging.error(error) + else: + logging.error("Project configurations are not defined") + else: + logging.error("Mobile config directory not found") + +# def replace_fullstory_flag(self, project_file_string, config_directory, config_name, config_folder, errors_texts): +# # get mobile config +# mobile_config = self.get_mobile_config(config_directory, config_folder, errors_texts) +# if mobile_config: +# # get FULLSTORY settings from mobile config +# fullstory_config = mobile_config.get('FULLSTORY', {}) +# if fullstory_config: +# fullstory_config_enabled = fullstory_config.get('ENABLED') +# fullstory_string = "FULLSTORY_ENABLED = YES;" if fullstory_config_enabled else "FULLSTORY_ENABLED = NO;" +# fullstory_regex = "FULLSTORY_ENABLED = .*;" +# # serach by regex and replace +# project_file_string = self.replace_parameter_for_build_config(project_file_string, config_name, fullstory_string, fullstory_regex, errors_texts) +# return project_file_string +# def main(): """ Parse the command line arguments, and pass them to WhitelabelApp. diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index d7cec08d1..ffd91e5dc 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -1,8 +1,19 @@ API_HOST_URL: 'http://localhost:8000' +SSO_URL: 'http://localhost:8000' +SSO_FINISHED_URL: 'http://localhost:8000' ENVIRONMENT_DISPLAY_NAME: 'Localhost' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' OAUTH_CLIENT_ID: '' +SSO_BUTTON_TITLE: + ar: "الدخول عبر SSO" + en: "Sign in with SSO" + + + UI_COMPONENTS: COURSE_UNIT_PROGRESS_ENABLED: false COURSE_NESTED_LIST_ENABLED: false + LOGIN_REGISTRATION_ENABLED: true + SAML_SSO_LOGIN_ENABLED: false + SAML_SSO_DEFAULT_LOGIN_BUTTON: false diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index d7cec08d1..0edb91b80 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -1,8 +1,18 @@ API_HOST_URL: 'http://localhost:8000' +SSO_URL: 'http://localhost:8000' +SSO_FINISHED_URL: 'http://localhost:8000' ENVIRONMENT_DISPLAY_NAME: 'Localhost' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' OAUTH_CLIENT_ID: '' +SSO_BUTTON_TITLE: + ar: "الدخول عبر SSO" + en: "Sign in with SSO" + + UI_COMPONENTS: COURSE_UNIT_PROGRESS_ENABLED: false COURSE_NESTED_LIST_ENABLED: false + LOGIN_REGISTRATION_ENABLED: true + SAML_SSO_LOGIN_ENABLED: false + SAML_SSO_DEFAULT_LOGIN_BUTTON: false diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index d7cec08d1..0edb91b80 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -1,8 +1,18 @@ API_HOST_URL: 'http://localhost:8000' +SSO_URL: 'http://localhost:8000' +SSO_FINISHED_URL: 'http://localhost:8000' ENVIRONMENT_DISPLAY_NAME: 'Localhost' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' OAUTH_CLIENT_ID: '' +SSO_BUTTON_TITLE: + ar: "الدخول عبر SSO" + en: "Sign in with SSO" + + UI_COMPONENTS: COURSE_UNIT_PROGRESS_ENABLED: false COURSE_NESTED_LIST_ENABLED: false + LOGIN_REGISTRATION_ENABLED: true + SAML_SSO_LOGIN_ENABLED: false + SAML_SSO_DEFAULT_LOGIN_BUTTON: false diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 102e9a266..aa3a39f5b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -15,7 +15,7 @@ update_fastlane before_all do xcodes( - version: '15.3', + version: '16.1', select_for_current_build_only: true, ) @@ -37,7 +37,8 @@ end lane :unit_tests do run_tests( workspace: "OpenEdX.xcworkspace", - device: "iPhone 15", - scheme: "OpenEdXDev" + device: "iPhone 16", + scheme: "OpenEdXDev", + xcargs: "-skipPackagePluginValidation -skipMacroValidation" # Ignore swiftLint plugin validation ) end diff --git a/generateAllMocks.sh b/generateAllMocks.sh index 0c4b5cfc7..6f30225ca 100755 --- a/generateAllMocks.sh +++ b/generateAllMocks.sh @@ -1,6 +1,8 @@ #!/bin/bash DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) cd "${DIR}" +cd ./Core +./../Pods/SwiftyMocky/bin/swiftymocky generate cd ./Authorization ./../Pods/SwiftyMocky/bin/swiftymocky generate cd ../Course @@ -14,4 +16,4 @@ cd ../Discussion cd ../Profile ./../Pods/SwiftyMocky/bin/swiftymocky generate cd ../WhatsNew -./../Pods/SwiftyMocky/bin/swiftymocky generate \ No newline at end of file +./../Pods/SwiftyMocky/bin/swiftymocky generate diff --git a/i18n_scripts/requirements.txt b/i18n_scripts/requirements.txt new file mode 100644 index 000000000..197d6c5be --- /dev/null +++ b/i18n_scripts/requirements.txt @@ -0,0 +1,6 @@ +# Translation processing dependencies +openedx-atlas==0.6.1 +localizable==0.1.3 + +# Using `pbxproj==4.2.0` with the ( https://github.com/kronenthaler/mod-pbxproj/pull/356 ) patch to support Localizable.strings files +https://github.com/kronenthaler/mod-pbxproj/archive/a9187b42dc224827f162c3e7b9b34f4c0d2654ee.zip diff --git a/i18n_scripts/translation.py b/i18n_scripts/translation.py new file mode 100644 index 000000000..8fd979591 --- /dev/null +++ b/i18n_scripts/translation.py @@ -0,0 +1,545 @@ +#!/usr/bin/env python3 +""" +This script performs two jobs: + 1- Combine the English translations from all modules in the repository to the I18N directory. After the English + translation is combined, it will be pushed to the openedx-translations repository as described in OEP-58. +2- Split the pulled translation files from the openedx-translations repository into the iOS app modules. + +More detailed specifications are described in the docs/0002-atlas-translations-management.rst design doc. +""" + +import argparse +import os +import re +import sys +from collections import defaultdict +from contextlib import contextmanager +from pathlib import Path + +import localizable +from pbxproj import XcodeProject +from pbxproj.pbxextensions import FileOptions + +LOCALIZABLE_FILES_TREE = '' +MAIN_MODULE_NAME = 'OpenEdX' +I18N_MODULE_NAME = 'I18N' + + +def parse_arguments(): + """ + This function is the argument parser for this script. + The script takes only one of the two arguments --split or --combine. + Additionally, the --replace-underscore argument can only be used with --split. + """ + parser = argparse.ArgumentParser(description='Split or Combine translations.') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--split', action='store_true', + help='Split translations into separate files for each module and language.') + group.add_argument('--combine', action='store_true', + help='Combine the English translations from all modules into a single file.') + group.add_argument('--clean', action='store_true', + help='Remove translation files and clean XCode projects.') + parser.add_argument('--replace-underscore', action='store_true', + help='Replace Transifex underscore "ar_IQ" language code with ' + 'iOS-compatible "ar-rIQ" codes (only with --split).') + parser.add_argument('--add-xcode-files', action='store_true', + help='Add the language files to the XCode project (only with --split).') + return parser.parse_args() + + +@contextmanager +def change_directory(new_dir: Path): + """ + Context manager to execute `os.chidir`. + + Usage: + + with change_directory('/some/path'): + do_stuff_here() + + :param new_dir: Path + """ + original_dir = os.getcwd() + try: + os.chdir(new_dir) + yield + finally: + os.chdir(original_dir) + + +def get_modules_dir(override: Path = None) -> Path: + """ + Gets the modeles directory (repository root directory). + """ + if override: + return override + + return Path(__file__).absolute().parent.parent + + +def get_translation_file_path(modules_dir: Path, module_name, lang_dir, create_dirs=False): + """ + Retrieves the path of the translation file for a specified module and language directory. + + Parameters: + modules_dir (Path): The path to the base directory containing all the modules. + module_name (str): The name of the module for which the translation path is being retrieved. + lang_dir (str): The name of the language directory within the module's directory. + create_dirs (bool): If True, creates the parent directories if they do not exist. Defaults to False. + + Returns: + Path: The path to the module's translation file (Localizable.strings). + """ + try: + if module_name == MAIN_MODULE_NAME: + # The main project structure is located into `OpenEdX` rather than `OpenEdX/OpenEdX` + module_path = modules_dir / module_name + else: + # Rest of modules such as Core, Course, Dashboard, etc follow the `Dashboard/Dashboard` structure + module_path = modules_dir / module_name / module_name + + lang_dir_path = module_path / lang_dir + if create_dirs: + lang_dir_path.mkdir(parents=True, exist_ok=True) + return lang_dir_path / 'Localizable.strings' + except Exception as e: + print(f"Error creating directory path: {e}", file=sys.stderr) + raise + + +def get_modules_to_translate(modules_dir: Path): + """ + Retrieve the names of modules that have translation files for a specified language. + + Parameters: + modules_dir (Path): The path to the directory containing all the modules. + + Returns: + list of str: A list of module names that have translation files for the specified language. + """ + try: + modules_list = [ + module_dir for module_dir in os.listdir(modules_dir) + if ( + (modules_dir / module_dir).is_dir() + and os.path.isfile(get_translation_file_path(modules_dir, module_dir, 'en.lproj')) + and module_dir != I18N_MODULE_NAME + and module_dir != MAIN_MODULE_NAME + ) + ] + return modules_list + except FileNotFoundError as e: + print(f"Directory not found: {e}", file=sys.stderr) + raise + except PermissionError as e: + print(f"Permission denied: {e}", file=sys.stderr) + raise + + +def get_translations(modules_dir: Path): + """ + Retrieve the translations from all modules in the modules_dir. + + Parameters: + modules_dir (Path): The directory containing the modules. + + Returns: + dict: A dict containing a list of dictionaries containing the 'key', 'value', and 'comment' for each + translation line. The key of the outer dict is the name of the module where the translations are going + to be saved. + """ + translations = [] + try: + modules = get_modules_to_translate(modules_dir) + for module in modules: + translation_file = get_translation_file_path(modules_dir, module, lang_dir='en.lproj') + module_translation = localizable.parse_strings(filename=translation_file) + + translations += [ + { + 'key': f"{module}.{translation_entry['key']}", + 'value': translation_entry['value'], + 'comment': translation_entry['comment'] + } for translation_entry in module_translation + ] + except Exception as e: + print(f"Error retrieving translations: {e}", file=sys.stderr) + raise + + return {I18N_MODULE_NAME: translations} + + +def combine_translation_files(modules_dir=None): + """ + Combine translation files from different modules into a single file. + """ + try: + modules_dir = get_modules_dir(override=modules_dir) + translation = get_translations(modules_dir) + write_translations_to_modules(modules_dir, 'en.lproj', translation) + except Exception as e: + print(f"Error combining translation files: {e}", file=sys.stderr) + raise + + +def get_languages_dirs(modules_dir: Path): + """ + Retrieve directories containing language files for translation. + + Args: + modules_dir (Path): The directory containing all the modules. + + Returns: + list: A list of directories containing language files for translation. Each directory represents + a specific language and ends with the '.lproj' extension. + """ + try: + lang_parent_dir = modules_dir / I18N_MODULE_NAME / I18N_MODULE_NAME + languages_dirs = [ + directory for directory in os.listdir(lang_parent_dir) + if directory.endswith('.lproj') and directory != "en.lproj" + ] + return languages_dirs + except FileNotFoundError as e: + print(f"Directory not found: {e}", file=sys.stderr) + raise + except PermissionError as e: + print(f"Permission denied: {e}", file=sys.stderr) + raise + + +def get_translations_from_file(modules_dir, lang_dir): + """ + Get translations from the translation file in the 'I18N' directory and distribute them into the appropriate + modules' directories. + + Args: + modules_dir (str): The directory containing all the modules. + lang_dir (str): The directory containing the translation file being split. + + Returns: + dict: A dictionary containing translations split by module. The keys are module names, + and the values are lists of dictionaries, each containing the 'key', 'value', and 'comment' + for each translation entry within the module. + """ + translations = defaultdict(list) + try: + translations_file_path = get_translation_file_path(modules_dir, I18N_MODULE_NAME, lang_dir) + lang_list = localizable.parse_strings(filename=translations_file_path) + for translation_entry in lang_list: + module_name, key_remainder = translation_entry['key'].split('.', maxsplit=1) + split_entry = { + 'key': key_remainder, + 'value': translation_entry['value'], + 'comment': translation_entry['comment'] + } + translations[module_name].append(split_entry) + except Exception as e: + print(f"Error extracting translations from file: {e}", file=sys.stderr) + raise + return translations + + +def write_translations_to_modules(modules_dir: Path, lang_dir, modules_translations): + """ + Write translations to language files for each module. + + Args: + modules_dir (str): The directory containing all the modules. + lang_dir (str): The directory of the translation file being written. + modules_translations (dict): A dictionary containing translations for each module. + + Returns: + None + """ + for module, translation_list in modules_translations.items(): + try: + translation_file_path = get_translation_file_path(modules_dir, module, lang_dir, create_dirs=True) + with open(translation_file_path, 'w') as f: + for translation_entry in translation_list: + write_line_and_comment(f, translation_entry) + except Exception as e: + print(f"Error writing translations to file.\n Module: {module}\n Error: {e}", file=sys.stderr) + raise + + # The main project structure is located into `OpenEdX` rather than `OpenEdX/OpenEdX` + # Empty files are added, so iOS knows which languages are supported in this app + main_translation_file_path = get_translation_file_path(modules_dir, MAIN_MODULE_NAME, lang_dir, create_dirs=True) + with open(main_translation_file_path, 'w') as f: + f.write(f'/* Empty {lang_dir}/Localizable.strings: Created by i18n_scripts/translation.py */') + + +def _escape(s): + """ + Reverse the replacements performed by _unescape() in the localizable library + """ + s = s.replace('\n', r'\n').replace('\r', r'\r').replace('"', r'\"') + return s + + +def write_line_and_comment(f, entry): + """ + Write a translation line with an optional comment to a file. + + Args: + file (file object): The file object to write to. + entry (dict): A dictionary containing the translation entry with 'key', 'value', and optional 'comment'. + + Returns: + None + """ + comment = entry.get('comment') # Retrieve the comment, if present + if comment: + f.write(f"/* {comment} */\n") + f.write(f'"{entry["key"]}" = "{_escape(entry["value"])}";\n') + + +def split_translation_files(modules_dir=None): + """ + Split translation files into separate files for each module and language. + + Args: + modules_dir (str, optional): The directory containing all the modules. If not provided, + it defaults to the parent directory of the directory containing this script. + + Returns: + None + """ + try: + modules_dir = get_modules_dir(override=modules_dir) + languages_dirs = get_languages_dirs(modules_dir) + for lang_dir in languages_dirs: + translations = get_translations_from_file(modules_dir, lang_dir) + write_translations_to_modules(modules_dir, lang_dir, translations) + except Exception as e: + print(f"Error splitting translation files: {e}", file=sys.stderr) + raise + + +def get_project_path(modules_dir: Path, module_name: str) -> Path: + """ + Using a module_name return the pbxproj path. + + :param modules_dir: + :param module_name: + :return: Path + """ + if module_name == MAIN_MODULE_NAME: + project_file_path = modules_dir / f'{module_name}.xcodeproj/project.pbxproj' + else: + project_file_path = modules_dir / module_name / f'{module_name}.xcodeproj/project.pbxproj' + + return project_file_path + + +def get_xcode_project(modules_dir: Path, module_name: str) -> XcodeProject: + """ + Initialize an XCode project instance for a given module. + """ + xcode_project = XcodeProject.load(get_project_path(modules_dir, module_name)) + return xcode_project + + +def list_translation_files(module_path: Path) -> [Path]: + """ + List translaiton files in a given path. + + This method doesn't return the `en.lproj` translation source strings. + """ + for localizable_abs_path in module_path.rglob('**/Localizable.strings'): + if localizable_abs_path.parent.name != 'en.lproj': + yield localizable_abs_path + + +def get_xcode_projects(modules_dir: Path) -> [{Path, XcodeProject}]: + """ + Return a list of module_name, xcode_project pairs. + """ + for module_name in get_modules_to_translate(modules_dir): + xcode_project = get_xcode_project(modules_dir, module_name) + yield module_name, xcode_project + + +def add_localizable(xcode_project: XcodeProject, localizable_relative_path: Path): + """ + Add localizable file properly to the PBXVariantGroup. + + This function depends on the https://github.com/kronenthaler/mod-pbxproj/pull/356 implementation. + + TODO: Refactor to use the `master` version once either of the following issues is closed: + - Issue by st3fan: https://github.com/kronenthaler/mod-pbxproj/issues/113 + - Proposal by OmarIthawi for Axim: https://github.com/kronenthaler/mod-pbxproj/pull/356 + + :param xcode_project: XcodeProject + :param localizable_relative_path: Path + :return: + """ + language, _rest = str(localizable_relative_path).split('.lproj') # e.g. `ar` or `fr-ca` + print(f' - Adding "{localizable_relative_path}" for the "{language}" language.') + localizable_groups = xcode_project.get_groups_by_name(name='Localizable.strings', + section='PBXVariantGroup') + if len(localizable_groups) != 1: + # We need a single group. If many are found then, it's a problem. + raise Exception(f'Error: Cannot find the Localizable.strings group, please add the English ' + f'source translation strings with the name Localizable.strings. ' + f'Results: "{localizable_groups}"') + localizable_group = localizable_groups[0] + + xcode_project.add_file( + str(localizable_relative_path), + name=language, + parent=localizable_group, + force=False, + tree=LOCALIZABLE_FILES_TREE, + file_options=FileOptions( + create_build_files=False, + ), + ) + + +def add_translation_files_to_xcode(modules_dir: Path = None): + """ + Add Localizable.strings files pulled from Transifex to XCode. + """ + try: + modules_dir = get_modules_dir(override=modules_dir) + for module_name, xcode_project in get_xcode_projects(modules_dir): + print(f'## Entering project: {module_name}') + module_path = modules_dir / module_name + project_files_path = module_path / module_name # e.g. openedx-app-ios/Authorization/Authorization + + with change_directory(project_files_path): + for localizable_abs_path in list_translation_files(module_path): + add_localizable( + xcode_project=xcode_project, + localizable_relative_path=localizable_abs_path.relative_to(project_files_path), + ) + xcode_project.save() + + # This project is used to specify which languages are supported by the app for iOS + print(f'## Entering project: {MAIN_MODULE_NAME}') + main_xcode_project = get_xcode_project(modules_dir, module_name=MAIN_MODULE_NAME) + main_xcode_project_module_path = modules_dir / MAIN_MODULE_NAME + with change_directory(main_xcode_project_module_path): + # The main project structure is located into `OpenEdX` rather than `OpenEdX/OpenEdX` + for localizable_abs_path in list_translation_files(main_xcode_project_module_path): + add_localizable( + xcode_project=main_xcode_project, + localizable_relative_path=localizable_abs_path.relative_to(main_xcode_project_module_path), + ) + main_xcode_project.save() + + except Exception as e: + print(f"Error: An unexpected error occurred in add_translation_files_to_xcode: {e}", file=sys.stderr) + raise + + +def remove_xcode_localizable_variants(xcode_project: XcodeProject) -> None: + """ + Remove all non-English localizable files from the XCode project. + + :param xcode_project: XcodeProject + :return: + """ + for file_ref in xcode_project.objects.get_objects_in_section('PBXFileReference'): + if ( + not file_ref.path.startswith('en.lproj') + and re.match(r'\w+.lproj', file_ref.path) + and file_ref.sourceTree == LOCALIZABLE_FILES_TREE + and getattr(file_ref, 'lastKnownFileType', None) == 'text.plist.strings' + ): + path = file_ref.path + language, _rest = str(path).split('.lproj') # e.g. `ar` or `fr-ca` + print(f' - Removing "{path}" from project resources for the "{language}" language.') + xcode_project.remove_files_by_path(file_ref.path, tree=LOCALIZABLE_FILES_TREE, target_name=language) + + +def delete_translation_files(module_path: Path, xcode_project_path_base: Path): + """ + Delete the files from the file system. + + :param module_path: Path + :param xcode_project_path_base: Path + :return: + """ + for localizable_abs_path in list_translation_files(module_path): + localizable_relative_path = localizable_abs_path.relative_to(xcode_project_path_base) + print(f' - Removing "{localizable_relative_path}" file from file system') + localizable_abs_path.unlink() + + +def clean_translation_files(modules_dir: Path = None): + """ + Remove translation files from both file system and XCode project files. + """ + try: + modules_dir = get_modules_dir(override=modules_dir) + for module_name, xcode_project in get_xcode_projects(modules_dir): + print(f'## Entering project: {module_name}') + module_path = modules_dir / module_name + delete_translation_files(module_path, xcode_project_path_base=module_path/module_name) + remove_xcode_localizable_variants(xcode_project) + xcode_project.save() + + # This project is used to specify which languages are supported by the app for iOS + print(f'## Entering project: {MAIN_MODULE_NAME}') + main_xcode_project = get_xcode_project(modules_dir, module_name=MAIN_MODULE_NAME) + main_xcode_project_module_path = modules_dir / MAIN_MODULE_NAME + # The main project structure is located into `OpenEdX` rather than `OpenEdX/OpenEdX` + delete_translation_files(main_xcode_project_module_path, main_xcode_project_module_path) + remove_xcode_localizable_variants(main_xcode_project) + main_xcode_project.save() + except Exception as e: + print(f"Error: An unexpected error occurred in clean_translation_files: {e}", file=sys.stderr) + raise + + +def replace_underscores(modules_dir=None): + try: + modules_dir = get_modules_dir(override=modules_dir) + languages_dirs = get_languages_dirs(modules_dir) + + for lang_dir in languages_dirs: + lang_old_path = os.path.dirname(get_translation_file_path(modules_dir, I18N_MODULE_NAME, lang_dir)) + try: + pattern = r'_(\w\w.lproj$)' + if re.search(pattern, lang_dir): + replacement = r'-\1' + new_name = re.sub(pattern, replacement, lang_dir) + lang_new_path = os.path.dirname(get_translation_file_path(modules_dir, I18N_MODULE_NAME, new_name)) + + os.rename(lang_old_path, lang_new_path) + print(f"Renamed {lang_old_path} to {lang_new_path}") + + except FileNotFoundError as e: + print(f"Error: The file or directory {lang_old_path} does not exist: {e}", file=sys.stderr) + raise + except PermissionError as e: + print(f"Error: Permission denied while renaming {lang_old_path}: {e}", file=sys.stderr) + raise + except Exception as e: + print(f"Error: An unexpected error occurred while renaming {lang_old_path}: {e}", + file=sys.stderr) + raise + + except Exception as e: + print(f"Error: An unexpected error occurred in rename_translations_files: {e}", file=sys.stderr) + raise + + +def main(): + args = parse_arguments() + if args.split: + if args.replace_underscore: + replace_underscores() + split_translation_files() + if args.add_xcode_files: + add_translation_files_to_xcode() + elif args.combine: + combine_translation_files() + elif args.clean: + clean_translation_files() + + +if __name__ == "__main__": + main()