From 2c0aa7a222dec1df826313fea16cf695feec3992 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 12 Feb 2019 13:40:40 -0500 Subject: [PATCH] Sketch out the onboarding permissions view. --- Signal.xcodeproj/project.pbxproj | 8 + .../BackupRestoreViewController.swift | 2 +- .../Registration/OnboardingDelegate.swift | 11 ++ .../OnboardingPermissionsViewController.swift | 180 ++++++++++++++++++ .../environment/PushRegistrationManager.swift | 2 +- SignalMessaging/categories/UIView+OWS.h | 1 + SignalMessaging/categories/UIView+OWS.m | 9 + 7 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 Signal/src/ViewControllers/Registration/OnboardingDelegate.swift create mode 100644 Signal/src/ViewControllers/Registration/OnboardingPermissionsViewController.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c26f2904f..9b7ca1504 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -72,6 +72,8 @@ 34480B671FD0AA9400BC14EF /* UIFont+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B651FD0AA9400BC14EF /* UIFont+OWS.m */; }; 34480B681FD0AA9400BC14EF /* UIFont+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = 34480B661FD0AA9400BC14EF /* UIFont+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; }; 344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */ = {isa = PBXBuildFile; fileRef = 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */; }; + 3448E15C22133274004B052E /* OnboardingPermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448E15B22133274004B052E /* OnboardingPermissionsViewController.swift */; }; + 3448E15E221333F5004B052E /* OnboardingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448E15D221333F5004B052E /* OnboardingDelegate.swift */; }; 344F248D2007CCD600CFB4F4 /* DisplayableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248C2007CCD600CFB4F4 /* DisplayableText.swift */; }; 345BC30C2047030700257B7C /* OWS2FASettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 345BC30B2047030600257B7C /* OWS2FASettingsViewController.m */; }; 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */; }; @@ -723,6 +725,8 @@ 34480B661FD0AA9400BC14EF /* UIFont+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIFont+OWS.h"; sourceTree = ""; }; 344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOrphanDataCleaner.h; sourceTree = ""; }; 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOrphanDataCleaner.m; sourceTree = ""; }; + 3448E15B22133274004B052E /* OnboardingPermissionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingPermissionsViewController.swift; sourceTree = ""; }; + 3448E15D221333F5004B052E /* OnboardingDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingDelegate.swift; sourceTree = ""; }; 34491FC11FB0F78500B3E5A3 /* my */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = my; path = translations/my.lproj/Localizable.strings; sourceTree = ""; }; 344F248C2007CCD600CFB4F4 /* DisplayableText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayableText.swift; sourceTree = ""; }; 345BC30A2047030600257B7C /* OWS2FASettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS2FASettingsViewController.h; sourceTree = ""; }; @@ -1439,6 +1443,8 @@ 3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */, 340FC879204DAC8C007AEB0F /* CodeVerificationViewController.h */, 340FC877204DAC8C007AEB0F /* CodeVerificationViewController.m */, + 3448E15D221333F5004B052E /* OnboardingDelegate.swift */, + 3448E15B22133274004B052E /* OnboardingPermissionsViewController.swift */, 346E9D5321B040B600562252 /* RegistrationController.swift */, 340FC878204DAC8C007AEB0F /* RegistrationViewController.h */, 340FC876204DAC8C007AEB0F /* RegistrationViewController.m */, @@ -3496,6 +3502,7 @@ 4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */, 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */, 34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */, + 3448E15C22133274004B052E /* OnboardingPermissionsViewController.swift in Sources */, 34D920E720E179C200D51158 /* OWSMessageFooterView.m in Sources */, 341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */, 348BB25D20A0C5530047AEC2 /* ContactShareViewHelper.swift in Sources */, @@ -3586,6 +3593,7 @@ 34D1F0861F8678AA0066283D /* ConversationViewController.m in Sources */, 3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */, B90418E6183E9DD40038554A /* DateUtil.m in Sources */, + 3448E15E221333F5004B052E /* OnboardingDelegate.swift in Sources */, 340FC8BD204DAC8D007AEB0F /* ShowGroupMembersViewController.m in Sources */, 3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */, 459311FC1D75C948008DD4F0 /* OWSDeviceTableViewCell.m in Sources */, diff --git a/Signal/src/ViewControllers/Registration/BackupRestoreViewController.swift b/Signal/src/ViewControllers/Registration/BackupRestoreViewController.swift index 7da4aea50..1887575c0 100644 --- a/Signal/src/ViewControllers/Registration/BackupRestoreViewController.swift +++ b/Signal/src/ViewControllers/Registration/BackupRestoreViewController.swift @@ -170,6 +170,6 @@ public class BackupRestoreViewController: OWSTableViewController { // MARK: Orientation public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return DefaultUIInterfaceOrientationMask() + return .portrait } } diff --git a/Signal/src/ViewControllers/Registration/OnboardingDelegate.swift b/Signal/src/ViewControllers/Registration/OnboardingDelegate.swift new file mode 100644 index 000000000..5e2bd4dda --- /dev/null +++ b/Signal/src/ViewControllers/Registration/OnboardingDelegate.swift @@ -0,0 +1,11 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import UIKit + +@objc +public protocol OnboardingController: class { + func onboardingPermissionsWasSkipped(viewController: UIViewController) + func onboardingPermissionsDidComplete(viewController: UIViewController) +} diff --git a/Signal/src/ViewControllers/Registration/OnboardingPermissionsViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingPermissionsViewController.swift new file mode 100644 index 000000000..4a349bb2b --- /dev/null +++ b/Signal/src/ViewControllers/Registration/OnboardingPermissionsViewController.swift @@ -0,0 +1,180 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import UIKit +import PromiseKit + +@objc +public class MockOnboardingController: NSObject, OnboardingController { + public func onboardingPermissionsWasSkipped(viewController: UIViewController) {} + + public func onboardingPermissionsDidComplete(viewController: UIViewController) {} +} + +// MARK: - + +@objc +public class OnboardingPermissionsViewController: OWSViewController { + // Unlike a delegate, the OnboardingController we should retain a strong + // reference to the onboardingController. + private var onboardingController: OnboardingController + + @objc + public init(onboardingController: OnboardingController) { + self.delegate = onboardingController + + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable, message: "use other init() instead.") + required public init?(coder aDecoder: NSCoder) { + notImplemented() + } + + // MARK: - + + override public func loadView() { + super.loadView() + + view.backgroundColor = Theme.backgroundColor + view.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32) + + // TODO: +// navigationItem.title = NSLocalizedString("SETTINGS_BACKUP", comment: "Label for the backup view in app settings.") + + navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("NAVIGATION_ITEM_SKIP_BUTTON", comment: "A button to skip a view."), + style: .plain, + target: self, + action: #selector(skipWasPressed)) + + let titleLabel = UILabel() + titleLabel.text = NSLocalizedString("ONBOARDING_PERMISSIONS_TITLE", comment: "Title of the 'onboarding permissions' view.") + titleLabel.textColor = Theme.primaryColor + titleLabel.font = UIFont.ows_dynamicTypeTitle2.ows_mediumWeight() + titleLabel.numberOfLines = 0 + titleLabel.lineBreakMode = .byWordWrapping + titleLabel.textAlignment = .center + view.addSubview(titleLabel) + titleLabel.autoPinWidthToSuperviewMargins() + titleLabel.autoPinEdge(toSuperviewMargin: .top) + + let explainerLabel = UILabel() + // TODO: Finalize copy. + explainerLabel.text = NSLocalizedString("ONBOARDING_PERMISSIONS_EXPLANATION", comment: "Explanation in the 'onboarding permissions' view.") + explainerLabel.textColor = Theme.secondaryColor + explainerLabel.font = UIFont.ows_dynamicTypeCaption1 + explainerLabel.numberOfLines = 0 + explainerLabel.textAlignment = .center + explainerLabel.lineBreakMode = .byWordWrapping + explainerLabel.isUserInteractionEnabled = true + explainerLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(explainerLabelTapped))) + + // TODO: Make sure this all fits if dynamic font sizes are maxed out. + let buttonHeight: CGFloat = 48 + let giveAccessButton = OWSFlatButton.button(title: NSLocalizedString("ONBOARDING_PERMISSIONS_GIVE_ACCESS_BUTTON", + comment: "Label for the 'give access' button in the 'onboarding permissions' view."), + font: OWSFlatButton.fontForHeight(buttonHeight), + titleColor: .white, + backgroundColor: .ows_materialBlue, + target: self, + selector: #selector(giveAccessPressed)) + giveAccessButton.autoSetDimension(.height, toSize: buttonHeight) + + let notNowButton = OWSFlatButton.button(title: NSLocalizedString("ONBOARDING_PERMISSIONS_GIVE_ACCESS_BUTTON", + comment: "Label for the 'give access' button in the 'onboarding permissions' view."), + font: OWSFlatButton.fontForHeight(buttonHeight), + titleColor: .white, + backgroundColor: .ows_materialBlue, + target: self, + selector: #selector(notNowPressed)) + notNowButton.autoSetDimension(.height, toSize: buttonHeight) + + let buttonStack = UIStackView(arrangedSubviews: [ + giveAccessButton, + notNowButton + ]) + buttonStack.axis = .vertical + buttonStack.alignment = .fill + buttonStack.spacing = 12 + + let stackView = UIStackView(arrangedSubviews: [ + explainerLabel, + buttonStack + ]) + stackView.axis = .vertical + stackView.alignment = .fill + stackView.spacing = 40 + view.addSubview(stackView) + stackView.autoPinWidthToSuperviewMargins() + stackView.autoVCenterInSuperview() + NSLayoutConstraint.autoSetPriority(.defaultHigh) { + stackView.autoPinEdge(.top, to: .bottom, of: titleLabel, withOffset: 20, relation: .greaterThanOrEqual) + } + } + + // MARK: Request Access + + private func requestAccess() { + Logger.info("") + + // TODO: We need to defer app's request notification permissions until onboarding is complete. + requestContactsAccess().then { _ in + return PushRegistrationManager.shared.registerUserNotificationSettings() + }.done { [weak self] in + guard let self = self else { + return + } + self.onboardingController.onboardingPermissionsDidComplete(viewController: self) + }.retainUntilComplete() + } + + private func requestContactsAccess() -> Promise { + Logger.info("") + + let (promise, resolver) = Promise.pending() + CNContactStore().requestAccess(for: CNEntityType.contacts) { (granted, error) -> Void in + if granted { + Logger.info("Granted.") + } else { + Logger.error("Error: \(String(describing: error)).") + } + // Always fulfill. + resolver.fulfill(()) + } + return promise + } + + // MARK: Orientation + + public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait + } + + // MARK: - Events + + @objc func skipWasPressed() { + Logger.info("") + + onboardingController.onboardingPermissionsWasSkipped(viewController: self) + } + + @objc func explainerLabelTapped(sender: UIGestureRecognizer) { + guard sender.state == .recognized else { + return + } + // TODO: + } + + @objc func giveAccessPressed() { + Logger.info("") + + requestAccess() + } + + @objc func notNowPressed() { + Logger.info("") + + delegate?.onboardingPermissionsWasSkipped(viewController: self) + } +} diff --git a/Signal/src/environment/PushRegistrationManager.swift b/Signal/src/environment/PushRegistrationManager.swift index 44ab9cb21..acadafecd 100644 --- a/Signal/src/environment/PushRegistrationManager.swift +++ b/Signal/src/environment/PushRegistrationManager.swift @@ -127,7 +127,7 @@ public enum PushRegistrationError: Error { // User notification settings must be registered *before* AppDelegate will // return any requested push tokens. - private func registerUserNotificationSettings() -> Promise { + public func registerUserNotificationSettings() -> Promise { AssertIsOnMainThread() Logger.info("registering user notification settings") return notificationPresenter.registerNotificationSettings() diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index 2a7f63224..0c47facd0 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -26,6 +26,7 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); // Pins the width of this view to the width of its superview, with uniform margins. - (NSArray *)autoPinWidthToSuperviewWithMargin:(CGFloat)margin; - (NSArray *)autoPinWidthToSuperview; +- (NSArray *)autoPinWidthToSuperviewMargins; // Pins the height of this view to the height of its superview, with uniform margins. - (NSArray *)autoPinHeightToSuperviewWithMargin:(CGFloat)margin; - (NSArray *)autoPinHeightToSuperview; diff --git a/SignalMessaging/categories/UIView+OWS.m b/SignalMessaging/categories/UIView+OWS.m index babbd3f9f..6eb7e98cf 100644 --- a/SignalMessaging/categories/UIView+OWS.m +++ b/SignalMessaging/categories/UIView+OWS.m @@ -44,6 +44,15 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) return result; } +- (NSArray *)autoPinWidthToSuperviewMargins +{ + NSArray *result = @[ + [self autoPinEdgeToSuperviewMargin:ALEdgeLeading], + [self autoPinEdgeToSuperviewMargin:ALEdgeTrailing], + ]; + return result; +} + - (NSArray *)autoPinWidthToSuperview { NSArray *result = @[