Skip to content

Commit

Permalink
Merge pull request wordpress-mobile#18750 from wordpress-mobile/featu…
Browse files Browse the repository at this point in the history
…re/18739-push-notification-handling

Blogging Prompts: Add handler for prompt notifications
  • Loading branch information
dvdchr authored May 27, 2022
2 parents eab9c96 + 0e32216 commit 7d4ae19
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 15 deletions.
18 changes: 18 additions & 0 deletions WordPress/Classes/Services/BloggingPromptsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,24 @@ class BloggingPromptsService {
fetchListPrompts(success: success, failure: failure)
}

/// Loads a single prompt with the given `promptID`.
///
/// - Parameters:
/// - promptID: The unique ID for the blogging prompt.
/// - blog: The blog associated with the prompt.
/// - Returns: The blogging prompt object if it exists, or nil otherwise.
func loadPrompt(with promptID: Int, in blog: Blog) -> BloggingPrompt? {
guard let siteID = blog.dotComID else {
return nil
}

let fetchRequest = BloggingPrompt.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "\(#keyPath(BloggingPrompt.siteID)) = %@ AND \(#keyPath(BloggingPrompt.promptID)) = %@", siteID, NSNumber(value: promptID))
fetchRequest.fetchLimit = 1

return (try? self.contextManager.mainContext.fetch(fetchRequest))?.first
}

// MARK: - Settings

/// Fetches the blogging prompt settings for the configured `siteID`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,6 @@ private extension PromptRemindersScheduler {
static let staticNotificationContent = NSLocalizedString("Tap to load today's prompt...", comment: "Title for a push notification with fixed content"
+ " that invites the user to load today's blogging prompt.")
static let defaultFileName = "PromptReminders.plist"
static let promptIDUserInfoKey = "promptID"
static let siteIDUserInfoKey = "siteID"
}

/// The actual implementation for the prompt notification scheduling.
Expand Down Expand Up @@ -236,10 +234,7 @@ private extension PromptRemindersScheduler {
content.title = Constants.notificationTitle
content.subtitle = blog.title ?? String()
content.body = prompt.text
content.userInfo = [
Constants.promptIDUserInfoKey: Int(prompt.promptID),
Constants.siteIDUserInfoKey: siteID
]
content.userInfo = notificationPayload(for: blog, prompt: prompt)

guard let reminderDateComponents = reminderDateComponents(for: prompt, at: time) else {
return nil
Expand Down Expand Up @@ -301,9 +296,7 @@ private extension PromptRemindersScheduler {
let content = UNMutableNotificationContent()
content.title = Constants.notificationTitle
content.body = Constants.staticNotificationContent
content.userInfo = [
Constants.siteIDUserInfoKey: siteID
]
content.userInfo = notificationPayload(for: blog)

var date = afterDate
var identifiers = [String]()
Expand Down Expand Up @@ -364,6 +357,23 @@ private extension PromptRemindersScheduler {
return identifier
}

func notificationPayload(for blog: Blog, prompt: BloggingPrompt? = nil) -> [AnyHashable: Any] {
guard let siteID = blog.dotComID?.intValue else {
return [:]
}

var userInfo: [AnyHashable: Any] = [
PushNotificationsManager.Notification.typeKey: PushNotificationsManager.Notification.bloggingPrompts,
PushNotificationsManager.BloggingPromptPayload.siteIDKey: siteID
]

if let prompt = prompt {
userInfo[PushNotificationsManager.BloggingPromptPayload.promptIDKey] = Int(prompt.promptID)
}

return userInfo
}

// MARK: Local Storage

func defaultFileURL() throws -> URL {
Expand Down
51 changes: 50 additions & 1 deletion WordPress/Classes/Utility/PushNotificationsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ final public class PushNotificationsManager: NSObject {
handleAuthenticationNotification,
handleInactiveNotification,
handleBackgroundNotification,
handleQuickStartLocalNotification]
handleQuickStartLocalNotification,
handleBloggingPromptNotification]

for handler in handlers {
if handler(userInfo, userInteraction, completionHandler) {
Expand Down Expand Up @@ -393,6 +394,7 @@ extension PushNotificationsManager {
static let originKey = "origin"
static let badgeResetValue = "badge-reset"
static let local = "qs-local-notification"
static let bloggingPrompts = "blogging-prompts-notification"
}

enum Tracking {
Expand Down Expand Up @@ -480,6 +482,53 @@ extension PushNotificationsManager {
}
}

// MARK: - Blogging Prompt notifications

extension PushNotificationsManager {

enum BloggingPromptPayload {
static let promptIDKey = "prompt_id"
static let siteIDKey = "site_id"
}

/// Handles Blogging Prompt local notifications.
///
/// - Parameters:
/// - userInfo: The notification's payload
/// - userInteraction: Indicates if the user interacted with the push notification
/// - completionHandler: A callback, to be executed on completion
/// - Returns: True when handled. False otherwise
@objc func handleBloggingPromptNotification(_ userInfo: NSDictionary, userInteraction: Bool, completionHandler: ((UIBackgroundFetchResult) -> Void)?) -> Bool {
guard let type = userInfo.string(forKey: Notification.typeKey),
type == Notification.bloggingPrompts else {
return false
}

// extract userInfo from payload.
guard let siteID = userInfo[BloggingPromptPayload.siteIDKey] as? Int else {
return false
}

// When the promptID is nil, it's most likely a static prompt notification.
let promptID = userInfo[BloggingPromptPayload.promptIDKey] as? Int

let source: BloggingPromptCoordinator.Source = {
if promptID != nil {
return .promptNotification
}
return .promptStaticNotification
}()

WPTabBarController.sharedInstance()?.showPromptAnsweringFlow(siteID: siteID, promptID: promptID, source: source)

// TODO: Tracking

completionHandler?(.newData)

return true
}
}

private extension Date {
var components: DateComponents {
return Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import UIKit

/// Helps manage the flow related to Blogging Prompts.
///
@objc class BloggingPromptCoordinator: NSObject {

private let promptsServiceFactory: BloggingPromptsServiceFactory

enum Errors: Error {
case invalidSite
case promptNotFound
case unknown
}

/// Defines the interaction sources for Blogging Prompts.
enum Source {
case dashboard
case featureIntroduction
case actionSheetHeader
case promptNotification
case promptStaticNotification
case unknown

var editorEntryPoint: PostEditorEntryPoint {
switch self {
case .dashboard:
return .dashboard
case .featureIntroduction:
return .bloggingPromptsFeatureIntroduction
case .actionSheetHeader:
return .bloggingPromptsActionSheetHeader
case .promptNotification, .promptStaticNotification:
return .bloggingPromptsNotification
default:
return .unknown
}
}
}

// MARK: Public Method

init(bloggingPromptsServiceFactory: BloggingPromptsServiceFactory = .init()) {
self.promptsServiceFactory = bloggingPromptsServiceFactory
}

/// Present the post creation flow to answer the prompt with `promptID`.
///
/// - Note: When the `promptID` is nil, the coordinator will attempt to fetch and use today's prompt from remote.
///
/// - Parameters:
/// - viewController: The view controller that will present the post creation flow.
/// - promptID: The ID of the blogging prompt. When nil, the method will use today's prompt.
/// - blog: The blog associated with the blogging prompt.
/// - completion: Closure invoked after the post creation flow is presented.
func showPromptAnsweringFlow(from viewController: UIViewController,
promptID: Int? = nil,
blog: Blog,
source: Source,
completion: (() -> Void)? = nil) {
fetchPrompt(with: promptID, blog: blog) { result in
guard case .success(let prompt) = result else {
completion?()
return
}

// Present the post creation flow.
let editor = EditPostViewController(blog: blog, prompt: prompt)
editor.modalPresentationStyle = .fullScreen
editor.entryPoint = source.editorEntryPoint
viewController.present(editor, animated: true)
completion?()
}
}
}

// MARK: Private Helpers

private extension BloggingPromptCoordinator {

func fetchPrompt(with localPromptID: Int? = nil, blog: Blog, completion: @escaping (Result<BloggingPrompt, Error>) -> Void) {
guard let service = promptsServiceFactory.makeService(for: blog) else {
completion(.failure(Errors.invalidSite))
return
}

// When the promptID is specified, there may be a cached prompt available.
if let promptID = localPromptID,
let prompt = service.loadPrompt(with: promptID, in: blog) {
completion(.success(prompt))
return
}

// Otherwise, try to fetch today's prompt from remote.
service.fetchTodaysPrompt { prompt in
guard let prompt = prompt else {
completion(.failure(Errors.promptNotFound))
return
}
completion(.success(prompt))

} failure: { error in
completion(.failure(error ?? Errors.unknown))
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

/// Encapsulates logic related to Blogging Prompts in WPTabBarController.
///
extension WPTabBarController {

@objc func makeBloggingPromptCoordinator() -> BloggingPromptCoordinator {
return BloggingPromptCoordinator()
}

func showPromptAnsweringFlow(siteID: Int, promptID: Int?, source: BloggingPromptCoordinator.Source) {
guard Feature.enabled(.bloggingPrompts),
let blog = accountSites?.first(where: { $0.dotComID == NSNumber(value: siteID) }),
let viewController = viewControllers?[selectedIndex] else {
return
}

bloggingPromptCoordinator.showPromptAnsweringFlow(from: viewController, promptID: promptID, blog: blog, source: source)
}

}

private extension WPTabBarController {

var accountSites: [Blog]? {
AccountService(managedObjectContext: ContextManager.shared.mainContext).defaultWordPressComAccount()?.visibleBlogs
}

}
1 change: 1 addition & 0 deletions WordPress/Classes/ViewRelated/Post/PostEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,5 @@ enum PostEditorEntryPoint: String {
case dashboard
case bloggingPromptsFeatureIntroduction
case bloggingPromptsActionSheetHeader
case bloggingPromptsNotification
}
2 changes: 2 additions & 0 deletions WordPress/Classes/ViewRelated/System/WPTabBarController.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extern NSString * const WPTabBarCurrentlySelectedScreenNotifications;

@class AbstractPost;
@class Blog;
@class BloggingPromptCoordinator;
@class BlogListViewController;
@class MeViewController;
@class MySitesCoordinator;
Expand All @@ -23,6 +24,7 @@ extern NSString * const WPTabBarCurrentlySelectedScreenNotifications;
@property (nonatomic, strong, readonly) UINavigationController *readerNavigationController;
@property (nonatomic, strong, readonly) MySitesCoordinator *mySitesCoordinator;
@property (nonatomic, strong, readonly) ReaderCoordinator *readerCoordinator;
@property (nonatomic, strong, readonly) BloggingPromptCoordinator *bloggingPromptCoordinator;
@property (nonatomic, strong) id<ScenePresenter> meScenePresenter;
@property (nonatomic, strong) id<ScenePresenter> whatIsNewScenePresenter;
@property (nonatomic, strong, readonly) ReaderTabViewModel *readerTabViewModel;
Expand Down
12 changes: 12 additions & 0 deletions WordPress/Classes/ViewRelated/System/WPTabBarController.m
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ @interface WPTabBarController () <UITabBarControllerDelegate, UIViewControllerRe
@property (nonatomic, strong) ReaderTabViewModel *readerTabViewModel;

@property (nonatomic, strong) MySitesCoordinator *mySitesCoordinator;
@property (nonatomic, strong) BloggingPromptCoordinator *bloggingPromptCoordinator;

@property (nonatomic, strong) UIImage *notificationsTabBarImage;
@property (nonatomic, strong) UIImage *notificationsTabBarImageUnread;
Expand Down Expand Up @@ -631,4 +632,15 @@ - (UIPresentationController *)presentationControllerForPresentedViewController:(
return _whatIsNewScenePresenter;
}

#pragma mark - Blogging Prompt
- (BloggingPromptCoordinator *)bloggingPromptCoordinator
{
if (_bloggingPromptCoordinator) {
return _bloggingPromptCoordinator;
}

self.bloggingPromptCoordinator = [self makeBloggingPromptCoordinator];
return _bloggingPromptCoordinator;
}

@end
Loading

0 comments on commit 7d4ae19

Please sign in to comment.