Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

End of Year: require account #474

Merged
merged 19 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
755c35b
Adds remote key for requiring an account or not to display EoY stories
leandroalonso Oct 31, 2022
457e54e
If the user is not logged in present the login/create account screen
leandroalonso Oct 31, 2022
5b6054e
Display EoY prompt on login/create account screen
leandroalonso Oct 31, 2022
6cc76de
Track when the modal is shown to avoid always showing it
leandroalonso Oct 31, 2022
2903fad
When a user login, reset the modal
leandroalonso Oct 31, 2022
8e7d636
Add a notification for when registration is not required for EOY
leandroalonso Oct 31, 2022
90dfeaa
Add a static property to keep track if account is needed or not
leandroalonso Oct 31, 2022
eeb2395
Update EOY account required after fetching new values
leandroalonso Oct 31, 2022
7608697
Display the prompt if account is not required anymore
leandroalonso Oct 31, 2022
676bbb4
Merge branch 'task/376-preload-stories' into task/376-require-registr…
leandroalonso Nov 1, 2022
a99abc9
Wrap ProfileIntroViewController into a navigation controller
leandroalonso Nov 2, 2022
ab1a897
Ensure ProfileIntroViewController to appear full screen
leandroalonso Nov 2, 2022
f0d4bda
Add a new notification called userSignedIn that is posted when the us…
emilylaguna Nov 3, 2022
bc14b31
Add an internal state to determine when we should show the end of year
emilylaguna Nov 3, 2022
966a81d
Update how the end of year prompt or stories are shown based on the i…
emilylaguna Nov 3, 2022
a1b50e1
Set the internal state to waiting for login
emilylaguna Nov 3, 2022
a32cfef
Update how we reset the modal shown setting
emilylaguna Nov 3, 2022
8cdb93c
Update the MainTabBar to use the EOY state methods
emilylaguna Nov 3, 2022
7915788
Fix lint
emilylaguna Nov 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions podcasts/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,16 +268,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Constants.RemoteParams.periodicSaveTimeMs: NSNumber(value: Constants.RemoteParams.periodicSaveTimeMsDefault),
Constants.RemoteParams.episodeSearchDebounceMs: NSNumber(value: Constants.RemoteParams.episodeSearchDebounceMsDefault),
Constants.RemoteParams.podcastSearchDebounceMs: NSNumber(value: Constants.RemoteParams.podcastSearchDebounceMsDefault),
Constants.RemoteParams.customStorageLimitGB: NSNumber(value: Constants.RemoteParams.customStorageLimitGBDefault)
Constants.RemoteParams.customStorageLimitGB: NSNumber(value: Constants.RemoteParams.customStorageLimitGBDefault),
Constants.RemoteParams.endOfYearRequireAccount: NSNumber(value: Constants.RemoteParams.endOfYearRequireAccountDefault)
])

remoteConfig.fetch(withExpirationDuration: 2.hour) { status, _ in
remoteConfig.fetch(withExpirationDuration: 2.hour) { [weak self] status, _ in
if status == .success {
remoteConfig.activate(completion: nil)

self?.updateEndOfYearRemoteValue()
}
}
}

private func updateEndOfYearRemoteValue() {
// Update if EOY requires an account to be seen
EndOfYear.requireAccount = Settings.endOfYearRequireAccount
}

private func postLaunchSetup() {
if !UserDefaults.standard.bool(forKey: "CreatedDefPlaylistsV2") {
PlaylistManager.createDefaultFilters()
Expand Down
4 changes: 4 additions & 0 deletions podcasts/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ struct Constants {
static let reviewRequestDates = "reviewRequestDates"

static let showBadgeFor2022EndOfYear = "showBadgeFor2022EndOfYear"
static let modal2022HasBeenShown = "modal2022HasBeenShown"
}

enum Values {
Expand Down Expand Up @@ -243,6 +244,9 @@ struct Constants {

static let customStorageLimitGB = "custom_storage_limit_gb"
static let customStorageLimitGBDefault: Int = 10

static let endOfYearRequireAccount = "end_of_year_require_account"
static let endOfYearRequireAccountDefault: Bool = true
}

static let defaultDebounceTime: TimeInterval = 0.5
Expand Down
73 changes: 72 additions & 1 deletion podcasts/End of Year/EndOfYear.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftUI
import PocketCastsServer
import MaterialComponents.MaterialBottomSheet
import PocketCastsDataModel

Expand All @@ -8,6 +9,21 @@ struct EndOfYear {
FeatureFlag.endOfYear && DataManager.sharedManager.isEligibleForEndOfYearStories()
}

/// Internal state machine to determine how we should react to login changes
/// and when to show the modal vs go directly to the stories
private static var state: EndOfYearState = .showModalIfNeeded

static var requireAccount: Bool = Settings.endOfYearRequireAccount {
didSet {
// If registration is not needed anymore and this user is logged out
// Show the prompt again.
if oldValue && !requireAccount && !SyncManager.isUserLoggedIn() {
Settings.endOfYearModalHasBeenShown = false
NotificationCenter.postOnMainThread(notification: .eoyRegistrationNotRequired, object: nil)
}
}
}

var presentationMode: UIModalPresentationStyle {
UIDevice.current.isiPad() ? .formSheet : .fullScreen
}
Expand All @@ -16,19 +32,54 @@ struct EndOfYear {
.init(top: 0, leading: 0, bottom: UIDevice.current.isiPad() ? 5 : 0, trailing: 0)
}

init() {
Self.requireAccount = Settings.endOfYearRequireAccount
}

func showPrompt(in viewController: UIViewController) {
guard Self.isEligible else {
guard Self.isEligible, !Settings.endOfYearModalHasBeenShown else {
return
}

MDCSwiftUIWrapper.present(EndOfYearModal(), in: viewController)
}

func showPromptBasedOnState(in viewController: UIViewController) {
switch Self.state {

// If we're in the default state, then check to see if we should show the prompt
case .showModalIfNeeded:
showPrompt(in: viewController)

// If we were in the waiting state, but the user has logged in, then show stories
case .loggedIn:
Self.state = .showModalIfNeeded
showStories(in: viewController)

// If the user has seen the prompt, and chosen to login, but then has cancelled out of the flow without logging in,
// When this code is ran from MainTabController viewDidAppear we will still be in the waiting state
// reset the state to the default to restart the process over again
case .waitingForLogin:
Self.state = .showModalIfNeeded
}
}

func showStories(in viewController: UIViewController) {
guard FeatureFlag.endOfYear else {
return
}

if Self.requireAccount && !SyncManager.isUserLoggedIn() {
Self.state = .waitingForLogin

let profileIntroController = ProfileIntroViewController()
profileIntroController.infoLabelText = L10n.eoyCreateAccountToSee
let navigationController = UINavigationController(rootViewController: profileIntroController)
navigationController.modalPresentationStyle = .fullScreen
viewController.present(navigationController, animated: true)
return
}

let storiesViewController = StoriesHostingController(rootView: StoriesView(dataSource: EndOfYearStoriesDataSource()).padding(storiesPadding))
storiesViewController.view.backgroundColor = .black
storiesViewController.modalPresentationStyle = presentationMode
Expand Down Expand Up @@ -59,6 +110,22 @@ struct EndOfYear {
StoryShareableProvider.generatedItem = asset() as? UIImage
}
}

func resetStateIfNeeded() {
// When a user logs in (or creates an account) we mark the EOY modal as not
// shown to show it again.
if Self.state == .showModalIfNeeded {
Settings.endOfYearModalHasBeenShown = false
return
}

guard Self.state == .waitingForLogin else { return }

// If we're in the waiting for login state (the user has seen the prompt, and chosen to login)
// Update the current state based on whether the user is logged in or not
// If the user did not login, then just reset the state to the default showModalIfNeeded
Self.state = SyncManager.isUserLoggedIn() ? .loggedIn : .showModalIfNeeded
}
}

class StoriesHostingController<ContentView: View>: UIHostingController<ContentView> {
Expand All @@ -70,3 +137,7 @@ class StoriesHostingController<ContentView: View>: UIHostingController<ContentVi
.portrait
}
}

private enum EndOfYearState {
case showModalIfNeeded, waitingForLogin, loggedIn
}
3 changes: 3 additions & 0 deletions podcasts/End of Year/Views/EndOfYearModal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ struct EndOfYearModal: View {
}
.frame(maxWidth: Constants.maxWidth)
.applyDefaultThemeOptions()
.onAppear {
Settings.endOfYearModalHasBeenShown = true
}
}

var pill: some View {
Expand Down
28 changes: 26 additions & 2 deletions podcasts/MainTabBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class MainTabBarController: UITabBarController, NavigationProtocol {
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(unhideNavBar), name: Constants.Notifications.unhideNavBarRequested, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(profileSeen), name: Constants.Notifications.profileSeen, object: nil)

observersForEndOfYearStats()
}

override func viewDidAppear(_ animated: Bool) {
Expand All @@ -61,7 +63,7 @@ class MainTabBarController: UITabBarController, NavigationProtocol {
checkPromotionFinishedAcknowledged()
checkWhatsNewAcknowledged()

endOfYear.showPrompt(in: self)
endOfYear.showPromptBasedOnState(in: self)
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
Expand Down Expand Up @@ -394,13 +396,35 @@ class MainTabBarController: UITabBarController, NavigationProtocol {
return true
}

// MARK: - End of Year badge
// MARK: - End of Year

@objc private func profileSeen() {
profileTabBarItem.badgeValue = nil
Settings.showBadgeFor2022EndOfYear = false
}

func observersForEndOfYearStats() {
guard FeatureFlag.endOfYear else {
return
}

NotificationCenter.default.addObserver(forName: .userSignedIn, object: nil, queue: .main) { notification in
self.endOfYear.resetStateIfNeeded()
}

// If the requirement for EOY changes and registration is not required anymore
// Show the modal
NotificationCenter.default.addObserver(forName: .eoyRegistrationNotRequired, object: nil, queue: .main) { [weak self] _ in
guard let self else {
return
}

if self.presentedViewController == nil {
self.endOfYear.showPrompt(in: self)
}
}
}

// MARK: - Orientation

// we implement this here to lock all views (except presented modal VCs to portrait)
Expand Down
1 change: 1 addition & 0 deletions podcasts/NewEmailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ class NewEmailViewController: UIViewController, UITextFieldDelegate {
ServerSettings.setSyncingEmail(email: username)

NotificationCenter.default.post(name: .userLoginDidChange, object: nil)
NotificationCenter.postOnMainThread(notification: .userSignedIn)

Analytics.track(.userAccountCreated)
}
Expand Down
6 changes: 6 additions & 0 deletions podcasts/Notifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ import Foundation
extension NSNotification.Name {
/// When a user has signed in, signed out, or been signed in during account creation
static let userLoginDidChange = NSNotification.Name("User.LoginChanged")

/// When a user logs via login or account creation
static let userSignedIn = NSNotification.Name("User.SignedOrCreatedAccount")

/// When the requirement for having an account or not to see End Of Year Stats changes
static let eoyRegistrationNotRequired = NSNotification.Name("EOY.RegistrationNotRequired")
}
4 changes: 3 additions & 1 deletion podcasts/ProfileIntroViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ class ProfileIntroViewController: PCViewController, SyncSigninDelegate {

@IBOutlet var infoLabel: ThemeableLabel! {
didSet {
infoLabel.text = L10n.signInMessage
infoLabel.text = infoLabelText ?? L10n.signInMessage
infoLabel.style = .primaryText02
}
}

var infoLabelText: String?

override func viewDidLoad() {
super.viewDidLoad()

Expand Down
17 changes: 16 additions & 1 deletion podcasts/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ class Settings: NSObject {
UserDefaults.standard.bool(forKey: Constants.UserDefaults.analyticsOptOut)
}

// MARK: - Profile Badge for End of Year 2022
// MARK: - End of Year 2022

class var showBadgeFor2022EndOfYear: Bool {
set {
Expand All @@ -731,6 +731,16 @@ class Settings: NSObject {
}
}

class var endOfYearModalHasBeenShown: Bool {
set {
UserDefaults.standard.set(newValue, forKey: Constants.UserDefaults.modal2022HasBeenShown)
}

get {
UserDefaults.standard.bool(forKey: Constants.UserDefaults.modal2022HasBeenShown)
}
}

// MARK: - Variables that are loaded/changed through Firebase

#if !os(watchOS)
Expand All @@ -746,6 +756,11 @@ class Settings: NSObject {
remoteMsToTime(key: Constants.RemoteParams.episodeSearchDebounceMs)
}

static var endOfYearRequireAccount: Bool {
let remote = RemoteConfig.remoteConfig().configValue(forKey: Constants.RemoteParams.endOfYearRequireAccount)
return remote.boolValue
}

private class func remoteMsToTime(key: String) -> TimeInterval {
let remoteMs = RemoteConfig.remoteConfig().configValue(forKey: key)

Expand Down
2 changes: 2 additions & 0 deletions podcasts/SyncSigninViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ class SyncSigninViewController: PCViewController, UITextFieldDelegate {
RefreshManager.shared.refreshPodcasts(forceEvenIfRefreshedRecently: true)
Settings.setPromotionFinishedAcknowledged(true)
Settings.setLoginDetailsUpdated()

NotificationCenter.postOnMainThread(notification: .userSignedIn)
})
}
}
Expand Down