From 36afcbdcc656742ce67e0f2496ba5f678af8f550 Mon Sep 17 00:00:00 2001 From: Ivan Magda Date: Thu, 4 Jan 2024 15:43:23 +0700 Subject: [PATCH] Implement interview preparation completed modal --- .../step/view/delegate/StepDelegate.kt | 5 ++ .../project.pbxproj | 16 ++++++ .../Models/Constants/Images/Images.swift | 4 -- .../Sources/Models/Constants/Strings.swift | 13 ++++- ...tageImplementStageCompletedModalView.swift | 2 +- .../Sources/Modules/Step/StepViewModel.swift | 34 +++++++++++++ ...terviewPreparationCompletedModalView.swift | 45 +++++++++++++++++ ...parationCompletedModalViewController.swift | 49 +++++++++++++++++++ .../Sources/Modules/Step/Views/StepView.swift | 18 ++++--- .../presentation/StepCompletionFeature.kt | 1 + .../presentation/StepCompletionReducer.kt | 2 +- 11 files changed, 174 insertions(+), 15 deletions(-) create mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/Modals/InterviewPreparationCompleted/InterviewPreparationCompletedModalView.swift create mode 100644 iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/Modals/InterviewPreparationCompleted/InterviewPreparationCompletedModalViewController.swift diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt index 859fe0dbb4..b7f686dc84 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt @@ -52,6 +52,11 @@ class StepDelegate( mainScreenRouter.switch(Tabs.STUDY_PLAN) } + StepCompletionFeature.Action.ViewAction.NavigateTo.Home -> { + fragment.requireRouter().backTo(MainScreen(Tabs.TRAINING)) + mainScreenRouter.switch(Tabs.TRAINING) + } + is StepCompletionFeature.Action.ViewAction.ReloadStep -> { fragment.requireRouter().replaceScreen(StepScreen(stepCompletionAction.stepRoute)) } diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index a6a19c2aea..0882faa5b9 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -79,6 +79,8 @@ 2C186AD72B4693E200DADB26 /* InterviewPreparationWidgetContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C186AD62B4693E200DADB26 /* InterviewPreparationWidgetContentView.swift */; }; 2C186AD92B46969D00DADB26 /* HomeWidgetCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C186AD82B46969D00DADB26 /* HomeWidgetCountView.swift */; }; 2C186ADB2B46989700DADB26 /* TopicsRepetitionsCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C186ADA2B46989700DADB26 /* TopicsRepetitionsCountView.swift */; }; + 2C186ADD2B46A08E00DADB26 /* InterviewPreparationCompletedModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C186ADC2B46A08E00DADB26 /* InterviewPreparationCompletedModalViewController.swift */; }; + 2C186ADF2B46A0A300DADB26 /* InterviewPreparationCompletedModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C186ADE2B46A0A300DADB26 /* InterviewPreparationCompletedModalView.swift */; }; 2C198DFE2AEA444100DCD35A /* FillBlanksSelectContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C198DFD2AEA444100DCD35A /* FillBlanksSelectContainerView.swift */; }; 2C198E012AEA835F00DCD35A /* StepQuizFillBlanksSelectOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C198E002AEA835F00DCD35A /* StepQuizFillBlanksSelectOptionsView.swift */; }; 2C198E032AEA869300DCD35A /* StepQuizFillBlanksSelectOptionsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C198E022AEA869300DCD35A /* StepQuizFillBlanksSelectOptionsCollectionViewCell.swift */; }; @@ -753,6 +755,8 @@ 2C186AD62B4693E200DADB26 /* InterviewPreparationWidgetContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterviewPreparationWidgetContentView.swift; sourceTree = ""; }; 2C186AD82B46969D00DADB26 /* HomeWidgetCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWidgetCountView.swift; sourceTree = ""; }; 2C186ADA2B46989700DADB26 /* TopicsRepetitionsCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicsRepetitionsCountView.swift; sourceTree = ""; }; + 2C186ADC2B46A08E00DADB26 /* InterviewPreparationCompletedModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterviewPreparationCompletedModalViewController.swift; sourceTree = ""; }; + 2C186ADE2B46A0A300DADB26 /* InterviewPreparationCompletedModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterviewPreparationCompletedModalView.swift; sourceTree = ""; }; 2C198DFD2AEA444100DCD35A /* FillBlanksSelectContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FillBlanksSelectContainerView.swift; sourceTree = ""; }; 2C198E002AEA835F00DCD35A /* StepQuizFillBlanksSelectOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizFillBlanksSelectOptionsView.swift; sourceTree = ""; }; 2C198E022AEA869300DCD35A /* StepQuizFillBlanksSelectOptionsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizFillBlanksSelectOptionsCollectionViewCell.swift; sourceTree = ""; }; @@ -1631,6 +1635,15 @@ path = Views; sourceTree = ""; }; + 2C186AE02B46A0AB00DADB26 /* InterviewPreparationCompleted */ = { + isa = PBXGroup; + children = ( + 2C186ADE2B46A0A300DADB26 /* InterviewPreparationCompletedModalView.swift */, + 2C186ADC2B46A08E00DADB26 /* InterviewPreparationCompletedModalViewController.swift */, + ); + path = InterviewPreparationCompleted; + sourceTree = ""; + }; 2C198DFC2AEA441E00DCD35A /* Select */ = { isa = PBXGroup; children = ( @@ -3228,6 +3241,7 @@ children = ( E97BEA1D2977D26F00348EEC /* TopicCompletedModalViewController.swift */, E9CC6C0329893D8400D8D070 /* TopicCompletedModalViewControllerDelegate.swift */, + 2C186AE02B46A0AB00DADB26 /* InterviewPreparationCompleted */, ); path = Modals; sourceTree = ""; @@ -4604,6 +4618,7 @@ E996D414292228A700A47498 /* TopicsRepetitionsView.swift in Sources */, 2CAE8D092805789100E6C83D /* StepCommentsStatisticsView.swift in Sources */, E94BB0462A9DEF7C00736B7C /* StepQuizParsonsControlsView.swift in Sources */, + 2C186ADD2B46A08E00DADB26 /* InterviewPreparationCompletedModalViewController.swift in Sources */, 2C5CBBE72948FC7A00113007 /* StepQuizSQLView.swift in Sources */, 2C54E4262A1F7086003406B9 /* TrackSelectionDetailsDescriptionView.swift in Sources */, 2C05AC462A0E9EBC0039C7EF /* ProjectSelectionListFeatureViewStateKsExtensions.swift in Sources */, @@ -4892,6 +4907,7 @@ 2C82BA322844B01D004C9013 /* PlaceholderView+Configurations.swift in Sources */, 2C25BFD52851F8F00036C689 /* UIColor+DesignSystem.swift in Sources */, 2C023C8D285DCA4300D2D5A9 /* DatasetExtensions.swift in Sources */, + 2C186ADF2B46A0A300DADB26 /* InterviewPreparationCompletedModalView.swift in Sources */, E91017152832975C002E70F5 /* CheckboxButton.swift in Sources */, 2C99B1002A14255F0018627B /* StudyPlanWidgetViewStateSectionItemStateWrapper.swift in Sources */, E9E964872A0B8D8200841DF6 /* StepQuizProblemsLimitView.swift in Sources */, diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Images/Images.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Images/Images.swift index 7325f96a48..9a5a368464 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Images/Images.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Images/Images.swift @@ -68,10 +68,6 @@ enum Images { static let icon = "stage-implement-unsupported-modal-icon" } - enum StageCompletedModal { - static let icon = "stage-implement-stage-completed-modal-icon" - } - enum ProjectCompletedModal { static let icon = "stage-implement-project-completed-modal-icon" } diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Strings.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Strings.swift index c4887a916d..90dc276bee 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Strings.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Models/Constants/Strings.swift @@ -257,14 +257,16 @@ enum Strings { static let networkError = sharedStrings.challenge_widget_network_error_text.localized() } - // MARK: - Interview preparation widget - + // MARK: - Interview Preparation - + + // MARK: Widget enum InterviewPreparationWidget { static let title = sharedStrings.interview_preparation_widget_title.localized() static let networkError = sharedStrings.interview_preparation_widget_network_error_text.localized() } - // MARK: - Interview preparation onboarding - + // MARK: Onboarding enum InterviewPreparationOnboarding { static let navigationTitle = sharedStrings.interview_preparation_onboarding_screen_title.localized() @@ -274,6 +276,13 @@ enum Strings { static let callToActionButton = sharedStrings.interview_preparation_onboarding_go_to_first_problem.localized() } + // MARK: Completed modal + + enum InterviewPreparationCompletedModal { + static let title = sharedStrings.interview_preparation_finished_modal_title.localized() + static let description = sharedStrings.interview_preparation_finished_modal_description.localized() + } + // MARK: - Topics widget - enum TopicsWidget { diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StageImplement/Modals/StageCompleted/StageImplementStageCompletedModalView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StageImplement/Modals/StageCompleted/StageImplementStageCompletedModalView.swift index 59bf85984b..38fc10c084 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StageImplement/Modals/StageCompleted/StageImplementStageCompletedModalView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StageImplement/Modals/StageCompleted/StageImplementStageCompletedModalView.swift @@ -17,7 +17,7 @@ struct StageImplementStageCompletedModalView: View { var body: some View { VStack(alignment: .leading, spacing: appearance.spacing) { - Image(Images.StageImplement.StageCompletedModal.icon) + Image(.stageImplementStageCompletedModalIcon) .renderingMode(.original) .aspectRatio(contentMode: .fit) .frame(maxWidth: .infinity, alignment: .leading) diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/StepViewModel.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/StepViewModel.swift index 18251d14e6..99cacee073 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/StepViewModel.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/StepViewModel.swift @@ -223,3 +223,37 @@ extension StepViewModel: ShareStreakModalViewControllerDelegate { ) } } + +// MARK: - StepViewModel: InterviewPreparationCompletedModalViewControllerDelegate - + +extension StepViewModel: InterviewPreparationCompletedModalViewControllerDelegate { + func interviewPreparationCompletedModalViewControllerDidAppear( + _ viewController: InterviewPreparationCompletedModalViewController + ) { + onNewMessage( + StepFeatureMessageStepCompletionMessage( + message: StepCompletionFeatureMessageInterviewPreparationCompletedModalShownEventMessage() + ) + ) + } + + func interviewPreparationCompletedModalViewControllerDidDisappear( + _ viewController: InterviewPreparationCompletedModalViewController + ) { + onNewMessage( + StepFeatureMessageStepCompletionMessage( + message: StepCompletionFeatureMessageInterviewPreparationCompletedModalHiddenEventMessage() + ) + ) + } + + func interviewPreparationCompletedModalViewControllerDidTapCallToActionButton( + _ viewController: InterviewPreparationCompletedModalViewController + ) { + onNewMessage( + StepFeatureMessageStepCompletionMessage( + message: StepCompletionFeatureMessageInterviewPreparationCompletedModalGoToTrainingClicked() + ) + ) + } +} diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/Modals/InterviewPreparationCompleted/InterviewPreparationCompletedModalView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/Modals/InterviewPreparationCompleted/InterviewPreparationCompletedModalView.swift new file mode 100644 index 0000000000..72b4ef6e98 --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/Modals/InterviewPreparationCompleted/InterviewPreparationCompletedModalView.swift @@ -0,0 +1,45 @@ +import SwiftUI + +extension InterviewPreparationCompletedModalView { + struct Appearance { + let spacing: CGFloat = LayoutInsets.defaultInset * 2 + let interitemSpacing = LayoutInsets.defaultInset + } +} + +struct InterviewPreparationCompletedModalView: View { + private(set) var appearance = Appearance() + + var onCallToActionTap: () -> Void = {} + + var body: some View { + VStack(alignment: .leading, spacing: appearance.spacing) { + Image(.stageImplementStageCompletedModalIcon) + .renderingMode(.original) + .aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity, alignment: .leading) + + VStack(alignment: .leading, spacing: appearance.interitemSpacing) { + Text(Strings.InterviewPreparationCompletedModal.title) + .font(.title2).bold() + .foregroundColor(.primaryText) + + Text(Strings.InterviewPreparationCompletedModal.description) + .font(.body) + .foregroundColor(.primaryText) + } + + Button( + Strings.Common.goToTraining, + action: onCallToActionTap + ) + .buttonStyle(.primary) + .shineEffect() + } + .padding([.horizontal, .bottom]) + } +} + +#Preview { + InterviewPreparationCompletedModalView() +} diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/Modals/InterviewPreparationCompleted/InterviewPreparationCompletedModalViewController.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/Modals/InterviewPreparationCompleted/InterviewPreparationCompletedModalViewController.swift new file mode 100644 index 0000000000..7da2ac9599 --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/Modals/InterviewPreparationCompleted/InterviewPreparationCompletedModalViewController.swift @@ -0,0 +1,49 @@ +import SwiftUI + +protocol InterviewPreparationCompletedModalViewControllerDelegate: AnyObject { + func interviewPreparationCompletedModalViewControllerDidAppear( + _ viewController: InterviewPreparationCompletedModalViewController + ) + func interviewPreparationCompletedModalViewControllerDidDisappear( + _ viewController: InterviewPreparationCompletedModalViewController + ) + func interviewPreparationCompletedModalViewControllerDidTapCallToActionButton( + _ viewController: InterviewPreparationCompletedModalViewController + ) +} + +final class InterviewPreparationCompletedModalViewController: PanModalSwiftUIViewController< + InterviewPreparationCompletedModalView +> { + private weak var delegate: InterviewPreparationCompletedModalViewControllerDelegate? + + convenience init(delegate: InterviewPreparationCompletedModalViewControllerDelegate?) { + var view = InterviewPreparationCompletedModalView() + + self.init( + isPresented: .init(get: { false }, set: { _ in }), + content: { view } + ) + + view.onCallToActionTap = { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.delegate?.interviewPreparationCompletedModalViewControllerDidTapCallToActionButton(strongSelf) + strongSelf.dismiss(animated: true) + } + + self.delegate = delegate + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + delegate?.interviewPreparationCompletedModalViewControllerDidAppear(self) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + delegate?.interviewPreparationCompletedModalViewControllerDidDisappear(self) + } +} diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/StepView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/StepView.swift index 6d3199f46c..cf64aa8136 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/StepView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Step/Views/StepView.swift @@ -112,6 +112,9 @@ struct StepView: View { case .studyPlan: dismissPanModalAndNavigateBack() TabBarRouter(tab: .studyPlan).route() + case .home: + dismissPanModalAndNavigateBack() + TabBarRouter(tab: .home).route() } case .showProblemOfDaySolvedModal(let showProblemOfDaySolvedModalViewAction): presentDailyStepCompletedModal( @@ -141,8 +144,8 @@ struct StepView: View { // MARK: - StepView (Modals) - -extension StepView { - private func presentTopicCompletedModal(modalText: String, isNextStepAvailable: Bool) { +private extension StepView { + func presentTopicCompletedModal(modalText: String, isNextStepAvailable: Bool) { let modal = TopicCompletedModalViewController( modalText: modalText, isNextStepAvailable: isNextStepAvailable, @@ -151,7 +154,7 @@ extension StepView { panModalPresenter.presentPanModal(modal) } - private func presentDailyStepCompletedModal( + func presentDailyStepCompletedModal( earnedGemsText: String, shareStreakData: StepCompletionFeatureShareStreakData ) { @@ -163,7 +166,7 @@ extension StepView { panModalPresenter.presentPanModal(modal) } - private func presentShareStreakModal(streak: Int) { + func presentShareStreakModal(streak: Int) { let modal = ShareStreakModalViewController( streak: streak, delegate: viewModel @@ -171,13 +174,14 @@ extension StepView { panModalPresenter.presentPanModal(modal) } - private func presentShareStreakSystemModal(streak: Int) { + func presentShareStreakSystemModal(streak: Int) { let activityViewController = ShareStreakAction.makeActivityViewController(for: streak) modalRouter.present(module: activityViewController, modalPresentationStyle: .automatic) } - private func presentInterviewPreparationFinishedModal() { - #warning("TODO: ALTAPPS-1093") + func presentInterviewPreparationFinishedModal() { + let modal = InterviewPreparationCompletedModalViewController(delegate: viewModel) + panModalPresenter.presentPanModal(modal) } } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionFeature.kt index 41c6139905..d45cbc99f8 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionFeature.kt @@ -167,6 +167,7 @@ object StepCompletionFeature { sealed interface NavigateTo : ViewAction { object Back : NavigateTo object StudyPlan : NavigateTo + object Home : NavigateTo } } } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionReducer.kt index c9b80a45a0..f45cedf40d 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionReducer.kt @@ -204,7 +204,7 @@ class StepCompletionReducer(private val stepRoute: StepRoute) : StateReducer