Merge branch 'nt/profile-name-reminder'

This commit is contained in:
Nora Trapp 2020-01-30 10:40:40 -08:00
commit ceae3f1f3d
10 changed files with 95 additions and 221 deletions

View File

@ -632,8 +632,7 @@
8889007123DBB39B00F40186 /* reactionMegaphone_6.png in Resources */ = {isa = PBXBuildFile; fileRef = 8889006923DBB39B00F40186 /* reactionMegaphone_6.png */; };
8889007223DBB39B00F40186 /* reactionMegaphone_4.png in Resources */ = {isa = PBXBuildFile; fileRef = 8889006A23DBB39B00F40186 /* reactionMegaphone_4.png */; };
8889007323DBB39B00F40186 /* reactionMegaphone_5.png in Resources */ = {isa = PBXBuildFile; fileRef = 8889006B23DBB39B00F40186 /* reactionMegaphone_5.png */; };
8889007523DBD0B400F40186 /* RequiredProfileNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8889007423DBD0B400F40186 /* RequiredProfileNames.swift */; };
8889007723DBD61200F40186 /* requiredProfileNamesSplash.json in Resources */ = {isa = PBXBuildFile; fileRef = 8889007623DBD61200F40186 /* requiredProfileNamesSplash.json */; };
8889007523DBD0B400F40186 /* ProfileNameReminder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8889007423DBD0B400F40186 /* ProfileNameReminder.swift */; };
888C828223D795FA0059464B /* pinCreationFail.json in Resources */ = {isa = PBXBuildFile; fileRef = 888C827F23D795F90059464B /* pinCreationFail.json */; };
888C828323D795FA0059464B /* pinCreationInProgress.json in Resources */ = {isa = PBXBuildFile; fileRef = 888C828023D795FA0059464B /* pinCreationInProgress.json */; };
88905E9E229CCA96004E4234 /* ExpirationNagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88905E9D229CCA96004E4234 /* ExpirationNagView.swift */; };
@ -1514,8 +1513,7 @@
8889006923DBB39B00F40186 /* reactionMegaphone_6.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = reactionMegaphone_6.png; sourceTree = "<group>"; };
8889006A23DBB39B00F40186 /* reactionMegaphone_4.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = reactionMegaphone_4.png; sourceTree = "<group>"; };
8889006B23DBB39B00F40186 /* reactionMegaphone_5.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = reactionMegaphone_5.png; sourceTree = "<group>"; };
8889007423DBD0B400F40186 /* RequiredProfileNames.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredProfileNames.swift; sourceTree = "<group>"; };
8889007623DBD61200F40186 /* requiredProfileNamesSplash.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = requiredProfileNamesSplash.json; sourceTree = "<group>"; };
8889007423DBD0B400F40186 /* ProfileNameReminder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameReminder.swift; sourceTree = "<group>"; };
888C827F23D795F90059464B /* pinCreationFail.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pinCreationFail.json; sourceTree = "<group>"; };
888C828023D795FA0059464B /* pinCreationInProgress.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = pinCreationInProgress.json; sourceTree = "<group>"; };
88905E9D229CCA96004E4234 /* ExpirationNagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpirationNagView.swift; sourceTree = "<group>"; };
@ -2965,7 +2963,7 @@
children = (
88A505F923DBA1360005C012 /* IntroducingPINs.swift */,
88A505FB23DBAE4F0005C012 /* Reactions.swift */,
8889007423DBD0B400F40186 /* RequiredProfileNames.swift */,
8889007423DBD0B400F40186 /* ProfileNameReminder.swift */,
);
path = "Upgrade Views";
sourceTree = "<group>";
@ -2982,7 +2980,6 @@
888C827F23D795F90059464B /* pinCreationFail.json */,
888C828023D795FA0059464B /* pinCreationInProgress.json */,
8837F73C23D8DE8400772A32 /* pinCreationSuccess.json */,
8889007623DBD61200F40186 /* requiredProfileNamesSplash.json */,
);
path = Lottie;
sourceTree = "<group>";
@ -3644,7 +3641,6 @@
45B74A7F2044AAB600CD42F8 /* note-quiet.aifc in Resources */,
4C9D349B2369F11F006A4307 /* notificationPermission1.png in Resources */,
45B74A842044AAB600CD42F8 /* popcorn.aifc in Resources */,
8889007723DBD61200F40186 /* requiredProfileNamesSplash.json in Resources */,
45B74A7D2044AAB600CD42F8 /* popcorn-quiet.aifc in Resources */,
45B74A822044AAB600CD42F8 /* pulse.aifc in Resources */,
4C9D349D2369F11F006A4307 /* notificationPermission2.png in Resources */,
@ -4416,7 +4412,7 @@
34EA69402194933900702471 /* MediaDownloadView.swift in Sources */,
340FC8AB204DAC8D007AEB0F /* DomainFrontingCountryViewController.m in Sources */,
4C586926224FAB83003FD070 /* AVAudioSession+OWS.m in Sources */,
8889007523DBD0B400F40186 /* RequiredProfileNames.swift in Sources */,
8889007523DBD0B400F40186 /* ProfileNameReminder.swift in Sources */,
3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */,
34B3F8751E8DF1700035BE1A /* CallViewController.swift in Sources */,
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,

File diff suppressed because one or more lines are too long

View File

@ -57,8 +57,7 @@ class ExperienceUpgradeManager: NSObject {
private static func hasSplash(forExperienceUpgrade experienceUpgrade: ExperienceUpgrade) -> Bool {
switch experienceUpgrade.id {
case .introducingPins,
.requiredProfileNames:
case .introducingPins:
return true
default:
return false
@ -70,9 +69,6 @@ class ExperienceUpgradeManager: NSObject {
case .introducingPins:
let vc = IntroducingPinsSplash(experienceUpgrade: experienceUpgrade)
return OWSNavigationController(rootViewController: vc)
case .requiredProfileNames:
let vc = RequiredProfileNamesSplash(experienceUpgrade: experienceUpgrade)
return OWSNavigationController(rootViewController: vc)
default:
return nil
}
@ -84,7 +80,7 @@ class ExperienceUpgradeManager: NSObject {
switch experienceUpgrade.id {
case .introducingPins,
.reactions,
.requiredProfileNames:
.profileNameReminder:
return true
default:
return false
@ -97,8 +93,8 @@ class ExperienceUpgradeManager: NSObject {
return IntroducingPinsMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
case .reactions:
return ReactionsMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
case .requiredProfileNames:
return RequiredProfileNamesMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
case .profileNameReminder:
return ProfileNameReminderMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
default:
return nil
}

View File

@ -0,0 +1,53 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
import Lottie
class ProfileNameReminderMegaphone: MegaphoneView {
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
let hasProfileNameAlready = OWSProfileManager.shared().localFullName()?.isEmpty == false
titleText = hasProfileNameAlready
? NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_HAS_NAME_TITLE",
comment: "Title for required profile name megaphone when user already has a profile name")
: NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_NO_NAME_TITLE",
comment: "Title for required profile name megaphone when user doesn't have a profile name")
bodyText = hasProfileNameAlready
? NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_HAS_NAME_BODY",
comment: "Body for required profile name megaphone when user already has a profile name")
: NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_NO_NAME_BODY",
comment: "Body for required profile name megaphone when user doesn't have a profile name")
imageName = "profileMegaphone"
let primaryButton = MegaphoneView.Button(
title: NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_ACTION",
comment: "Action text for required profile name megaphone")
) { [weak self] in
let vc = ProfileViewController.forExperienceUpgrade {
self?.markAsComplete()
fromViewController.navigationController?.popToViewController(fromViewController, animated: true) {
fromViewController.navigationController?.setNavigationBarHidden(false, animated: false)
self?.dismiss(animated: false)
self?.presentToast(
text: NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_TOAST",
comment: "Toast indicating that a PIN has been created."),
fromViewController: fromViewController
)
}
}
fromViewController.navigationController?.pushViewController(vc, animated: true)
}
let secondaryButton = snoozeButton(fromViewController: fromViewController)
setButtons(primary: primaryButton, secondary: secondaryButton)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -1,188 +0,0 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
import Lottie
class RequiredProfileNamesMegaphone: MegaphoneView {
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
let hasProfileNameAlready = OWSProfileManager.shared().localFullName()?.isEmpty == false
titleText = hasProfileNameAlready
? NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_HAS_NAME_TITLE",
comment: "Title for required profile name megaphone when user already has a profile name")
: NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_NO_NAME_TITLE",
comment: "Title for required profile name megaphone when user doesn't have a profile name")
bodyText = hasProfileNameAlready
? NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_HAS_NAME_BODY",
comment: "Body for required profile name megaphone when user already has a profile name")
: NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_NO_NAME_BODY",
comment: "Body for required profile name megaphone when user doesn't have a profile name")
imageName = "profileMegaphone"
let primaryButton = MegaphoneView.Button(
title: NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_ACTION",
comment: "Action text for required profile name megaphone")
) { [weak self] in
let vc = ProfileViewController.forExperienceUpgrade {
self?.markAsComplete()
fromViewController.navigationController?.popToViewController(fromViewController, animated: true) {
fromViewController.navigationController?.setNavigationBarHidden(false, animated: false)
self?.dismiss(animated: false)
self?.presentToast(
text: NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_TOAST",
comment: "Toast indicating that a PIN has been created."),
fromViewController: fromViewController
)
}
}
fromViewController.navigationController?.pushViewController(vc, animated: true)
}
let secondaryButton = snoozeButton(fromViewController: fromViewController)
setButtons(primary: primaryButton, secondary: secondaryButton)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class RequiredProfileNamesSplash: SplashViewController {
let animationView = AnimationView(name: "requiredProfileNamesSplash")
var ows2FAManager: OWS2FAManager {
return .shared()
}
var hasProfileNameAlready: Bool {
return OWSProfileManager.shared().localFullName()?.isEmpty == false
}
override var canDismissWithGesture: Bool { return false }
// MARK: - View lifecycle
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
animationView.play()
navigationController?.setNavigationBarHidden(true, animated: false)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(false, animated: false)
}
override func loadView() {
self.view = UIView.container()
self.view.backgroundColor = Theme.backgroundColor
animationView.contentMode = .scaleAspectFit
animationView.loopMode = .playOnce
animationView.backgroundBehavior = .forceFinish
view.addSubview(animationView)
animationView.autoPinTopToSuperviewMargin(withInset: 10)
animationView.autoPinWidthToSuperview()
animationView.setContentHuggingLow()
animationView.setCompressionResistanceLow()
let title: String
let body: String
if hasProfileNameAlready {
title = NSLocalizedString("REQUIRE_PROFILE_NAMES_SPLASH_CONFIRMATION_TITLE",
comment: "Header for required profile names splash screen when the user already has a profile name")
body = NSLocalizedString("REQUIRE_PROFILE_NAMES_SPLASH_CONFIRMATION_DESCRIPTION",
comment: "Body text for required profile names splash screen when the user already has a profile name")
} else {
title = NSLocalizedString("REQUIRE_PROFILE_NAMES_SPLASH_CREATION_TITLE",
comment: "Header for required profile names splash screen when the user doesn't have a profile name")
body = NSLocalizedString("REQUIRE_PROFILE_NAMES_SPLASH_CREATION_DESCRIPTION",
comment: "Body text for required profile names splash screen when the user doesn't have a profile name")
}
let hMargin: CGFloat = ScaleFromIPhone5To7Plus(16, 24)
// Title label
let titleLabel = UILabel()
titleLabel.text = title
titleLabel.textAlignment = .center
titleLabel.font = UIFont.ows_dynamicTypeTitle1.ows_semibold()
titleLabel.textColor = Theme.primaryTextColor
titleLabel.minimumScaleFactor = 0.5
titleLabel.adjustsFontSizeToFitWidth = true
view.addSubview(titleLabel)
titleLabel.autoPinWidthToSuperview(withMargin: hMargin)
// The title label actually overlaps the hero image because it has a long shadow
// and we want the text to partially sit on top of this.
titleLabel.autoPinEdge(.top, to: .bottom, of: animationView, withOffset: -10)
titleLabel.setContentHuggingVerticalHigh()
// Body label
let bodyLabel = UILabel()
bodyLabel.text = body
bodyLabel.font = UIFont.ows_dynamicTypeBody
bodyLabel.textColor = Theme.primaryTextColor
bodyLabel.numberOfLines = 0
bodyLabel.lineBreakMode = .byWordWrapping
bodyLabel.textAlignment = .center
view.addSubview(bodyLabel)
bodyLabel.autoPinWidthToSuperview(withMargin: hMargin)
bodyLabel.autoPinEdge(.top, to: .bottom, of: titleLabel, withOffset: 6)
bodyLabel.setContentHuggingVerticalHigh()
// Primary button
let primaryButton = OWSFlatButton.button(title: primaryButtonTitle(),
font: UIFont.ows_dynamicTypeBody.ows_semibold(),
titleColor: .white,
backgroundColor: .ows_signalBlue,
target: self,
selector: #selector(didTapPrimaryButton))
primaryButton.autoSetHeightUsingFont()
view.addSubview(primaryButton)
primaryButton.autoPinWidthToSuperview(withMargin: hMargin)
primaryButton.autoPinEdge(.top, to: .bottom, of: bodyLabel, withOffset: 28)
primaryButton.setContentHuggingVerticalHigh()
primaryButton.autoPinBottomToSuperviewMargin(withInset: ScaleFromIPhone5(10))
}
@objc
func didTapPrimaryButton(_ sender: UIButton) {
let vc = ProfileViewController.forExperienceUpgrade { [weak self] in
self?.dismiss(animated: true)
}
navigationController?.pushViewController(vc, animated: true)
}
func primaryButtonTitle() -> String {
return hasProfileNameAlready
? NSLocalizedString("REQUIRE_PROFILE_NAMES_SPLASH_CONFIRMATION_BUTTON",
comment: "Button to start a confirm profile name flow from the one time splash screen that appears after upgrading")
: NSLocalizedString("REQUIRE_PROFILE_NAMES_SPLASH_CREATION_BUTTON",
comment: "Button to start a create profile name flow from the one time splash screen that appears after upgrading")
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
guard let fromViewController = presentingViewController else {
return owsFailDebug("Trying to dismiss while not presented.")
}
super.dismiss(animated: flag) { [weak self] in
self?.presentToast(
text: NSLocalizedString("REQUIRE_PROFILE_NAMES_MEGAPHONE_TOAST",
comment: "Toast indicating that a PIN has been created."),
fromViewController: fromViewController
)
completion?()
}
}
}

View File

@ -562,6 +562,14 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat
.then(^{
[modalActivityIndicator dismissWithCompletion:^{
[weakSelf updateProfileCompleted];
// Clear the profile name experience upgrade if the user edits their profile name,
// even if they didn't dismiss the reminder directly.
[ProfileViewController.databaseStorage
asyncWriteWithBlock:^(SDSAnyWriteTransaction *transaction) {
[ExperienceUpgradeFinder
clearProfileNameReminderWithTransaction:transaction.unwrapGrdbWrite];
}];
}];
})
.catch(^(NSError *error) {

View File

@ -68,6 +68,10 @@ public class ReactionManager: NSObject {
}
SSKEnvironment.shared.messageSenderJobQueue.add(message: outgoingMessage.asPreparer, transaction: transaction)
// Mark the reactions experience upgrade complete if the user
// sends a reaction, even if they didn't dismiss it directly.
ExperienceUpgradeFinder.markAsComplete(experienceUpgradeId: .reactions, transaction: transaction.unwrapGrdbWrite)
}
@objc

View File

@ -8,7 +8,7 @@ import Reachability
public enum ExperienceUpgradeId: String, CaseIterable {
case introducingPins = "009"
case reactions = "010"
case requiredProfileNames = "011"
case profileNameReminder = "011"
// Until this flag is true the upgrade won't display to users.
var hasLaunched: Bool {
@ -19,8 +19,8 @@ public enum ExperienceUpgradeId: String, CaseIterable {
SSKEnvironment.shared.reachabilityManager.isReachable
case .reactions:
return FeatureFlags.reactionSend
case .requiredProfileNames:
return RemoteConfig.requiredProfileNames
case .profileNameReminder:
return RemoteConfig.profileNameReminder
}
}
@ -55,7 +55,7 @@ public enum ExperienceUpgradeId: String, CaseIterable {
@objc
public class ExperienceUpgradeFinder: NSObject {
// MARK: - Instance Methods
// MARK: -
public class func next(transaction: GRDBReadTransaction) -> ExperienceUpgrade? {
return allActiveExperienceUpgrades(transaction: transaction).first { !$0.isSnoozed }
@ -83,6 +83,13 @@ public class ExperienceUpgradeFinder: NSObject {
experienceUpgrade.upsertWith(transaction: transaction.asAnyWrite) { $0.lastSnoozedTimestamp = Date().timeIntervalSince1970 }
}
public class func markAsComplete(experienceUpgradeId: ExperienceUpgradeId, transaction: GRDBWriteTransaction) {
Logger.info("marking experience upgrade as complete \(experienceUpgradeId)")
allActiveExperienceUpgrades(transaction: transaction)
.filter { $0.id == experienceUpgradeId }
.forEach { markAsComplete(experienceUpgrade: $0, transaction: transaction) }
}
public class func markAsComplete(experienceUpgrade: ExperienceUpgrade, transaction: GRDBWriteTransaction) {
Logger.info("marking experience upgrade as complete \(experienceUpgrade.uniqueId)")
experienceUpgrade.upsertWith(transaction: transaction.asAnyWrite) { $0.isComplete = true }
@ -96,11 +103,20 @@ public class ExperienceUpgradeFinder: NSObject {
.forEach { markAsComplete(experienceUpgrade: $0, transaction: transaction) }
}
// MARK: - Experience Specific Helpers
@objc
public class func hasPendingPinExperienceUpgrade(transaction: GRDBReadTransaction) -> Bool {
return hasIncomplete(experienceUpgradeId: .introducingPins, transaction: transaction)
}
@objc
class func clearProfileNameReminder(transaction: GRDBWriteTransaction) {
ExperienceUpgradeFinder.markAsComplete(experienceUpgradeId: .profileNameReminder, transaction: transaction)
}
// MARK: -
/// Returns an array of all experience upgrades currently being run that have
/// yet to be completed. Sorted by the order of the `ExperienceUpgradeId` enumeration.
private class func allActiveExperienceUpgrades(transaction: GRDBReadTransaction) -> [ExperienceUpgrade] {
@ -144,11 +160,6 @@ public class ExperienceUpgradeFinder: NSObject {
}
}
@objc
extension OWS2FAManager {
}
public extension ExperienceUpgrade {
var id: ExperienceUpgradeId! {
return ExperienceUpgradeId(rawValue: uniqueId)

View File

@ -262,7 +262,4 @@ public class FeatureFlags: NSObject {
@objc
public static let useZKGroups = false
@objc
public static let requiredProfileNames = build.includes(.qa)
}

View File

@ -25,10 +25,8 @@ public class RemoteConfig: NSObject {
}
@objc
public static var requiredProfileNames: Bool {
// Block this off until we're ready to be turning this on for the public.
guard FeatureFlags.requiredProfileNames else { return false }
return isEnabled("ios.requiredProfileNames")
public static var profileNameReminder: Bool {
return isEnabled("ios.profileNameReminder")
}
@objc