Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Fix #1660: Show monthly ad grant reminders
Browse files Browse the repository at this point in the history
These notifications are scheduled based on when a user sees ads are always scheduled for the following month. They are only cancelled if the user successfully claims an ad grant
  • Loading branch information
kylehickinson committed Nov 19, 2019
1 parent fdb3f85 commit bf54017
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 0 deletions.
3 changes: 3 additions & 0 deletions BraveRewardsUI/Ads/AdsNotificationHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import UIKit
import BraveRewards
import pop
import SnapKit
import BraveShared

public class AdsNotificationHandler: BraveAdsNotificationHandler {
/// An action type occuring on the ad
Expand Down Expand Up @@ -59,6 +60,8 @@ public class AdsNotificationHandler: BraveAdsNotificationHandler {
}

self.ads.reportNotificationEvent(notification.id, eventType: .viewed)
MonthlyAdsGrantReminder.schedule()

adsViewController.display(ad: notification, handler: { [weak self] (notification, action) in
guard let self = self else { return }
switch action {
Expand Down
4 changes: 4 additions & 0 deletions BraveRewardsUI/Extensions/BraveLedgerExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import Foundation
import BraveRewards
import Shared
import BraveShared

private let log = Logger.rewardsLogger

Expand Down Expand Up @@ -205,6 +206,9 @@ extension BraveLedger {

self.attestPromotion(promotion.id, solution: solution) { result, promotion in
if result == .ledgerOk {
if promotion?.type == .ads {
MonthlyAdsGrantReminder.cancelCurrentMonth()
}
self.updatePromotions {
completion(true)
}
Expand Down
6 changes: 6 additions & 0 deletions BraveShared/BraveStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -581,3 +581,9 @@ extension Strings {
public static let OBErrorDetails = NSLocalizedString("OBErrorDetails", bundle: Bundle.braveShared, value: "Something went wrong while creating your wallet. Please try again", comment: "A generic error body for onboarding")
public static let OBErrorOkay = NSLocalizedString("OBErrorOkay", bundle: Bundle.braveShared, value: "Okay", comment: "")
}

// MARK: - Ads Notifications
extension Strings {
public static let MonthlyAdsClaimNotificationTitle = NSLocalizedString("MonthlyAdsClaimNotificationTitle", bundle: Bundle.braveShared, value: "Claim your Brave ads rewards", comment: "The title of the notification that goes out monthly to users who can claim an ads grant")
public static let MonthlyAdsClaimNotificationBody = NSLocalizedString("MonthlyAdsClaimNotificationBody", bundle: Bundle.braveShared, value: "Time to get rewarded for those ads you've been seeing in Brave.", comment: "The body of the notification that goes out monthly to users who can claim an ads grant")
}
103 changes: 103 additions & 0 deletions BraveShared/Rewards/MonthlyAdsGrantReminder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

import Foundation
import Shared
import UserNotifications

private let log = Logger.rewardsLogger

/// Manages the scheduling and cancellation of the monthly ad grant reminder notification
final public class MonthlyAdsGrantReminder {

@available(*, unavailable)
init() { }

/// The prefix for all notifications scheduled
static private let idPrefix = "rewards.notification.monthly-claim"

/// Returns true if the given notification is one scheduled by `MonthlyAdsGrantReminder`
static public func isMonthlyAdsReminderNotification(_ notification: UNNotification) -> Bool {
return notification.request.identifier.hasPrefix(idPrefix)
}

/// Get the identifier for a notification given its month
static private func identifier(for month: Int) -> String {
return "\(idPrefix)-\(month)"
}

/// Cancels the current month's notification if one exists
///
/// Trigger this when the user claims an ad grant.
static public func cancelCurrentMonth() {
let month = Calendar.current.component(.month, from: Date())
let id = identifier(for: month)
let center = UNUserNotificationCenter.current()
center.removePendingNotificationRequests(withIdentifiers: [id])
center.removeDeliveredNotifications(withIdentifiers: [id])
log.debug("Cancelled monthly ad grant reminder for month: \(month)")
}

/// Retrieve the next month as an integer (1-12). For example, if the current month was december,
/// this would return `1` for January.
static private func nextMonth() -> Int? {
guard let nextMonthsDate = Calendar.current.date(byAdding: .month, value: 1, to: Date()) else {
// Apocalypse...
return nil
}
return Calendar.current.component(.month, from: nextMonthsDate)
}

/// Schedules a notification for the following month if one doesn't already exist for that month
///
/// Trigger this when the user views an ad
static public func schedule() {
guard let month = nextMonth() else {
log.error("Failed to obtain month to schedule notification")
return
}
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.provisional, .alert, .sound, .badge]) { (granted, error) in
if let error = error {
log.error("Failed to request notifications permissions: \(error)")
return
}
if !granted {
log.info("Not authorized to schedule a notification")
return
}

let id = self.identifier(for: month)

center.getPendingNotificationRequests { requests in
if requests.contains(where: { $0.identifier == id }) {
// Already has one scheduled no need to schedule again
return
}

let content = UNMutableNotificationContent()
content.title = Strings.MonthlyAdsClaimNotificationTitle
content.body = Strings.MonthlyAdsClaimNotificationBody

let trigger = UNCalendarNotificationTrigger(
dateMatching: .init(month: month, day: 7, hour: 13, minute: 21),
repeats: false
)
let request = UNNotificationRequest(
identifier: id,
content: content,
trigger: trigger
)
center.add(request) { error in
if let error = error {
log.error("Failed to add notification request: \(request) with error: \(error)")
return
}
log.debug("Scheduled monthly ad grant reminder: \(request). Trigger date: \(String(describing: trigger.nextTriggerDate()))")
}
}
}
}
}

16 changes: 16 additions & 0 deletions Client.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@
279C756B219DDE3B001CD1CB /* FingerprintingProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279C756A219DDE3B001CD1CB /* FingerprintingProtection.swift */; };
279C75C821A5B37D001CD1CB /* FingerprintingProtection.js in Resources */ = {isa = PBXBuildFile; fileRef = 279C75C021A5B37D001CD1CB /* FingerprintingProtection.js */; };
27A586E1214C0DDD000CAE3C /* PreferencesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A586E0214C0DDD000CAE3C /* PreferencesTest.swift */; };
27AC169323834175004BE19C /* MonthlyAdsGrantReminder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27AC168F23833A16004BE19C /* MonthlyAdsGrantReminder.swift */; };
27AC169823834510004BE19C /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27AC169623834510004BE19C /* UserNotifications.framework */; };
27B1E26D235E58190062E86F /* LocaleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B1E26C235E58190062E86F /* LocaleExtensions.swift */; };
27C461DE211B76500088A441 /* ShieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C461DD211B76500088A441 /* ShieldsView.swift */; };
27C46201211CD8D20088A441 /* DeferredTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A176323020CF2A6000126F25 /* DeferredTestUtils.swift */; };
Expand Down Expand Up @@ -1500,6 +1502,8 @@
279C756A219DDE3B001CD1CB /* FingerprintingProtection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FingerprintingProtection.swift; sourceTree = "<group>"; };
279C75C021A5B37D001CD1CB /* FingerprintingProtection.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = FingerprintingProtection.js; sourceTree = "<group>"; };
27A586E0214C0DDD000CAE3C /* PreferencesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTest.swift; sourceTree = "<group>"; };
27AC168F23833A16004BE19C /* MonthlyAdsGrantReminder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthlyAdsGrantReminder.swift; sourceTree = "<group>"; };
27AC169623834510004BE19C /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
27B1E26C235E58190062E86F /* LocaleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleExtensions.swift; sourceTree = "<group>"; };
27C461DD211B76500088A441 /* ShieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShieldsView.swift; sourceTree = "<group>"; };
27D114D32358FBBF00166534 /* BraveRewardsSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BraveRewardsSettingsViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2481,6 +2485,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
27AC169823834510004BE19C /* UserNotifications.framework in Frameworks */,
5DE768A520B3458400FF5533 /* Shared.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -3208,6 +3213,14 @@
path = Rewards;
sourceTree = "<group>";
};
27AC169223834134004BE19C /* Rewards */ = {
isa = PBXGroup;
children = (
27AC168F23833A16004BE19C /* MonthlyAdsGrantReminder.swift */,
);
path = Rewards;
sourceTree = "<group>";
};
27F443962135E11200296C58 /* BraveShareTo */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3858,6 +3871,7 @@
5DE7688520B3456D00FF5533 /* BraveShared */ = {
isa = PBXGroup;
children = (
27AC169223834134004BE19C /* Rewards */,
0AD4FEE8223A998700E00C05 /* Javascript */,
0AD4FED9223A8AF800E00C05 /* Extensions */,
0AD5E9BB2200590C00D0D91B /* Shields */,
Expand Down Expand Up @@ -3931,6 +3945,7 @@
7B604FC11C496005006EEEC3 /* Frameworks */ = {
isa = PBXGroup;
children = (
27AC169623834510004BE19C /* UserNotifications.framework */,
27FA2D4F234CC9FB004D5D2D /* BraveRewards.framework */,
0AC8E23A22A6C56D0064F3FA /* libYubiKit.a */,
272FCA98225CF8F00091E645 /* OnePasswordExtension.framework */,
Expand Down Expand Up @@ -5810,6 +5825,7 @@
5DE768A720B345C600FF5533 /* BraveStrings.swift in Sources */,
0AD4FEF2223AA6CA00E00C05 /* JSContextExtensions.swift in Sources */,
0AD4FEF4223AC32200E00C05 /* JSValueExtensions.swift in Sources */,
27AC169323834175004BE19C /* MonthlyAdsGrantReminder.swift in Sources */,
0AD4FEEE223AA09D00E00C05 /* BrowserifyExposable.swift in Sources */,
0A917388231D11960069A08B /* AppReview.swift in Sources */,
5D6DDEF3214003A6001FF0AE /* DAU.swift in Sources */,
Expand Down
18 changes: 18 additions & 0 deletions Client/Application/Delegates/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIViewControllerRestorati
@discardableResult fileprivate func startApplication(_ application: UIApplication, withLaunchOptions launchOptions: [AnyHashable: Any]?) -> Bool {
log.info("startApplication begin")

UNUserNotificationCenter.current().delegate = self

// Set the Firefox UA for browsing.
setUserAgent()

Expand Down Expand Up @@ -488,3 +490,19 @@ extension AppDelegate: MFMailComposeViewControllerDelegate {
startApplication(application!, withLaunchOptions: self.launchOptions)
}
}

extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
if MonthlyAdsGrantReminder.isMonthlyAdsReminderNotification(response.notification) {
// Open the rewards panel, showing the user their grant
if UIApplication.shared.applicationState != .active {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Give the UI a chance to be put together first
self.browserViewController.showBraveRewardsPanel()
}
} else {
browserViewController.showBraveRewardsPanel()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ extension BrowserViewController {
}
// Hide the current tab
rewards.ledger.selectedTabId = 0
// Fetch new promotions
rewards.ledger.fetchPromotions(nil)
}
}

Expand Down

0 comments on commit bf54017

Please sign in to comment.