diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index c2a2969408..8c98fd3550 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -869,6 +869,7 @@ 2C315E6F1F0A947D0039E4F0 /* LessonPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0899842B1ECDE194005C0B27 /* LessonPresenter.swift */; }; 2C315E701F0A947D0039E4F0 /* LessonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0899842E1ECDE19E005C0B27 /* LessonView.swift */; }; 2C315E711F0A947D0039E4F0 /* PagerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C1943F1ED0A05D00A41B72 /* PagerController.swift */; }; + 2C4DA9E91F38729500E392FA /* LocalNotificationsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4DA9E81F38729500E392FA /* LocalNotificationsHelper.swift */; }; 2C5F48D71F0F6FBD00C3A398 /* AdaptiveStepPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5F48D51F0F6ED800C3A398 /* AdaptiveStepPresenter.swift */; }; 2C5F48DB1F0FA0A500C3A398 /* AdaptiveStepsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5F48D91F0FA04300C3A398 /* AdaptiveStepsPresenter.swift */; }; 2C5F6BA91F20B2A900C251D3 /* CongratulationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5F6BA71F20B2A900C251D3 /* CongratulationViewController.swift */; }; @@ -1843,6 +1844,7 @@ 2C04C1091F16075A006B69B3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = en; path = en.lproj/step3.html; sourceTree = ""; }; 2C04C10A1F16075D006B69B3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = en; path = en.lproj/step2.html; sourceTree = ""; }; 2C04C10B1F16075F006B69B3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = en; path = en.lproj/step1.html; sourceTree = ""; }; + 2C4DA9E81F38729500E392FA /* LocalNotificationsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalNotificationsHelper.swift; sourceTree = ""; }; 2C5F48D51F0F6ED800C3A398 /* AdaptiveStepPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveStepPresenter.swift; sourceTree = ""; }; 2C5F48D91F0FA04300C3A398 /* AdaptiveStepsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveStepsPresenter.swift; sourceTree = ""; }; 2C5F6BA71F20B2A900C251D3 /* CongratulationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CongratulationViewController.swift; sourceTree = ""; }; @@ -3484,6 +3486,7 @@ 2C76ACCC1F16496C0077D9D7 /* RatingHelper.swift */, 2CCEB4941F27755800B45D63 /* AnalyticsEvents+Adaptive.swift */, 2C733C391F29E090000E7FAF /* StatsHelper.swift */, + 2C4DA9E81F38729500E392FA /* LocalNotificationsHelper.swift */, ); name = Helpers; sourceTree = ""; @@ -5370,6 +5373,7 @@ 0805FE461F0D390B001226B4 /* CodePlaygroundManager.swift in Sources */, 864D66AB1E83DE03001E8D9E /* Scripts.swift in Sources */, 864D66AC1E83DE03001E8D9E /* Scripts.plist in Sources */, + 2C4DA9E91F38729500E392FA /* LocalNotificationsHelper.swift in Sources */, 864D66AD1E83DE03001E8D9E /* Constants.swift in Sources */, 864D66AE1E83DE03001E8D9E /* Images.swift in Sources */, 864D66B01E83DE03001E8D9E /* ApplicationInfo.swift in Sources */, diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 8c926e63f7..df59345ae1 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -249,4 +249,8 @@ NewLevelCongratulationShareText = "I've reached level %@ in %@ app. Join and bea NewLevelCongratulationText = "You've reached level %@!"; RatingCongratulationText = "Correct! +%@ XP"; ShareAchievement = "Share achievement"; +RetentionNotificationYesterday = "You scored %@ XP yesterday. Go back and improve your score!"; +RetentionNotificationYesterdayZero = "You didn't score any points for yesterday. Improve your score today!"; +RetentionNotificationYesterdayStreak = "You have been learning %@ in a row. Go back and keep learning!"; +RetentionNotificationWeekly = "You haven't done it for a long time. Go back and keep learning!"; SocialSignupWithExistingEmail = "This email is already used. To log in using this social account you have to log in via email and password"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 805b7be463..77911622dc 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -250,4 +250,8 @@ NewLevelCongratulationShareText = "Я достиг %@ уровня в прило NewLevelCongratulationText = "Вы достигли %@ уровня!"; RatingCongratulationText = "Правильно! +%@ опыта"; ShareAchievement = "Поделиться достижением"; +RetentionNotificationYesterday = "Вчера Вы набрали %@ опыта. Возвращайтесь и улучшите свой результат!"; +RetentionNotificationYesterdayZero = "За вчерашний день Вы ничего не решили. Улучшите свой результат сегодня!"; +RetentionNotificationYesterdayStreak = "Вы занимаетесь уже %@ подряд. Возвращайтесь и продолжайте учиться!"; +RetentionNotificationWeekly = "Вы давно не занимались. Возвращайтесь и продолжайте учиться!"; SocialSignupWithExistingEmail = "Указанный email привязан к другому аккаунту. Чтобы входить через эту социальную сеть, необходимо войти, используя email и пароль"; diff --git a/StepicAdaptiveCourse/AdaptiveAppDelegate.swift b/StepicAdaptiveCourse/AdaptiveAppDelegate.swift index 817fcd9dc1..80b3bde530 100644 --- a/StepicAdaptiveCourse/AdaptiveAppDelegate.swift +++ b/StepicAdaptiveCourse/AdaptiveAppDelegate.swift @@ -28,12 +28,26 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DefaultsContainer.launch.didLaunch = true } + LocalNotificationsHelper.registerNotifications() + + if let launchNotification = launchOptions?[UIApplicationLaunchOptionsKey.localNotification] as? UILocalNotification { + if let userInfo = launchNotification.userInfo as? [String: String], let notificationType = userInfo["type"] { + AnalyticsReporter.reportEvent(AnalyticsEvents.Adaptive.localNotification, parameters: ["type": notificationType]) + } + } + return true } func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + LocalNotificationsHelper.schedule(notification: .tomorrow) + LocalNotificationsHelper.schedule(notification: .weekly) + } + + func application(_ application: UIApplication, didReceive notification: UILocalNotification) { + if let userInfo = notification.userInfo as? [String: String], let notificationType = userInfo["type"] { + AnalyticsReporter.reportEvent(AnalyticsEvents.Adaptive.localNotification, parameters: ["type": notificationType]) + } } func applicationDidEnterBackground(_ application: UIApplication) { @@ -46,7 +60,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + LocalNotificationsHelper.cancelAllNotifications() } func applicationWillTerminate(_ application: UIApplication) { diff --git a/StepicAdaptiveCourse/AnalyticsEvents+Adaptive.swift b/StepicAdaptiveCourse/AnalyticsEvents+Adaptive.swift index c39a257985..03f225d124 100644 --- a/StepicAdaptiveCourse/AnalyticsEvents+Adaptive.swift +++ b/StepicAdaptiveCourse/AnalyticsEvents+Adaptive.swift @@ -25,5 +25,6 @@ extension AnalyticsEvents { static let easy = "adaptive_reaction_easy" static let hard = "adaptive_reaction_hard" } + static let localNotification = "adaptive_opened_by_local_notification" } } diff --git a/StepicAdaptiveCourse/LocalNotificationsHelper.swift b/StepicAdaptiveCourse/LocalNotificationsHelper.swift new file mode 100644 index 0000000000..200545f9a3 --- /dev/null +++ b/StepicAdaptiveCourse/LocalNotificationsHelper.swift @@ -0,0 +1,88 @@ +// +// LocalNotificationsHelper.swift +// Stepic +// +// Created by Vladislav Kiryukhin on 07.08.2017. +// Copyright © 2017 Alex Karpov. All rights reserved. +// + +import UIKit + +enum LocalNotification { + case tomorrow, weekly + + var fireDate: Date { + switch self { + case .tomorrow: + return Date(timeIntervalSinceNow: 24 * 60 * 60) + case .weekly: + return Date(timeIntervalSinceNow: 2 * 24 * 60 * 60) + } + } + + var repeatInterval: NSCalendar.Unit { + switch self { + case .tomorrow: + return NSCalendar.Unit(rawValue: 0) + case .weekly: + return NSCalendar.Unit.weekOfYear + } + } + + var notification: UILocalNotification { + let localNotification = UILocalNotification() + localNotification.soundName = UILocalNotificationDefaultSoundName + localNotification.repeatInterval = self.repeatInterval + localNotification.fireDate = self.fireDate + + switch self { + case .tomorrow: + let streak = StatsHelper.currentDayStreak + if streak == 0 { + // 0 points today, 0 points prev + localNotification.alertBody = NSLocalizedString("RetentionNotificationYesterdayZero", comment: "") + localNotification.userInfo = ["type": "yesterday_zero"] + } else if streak == 1 { + // X points today, 0 points prev + if let todayXP = StatsHelper.loadStats()?[StatsHelper.dayByDate(Date())] { + localNotification.alertBody = String(format: NSLocalizedString("RetentionNotificationYesterday", comment: ""), "\(todayXP)") + localNotification.userInfo = ["type": "yesterday"] + } + } else { + // X points today, X points prev + var streakDays = "\(streak) " + switch (streak % 10) { + case 1: streakDays += NSLocalizedString("days1", comment: "") + case 2, 3, 4: streakDays += NSLocalizedString("days234", comment: "") + default: streakDays += NSLocalizedString("days567890", comment: "") + } + + localNotification.alertBody = String(format: NSLocalizedString("RetentionNotificationYesterdayStreak", comment: ""), "\(streakDays)") + localNotification.userInfo = ["type": "yesterday_streak"] + } + case .weekly: + localNotification.alertBody = NSLocalizedString("RetentionNotificationWeekly", comment: "") + localNotification.userInfo = ["type": "weekly"] + } + + return localNotification + } +} + +class LocalNotificationsHelper { + static func cancelAllNotifications() { + print("local notifications: cancelled all") + UIApplication.shared.cancelAllLocalNotifications() + } + + static func schedule(notification: LocalNotification) { + print("local notifications: scheduled notification with fire date = \(notification.fireDate)") + UIApplication.shared.scheduleLocalNotification(notification.notification) + } + + static func registerNotifications() { + let settings: UIUserNotificationSettings = + UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil) + UIApplication.shared.registerUserNotificationSettings(settings) + } +} diff --git a/StepicAdaptiveCourse/StatsHelper.swift b/StepicAdaptiveCourse/StatsHelper.swift index 5557e14728..83fe98af2b 100644 --- a/StepicAdaptiveCourse/StatsHelper.swift +++ b/StepicAdaptiveCourse/StatsHelper.swift @@ -15,13 +15,27 @@ class StatsHelper { private static let secondsInDay: TimeInterval = 24 * 60 * 60 - private static func dayByDate(_ date: Date) -> Int { + static var currentDayStreak: Int { + get { + var curDay = StatsHelper.dayByDate(Date()) + while curDay > 0 { + if let todayXP = StatsHelper.loadStats()?[curDay], todayXP != 0 { + curDay -= 1 + } else { + break + } + } + return StatsHelper.dayByDate(Date()) - curDay + } + } + + static func dayByDate(_ date: Date) -> Int { // Day num (01.01.1970 - 0, 02.01.1970 - 1, ...) let dayNum = Int(date.timeIntervalSince1970 / secondsInDay) return dayNum } - private static func dateByDay(_ day: Int) -> Date { + static func dateByDay(_ day: Int) -> Date { // 00:00 am target day let date = Date(timeIntervalSince1970: secondsInDay * Double(day)) return date