Skip to content

Commit

Permalink
Shared, iOS: Add notification request widget (#1190)
Browse files Browse the repository at this point in the history
^ALTAPPS-1348
  • Loading branch information
ivan-magda authored Sep 25, 2024
1 parent 4353d4f commit 8c04adb
Show file tree
Hide file tree
Showing 41 changed files with 1,119 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ class StudyPlanFragment :
)
)
}
is StudyPlanScreenFeature.Action.ViewAction.NotificationDailyStudyReminderWidgetViewAction -> {
// TODO: ALTAPPS-1347 handle notification daily study reminder widget view actions
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions config/detekt/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@
<ID>MaxLineLength:ExpandableTextView.kt$ExpandableTextView$*</ID>
<ID>MaxLineLength:HtmlText.kt$text</ID>
<ID>MaxLineLength:LinearProgressIndicator.kt$*</ID>
<ID>MaxLineLength:NotificationDailyStudyReminderWidgetComponentImpl.kt$NotificationDailyStudyReminderWidgetComponentImpl$override</ID>
<ID>MaxLineLength:NotificationDailyStudyReminderWidgetComponentImpl.kt$NotificationDailyStudyReminderWidgetComponentImpl$private</ID>
<ID>MaxLineLength:ProjectSelectionDetailsClickedSelectThisProjectHyperskillAnalyticEvent.kt$ProjectSelectionDetailsClickedSelectThisProjectHyperskillAnalyticEvent$*</ID>
<ID>MaxLineLength:RepositoryCacheProxy.kt$RepositoryCacheProxy$*</ID>
<ID>MaxLineLength:StepComponentImpl.kt$StepComponentImpl$nextLearningActivityStateRepository = appGraph.stateRepositoriesComponent.nextLearningActivityStateRepository</ID>
Expand Down
12 changes: 12 additions & 0 deletions iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@
2C80D4FD288C4D0D00B2CD1E /* StepQuizCodeFullScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C80D4FC288C4D0D00B2CD1E /* StepQuizCodeFullScreenViewModel.swift */; };
2C80D4FF288C4D4400B2CD1E /* StepQuizCodeFullScreenOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C80D4FE288C4D4400B2CD1E /* StepQuizCodeFullScreenOutputProtocol.swift */; };
2C80D503288C5EBB00B2CD1E /* StepQuizCodeNavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C80D502288C5EBB00B2CD1E /* StepQuizCodeNavigationState.swift */; };
2C8118292CA3BEB600DCE9A8 /* NotificationDailyStudyReminderWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8118282CA3BEB600DCE9A8 /* NotificationDailyStudyReminderWidgetView.swift */; };
2C829B912B88583300765335 /* StepQuizUnsupportedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C829B902B88583300765335 /* StepQuizUnsupportedView.swift */; };
2C82BA322844B01D004C9013 /* PlaceholderView+Configurations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C82BA312844B01D004C9013 /* PlaceholderView+Configurations.swift */; };
2C83FBBE2B177633007AD7E2 /* LeaderboardTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C83FBBD2B177633007AD7E2 /* LeaderboardTab.swift */; };
Expand Down Expand Up @@ -1118,6 +1119,7 @@
2C80D4FC288C4D0D00B2CD1E /* StepQuizCodeFullScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeFullScreenViewModel.swift; sourceTree = "<group>"; };
2C80D4FE288C4D4400B2CD1E /* StepQuizCodeFullScreenOutputProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeFullScreenOutputProtocol.swift; sourceTree = "<group>"; };
2C80D502288C5EBB00B2CD1E /* StepQuizCodeNavigationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeNavigationState.swift; sourceTree = "<group>"; };
2C8118282CA3BEB600DCE9A8 /* NotificationDailyStudyReminderWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationDailyStudyReminderWidgetView.swift; sourceTree = "<group>"; };
2C829B902B88583300765335 /* StepQuizUnsupportedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizUnsupportedView.swift; sourceTree = "<group>"; };
2C82BA312844B01D004C9013 /* PlaceholderView+Configurations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaceholderView+Configurations.swift"; sourceTree = "<group>"; };
2C83FBBD2B177633007AD7E2 /* LeaderboardTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardTab.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1937,6 +1939,7 @@
2CB0ADE92B04AC9E0089D557 /* HomeSubmodules */,
E6992F3BBF430924F32DC178 /* Leaderboard */,
1B096FE500BA52CA6E56B26D /* ManageSubscription */,
2C8118272CA3BE9900DCE9A8 /* NotificationDailyStudyReminderWidget */,
B58361EACE24BF4B761F10BA /* NotificationsOnboarding */,
2C9320F32B68F13000999992 /* Paywall */,
2CE3F5BA2BD7AE7C000B51A4 /* ProblemsLimitInfo */,
Expand Down Expand Up @@ -2950,6 +2953,14 @@
path = Cells;
sourceTree = "<group>";
};
2C8118272CA3BE9900DCE9A8 /* NotificationDailyStudyReminderWidget */ = {
isa = PBXGroup;
children = (
2C8118282CA3BEB600DCE9A8 /* NotificationDailyStudyReminderWidgetView.swift */,
);
path = NotificationDailyStudyReminderWidget;
sourceTree = "<group>";
};
2C82BA302844AFED004C9013 /* PlaceholderView */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5378,6 +5389,7 @@
2C9AA3F52C24611300F5170E /* WelcomeOnboardingChooseProgrammingLanguageView.swift in Sources */,
2C20FBA8284F193A006D879E /* ContentProcessingRule.swift in Sources */,
E9AB311429DED7FE00645376 /* StudyPlanSectionItemIconView.swift in Sources */,
2C8118292CA3BEB600DCE9A8 /* NotificationDailyStudyReminderWidgetView.swift in Sources */,
2C7CB66B2ADFB947006F78DA /* StepQuizFillBlanksAssembly.swift in Sources */,
2CBC97CA2A5553330078E445 /* StageImplementStageCompletedModalViewController.swift in Sources */,
2C20FBC9284F6F97006D879E /* UnitConverters.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// swiftlint:disable all
import Foundation
import shared

Expand Down Expand Up @@ -279,6 +280,11 @@ enum Strings {
static let subtitle = sharedStrings.users_interview_widget_subtitle.localized()
}

enum NotificationDailyStudyReminderWidget {
static let title = sharedStrings.notification_daily_study_reminder_widget_title.localized()
static let subtitle = sharedStrings.notification_daily_study_reminder_widget_subtitle.localized()
}

// MARK: - Topics widget -

enum TopicsWidget {
Expand Down Expand Up @@ -673,3 +679,4 @@ enum Strings {
static let showMoreButton = sharedStrings.comments_show_more_btn.localized()
}
}
// swiftlint:enable all
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import SwiftUI

extension NotificationDailyStudyReminderWidgetView {
struct Appearance {
let illustrationSize = CGSize(width: 89, height: 86)

let spacing = LayoutInsets.defaultInset
let interitemSpacing = LayoutInsets.smallInset
}
}

struct NotificationDailyStudyReminderWidgetView: View {
private(set) var appearance = Appearance()

var onCallToAction: () -> Void
var onClose: () -> Void
var onViewedEvent: () -> Void

var body: some View {
ZStack {
UIViewControllerEventsWrapper(onViewDidAppear: onViewedEvent)

Button(
action: onCallToAction,
label: {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) {
HStack(alignment: .center, spacing: appearance.spacing) {
textConent
illustration
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding([.leading, .vertical])

closeButton
}
.foregroundColor(.white)
.background(backgroundGradient)
}
)
.buttonStyle(BounceButtonStyle())
}
}

private var textConent: some View {
VStack(alignment: .leading, spacing: appearance.interitemSpacing) {
Text(Strings.NotificationDailyStudyReminderWidget.title)
.font(.headline)
Text(Strings.NotificationDailyStudyReminderWidget.subtitle)
.font(.subheadline)
}
}

private var illustration: some View {
Image(.usersInterviewWidgetIllustration)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(size: appearance.illustrationSize)
}

private var closeButton: some View {
Button(
action: onClose,
label: {
Image(systemName: "xmark.circle.fill")
.padding(.all, appearance.interitemSpacing)
}
)
}

private var backgroundGradient: some View {
Image(.usersInterviewWidgetGradient)
.renderingMode(.original)
.resizable()
.addBorder(color: .clear, width: 0)
}
}

#if DEBUG
#Preview {
NotificationDailyStudyReminderWidgetView(
onCallToAction: {},
onClose: {},
onViewedEvent: {}
)
.padding()
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ final class SpacebotWowAnimationView: UIView {
animationView.play { [weak self] completed in
completion?(completed)

guard let self else {
guard let strongSelf = self else {
return
}

self.animationView.alpha = self.appearance.animationViewAlphaHidden
self.animationView.currentProgress = 0
strongSelf.animationView.alpha = strongSelf.appearance.animationViewAlphaHidden
strongSelf.animationView.currentProgress = 0
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,24 @@ final class StepQuizMatchingViewModel: ObservableObject, StepQuizChildQuizInputP
selectedColumnsIDs: matchItem.option != nil ? [matchItem.option.require().id] : [],
isMultipleChoice: false,
onColumnsSelected: { [weak self] selectedColumnsIDs in
guard let self,
guard let strongSelf = self,
let selectedColumnID = selectedColumnsIDs.first,
matchItem.option?.id != selectedColumnID,
let currentItemIndex = self.viewData.items.firstIndex(of: matchItem) else {
let currentItemIndex = strongSelf.viewData.items.firstIndex(of: matchItem) else {
return
}

if let swappingIndex = self.viewData.items.firstIndex(where: { $0.option?.id == selectedColumnID }) {
let tmp = self.viewData.items[currentItemIndex].option
self.viewData.items[currentItemIndex].option = self.viewData.items[swappingIndex].option
self.viewData.items[swappingIndex].option = tmp
if let swapIndex = strongSelf.viewData.items.firstIndex(where: { $0.option?.id == selectedColumnID }) {
let tmp = strongSelf.viewData.items[currentItemIndex].option
strongSelf.viewData.items[currentItemIndex].option = strongSelf.viewData.items[swapIndex].option
strongSelf.viewData.items[swapIndex].option = tmp
} else {
self.viewData.items[currentItemIndex].option = self.options.first(
strongSelf.viewData.items[currentItemIndex].option = strongSelf.options.first(
where: { $0.id == selectedColumnID }
)
}

self.outputCurrentReply()
strongSelf.outputCurrentReply()
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ final class StudyPlanAssembly: UIKitAssembly {
let studyPlanScreenComponent = AppGraphBridge.sharedAppGraph.buildStudyPlanScreenComponent()

let viewModel = StudyPlanViewModel(
notificationsRegistrationService: .shared,
feature: studyPlanScreenComponent.studyPlanScreenFeature
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,22 @@ final class StudyPlanViewModel: FeatureViewModel<

var studyPlanWidgetStateKs: StudyPlanWidgetViewStateKs { .init(state.studyPlanWidgetViewState) }
var gamificationToolbarViewStateKs: GamificationToolbarFeatureViewStateKs { .init(state.toolbarViewState) }
var usersInterviewWidgetFeatureStateKs: UsersInterviewWidgetFeatureStateKs {
var usersInterviewWidgetStateKs: UsersInterviewWidgetFeatureStateKs {
.init(state.usersInterviewWidgetState)
}
var notificationDailyStudyReminderWidgetViewStateKs: NotificationDailyStudyReminderWidgetFeatureViewStateKs {
.init(state.notificationDailyStudyReminderWidgetViewState)
}

private let notificationsRegistrationService: NotificationsRegistrationService

init(
notificationsRegistrationService: NotificationsRegistrationService,
feature: Presentation_reduxFeature
) {
self.notificationsRegistrationService = notificationsRegistrationService
super.init(feature: feature)
}

override func shouldNotifyStateDidChange(
oldState: StudyPlanScreenFeature.ViewState,
Expand All @@ -24,10 +37,12 @@ final class StudyPlanViewModel: FeatureViewModel<

func doLoadStudyPlan() {
onNewMessage(StudyPlanScreenFeatureMessageInitialize())
initializeNotificationDailyStudyReminderWidgetFeature()
}

func doRetryContentLoading() {
onNewMessage(StudyPlanScreenFeatureMessageRetryContentLoading())
initializeNotificationDailyStudyReminderWidgetFeature()
}

func doScreenBecomesActive() {
Expand Down Expand Up @@ -136,6 +151,66 @@ final class StudyPlanViewModel: FeatureViewModel<
}
}

// MARK: - StudyPlanViewModel (NotificationDailyStudyReminderWidget) -

extension StudyPlanViewModel {
func doNotificationDailyStudyReminderWidgetCallToAction() {
onNewMessage(
StudyPlanScreenFeatureMessageNotificationDailyStudyReminderWidgetMessage(
message: NotificationDailyStudyReminderWidgetFeatureMessageWidgetClicked()
)
)
}

func doNotificationDailyStudyReminderWidgetCloseAction() {
onNewMessage(
StudyPlanScreenFeatureMessageNotificationDailyStudyReminderWidgetMessage(
message: NotificationDailyStudyReminderWidgetFeatureMessageCloseClicked()
)
)
}

func doNotificationDailyStudyReminderWidgetRequestNotificationPermission() {
Task(priority: .userInitiated) {
let isGranted = await notificationsRegistrationService.requestAuthorizationIfNeeded()

await MainActor.run {
onNewMessage(
StudyPlanScreenFeatureMessageNotificationDailyStudyReminderWidgetMessage(
message: NotificationDailyStudyReminderWidgetFeatureMessageNotificationPermissionRequestResult(
isPermissionGranted: isGranted
)
)
)
}
}
}

func logNotificationDailyStudyReminderWidgetViewedEvent() {
onNewMessage(
StudyPlanScreenFeatureMessageNotificationDailyStudyReminderWidgetMessage(
message: NotificationDailyStudyReminderWidgetFeatureMessageViewedEventMessage()
)
)
}

private func initializeNotificationDailyStudyReminderWidgetFeature() {
Task(priority: .userInitiated) {
let isNotificationPermissionGranted = await NotificationPermissionStatus.current.isRegistered

await MainActor.run {
onNewMessage(
StudyPlanScreenFeatureMessageNotificationDailyStudyReminderWidgetMessage(
message: NotificationDailyStudyReminderWidgetFeatureMessageInitialize(
isNotificationPermissionGranted: isNotificationPermissionGranted
)
)
)
}
}
}
}

// MARK: - StudyPlanViewModel: StageImplementUnsupportedModalViewControllerDelegate -

extension StudyPlanViewModel: StageImplementUnsupportedModalViewControllerDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ struct StudyPlanView: View {
.padding(.bottom, appearance.trackTitleBottomPadding)
}

let usersInterviewWidgetFeatureStateKs = viewModel.usersInterviewWidgetFeatureStateKs
let usersInterviewWidgetFeatureStateKs = viewModel.usersInterviewWidgetStateKs
if usersInterviewWidgetFeatureStateKs != .hidden {
UsersInterviewWidgetAssembly(
stateKs: usersInterviewWidgetFeatureStateKs,
Expand All @@ -98,6 +98,14 @@ struct StudyPlanView: View {
.makeModule()
}

if viewModel.notificationDailyStudyReminderWidgetViewStateKs != .hidden {
NotificationDailyStudyReminderWidgetView(
onCallToAction: viewModel.doNotificationDailyStudyReminderWidgetCallToAction,
onClose: viewModel.doNotificationDailyStudyReminderWidgetCloseAction,
onViewedEvent: viewModel.logNotificationDailyStudyReminderWidgetViewedEvent
)
}

if data.isPaywallBannerShown {
StudyPlanPaywallBanner(
action: viewModel.doPaywallBannerAction
Expand Down Expand Up @@ -152,6 +160,10 @@ private extension StudyPlanView {
handleUsersInterviewWidgetViewAction(
usersInterviewWidgetViewAction.viewAction
)
case .notificationDailyStudyReminderWidgetViewAction(let notificationDailyStudyReminderWidgetViewAction):
handleNotificationDailyStudyReminderWidgetViewAction(
notificationDailyStudyReminderWidgetViewAction.viewAction
)
}
}

Expand Down Expand Up @@ -236,4 +248,13 @@ private extension StudyPlanView {
)
}
}

func handleNotificationDailyStudyReminderWidgetViewAction(
_ viewAction: NotificationDailyStudyReminderWidgetFeatureActionViewAction
) {
switch NotificationDailyStudyReminderWidgetFeatureActionViewActionKs(viewAction) {
case .requestNotificationPermission:
viewModel.doNotificationDailyStudyReminderWidgetRequestNotificationPermission()
}
}
}
Loading

0 comments on commit 8c04adb

Please sign in to comment.