Split it based on whether the purpose is “editing” or “sharing”. If we’re editing, it generally means we have full access to contacts, and that we’ve checked which of the contacts are registered on Signal. If we’re “sharing”, we don’t care whether or not they’re registered. If we’re “sharing”, we check whether or not we’ve prompted for the contacts permission yet. If not, we’ll show the prompt. If we’re “editing”, we assume we’ve already prompted (which is already the case today), so `.notDetermined` isn’t a possible state. Other things to note: - The `supportsContactEditing` was always true, so it’s been removed. - The error when contacts access isn’t allowed is now shown more consistently. Previously, it was sometimes shown in response to initializing a view controller. - Some RecipientPickerViewController code was moved from Obj-C to Swift. This is probably useful on its own, but it’ll also make it easier to build some new UI in subsequent commits.
272 lines
11 KiB
Swift
272 lines
11 KiB
Swift
//
|
|
// Copyright 2016 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import ContactsUI
|
|
import Foundation
|
|
import MessageUI
|
|
import SignalMessaging
|
|
import SignalServiceKit
|
|
|
|
@objc(OWSInviteFlow)
|
|
public class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailComposeViewControllerDelegate, ContactsPickerDelegate {
|
|
private enum Channel {
|
|
case message, mail
|
|
|
|
var actionTitle: String {
|
|
switch self {
|
|
case .message:
|
|
return OWSLocalizedString("SHARE_ACTION_MESSAGE", comment: "action sheet item to open native messages app")
|
|
case .mail:
|
|
return OWSLocalizedString("SHARE_ACTION_MAIL", comment: "action sheet item to open native mail app")
|
|
}
|
|
}
|
|
|
|
var cellSubtitleType: SubtitleCellValue {
|
|
switch self {
|
|
case .message:
|
|
return .phoneNumber
|
|
case .mail:
|
|
return .email
|
|
}
|
|
}
|
|
}
|
|
|
|
private let installUrl = "https://signal.org/install/"
|
|
private let homepageUrl = "https://signal.org"
|
|
|
|
private weak var presentingViewController: UIViewController?
|
|
private var modalPresentationViewController: UIViewController?
|
|
|
|
private var channel: Channel?
|
|
|
|
@objc
|
|
public required init(presentingViewController: UIViewController) {
|
|
self.presentingViewController = presentingViewController
|
|
|
|
super.init()
|
|
}
|
|
|
|
deinit {
|
|
Logger.verbose("deinit")
|
|
}
|
|
|
|
// MARK: - Localized Strings
|
|
|
|
public static var unsupportedFeatureMessage: String {
|
|
OWSLocalizedString(
|
|
"UNSUPPORTED_FEATURE_ERROR",
|
|
comment: "When inviting contacts to use Signal, this error is shown if the device doesn't support SMS or if there aren't any registered email accounts."
|
|
)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public func present(isAnimated: Bool, completion: (() -> Void)?) {
|
|
let channels = [messageChannel(), mailChannel()].compacted()
|
|
if channels.count > 1 {
|
|
let actionSheetController = ActionSheetController(title: nil, message: nil)
|
|
actionSheetController.addAction(OWSActionSheets.dismissAction)
|
|
for channel in channels {
|
|
actionSheetController.addAction(ActionSheetAction(title: channel.actionTitle, style: .default) { [weak self] _ in
|
|
self?.presentInviteFlow(channel: channel)
|
|
})
|
|
}
|
|
presentingViewController?.present(actionSheetController, animated: isAnimated, completion: completion)
|
|
} else if let supportedChannel = channels.first {
|
|
presentInviteFlow(channel: supportedChannel)
|
|
}
|
|
}
|
|
|
|
func presentViewController(_ vc: UIViewController, animated: Bool, completion: (() -> Void)? = nil) {
|
|
let navController = OWSNavigationController(rootViewController: vc)
|
|
presentingViewController?.presentFormSheet(navController, animated: true)
|
|
modalPresentationViewController = navController
|
|
}
|
|
|
|
func popToPresentingViewController(animated: Bool, completion: (() -> Void)? = nil) {
|
|
guard let modalVC = modalPresentationViewController else {
|
|
owsFailDebug("Missing modal view controller")
|
|
return
|
|
}
|
|
modalVC.dismiss(animated: true, completion: completion)
|
|
self.modalPresentationViewController = nil
|
|
}
|
|
|
|
// MARK: ContactsPickerDelegate
|
|
|
|
public func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
|
|
Logger.debug("didSelectContacts:\(contacts)")
|
|
|
|
guard let inviteChannel = channel else {
|
|
Logger.error("unexpected nil channel after returning from contact picker.")
|
|
popToPresentingViewController(animated: true)
|
|
return
|
|
}
|
|
|
|
switch inviteChannel {
|
|
case .message:
|
|
let phoneNumbers: [String] = contacts.map { $0.userTextPhoneNumbers.first }.filter { $0 != nil }.map { $0! }
|
|
dismissAndSendSMSTo(phoneNumbers: phoneNumbers)
|
|
case .mail:
|
|
let recipients: [String] = contacts.map { $0.emails.first }.filter { $0 != nil }.map { $0! }
|
|
sendMailTo(emails: recipients)
|
|
}
|
|
}
|
|
|
|
public func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool {
|
|
guard let inviteChannel = channel else {
|
|
Logger.error("unexpected nil channel in contact picker.")
|
|
return true
|
|
}
|
|
|
|
switch inviteChannel {
|
|
case .message:
|
|
return contact.userTextPhoneNumbers.count > 0
|
|
case .mail:
|
|
return contact.emails.count > 0
|
|
}
|
|
}
|
|
|
|
public func contactsPickerDidCancel(_: ContactsPicker) {
|
|
Logger.debug("")
|
|
popToPresentingViewController(animated: true)
|
|
}
|
|
|
|
public func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
|
|
owsFailDebug("InviteFlow only supports multi-select")
|
|
popToPresentingViewController(animated: true)
|
|
}
|
|
|
|
// MARK: SMS
|
|
|
|
private func messageChannel() -> Channel? {
|
|
guard MFMessageComposeViewController.canSendText() else {
|
|
Logger.info("Device cannot send text")
|
|
return nil
|
|
}
|
|
return .message
|
|
}
|
|
|
|
private func presentInviteFlow(channel: Channel) {
|
|
guard let presentingViewController else { return }
|
|
|
|
self.channel = channel
|
|
contactsViewHelper.checkSharingAuthorization(
|
|
purpose: .invite,
|
|
authorizedBehavior: .runAction({
|
|
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: channel.cellSubtitleType)
|
|
picker.contactsPickerDelegate = self
|
|
picker.title = OWSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
|
self.presentViewController(picker, animated: true)
|
|
}),
|
|
unauthorizedBehavior: .presentError(from: presentingViewController)
|
|
)
|
|
}
|
|
|
|
public func dismissAndSendSMSTo(phoneNumbers: [String]) {
|
|
popToPresentingViewController(animated: true) {
|
|
if phoneNumbers.count > 1 {
|
|
let warning = ActionSheetController(
|
|
title: nil,
|
|
message: OWSLocalizedString(
|
|
"INVITE_WARNING_MULTIPLE_INVITES_BY_TEXT",
|
|
comment: "Alert warning that sending an invite to multiple users will create a group message whose recipients will be able to see each other."
|
|
)
|
|
)
|
|
warning.addAction(ActionSheetAction(
|
|
title: CommonStrings.continueButton,
|
|
style: .default,
|
|
handler: { [weak self] _ in
|
|
self?.sendSMSTo(phoneNumbers: phoneNumbers)
|
|
}
|
|
))
|
|
warning.addAction(OWSActionSheets.cancelAction)
|
|
|
|
self.presentingViewController?.presentActionSheet(warning)
|
|
} else {
|
|
self.sendSMSTo(phoneNumbers: phoneNumbers)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func sendSMSTo(phoneNumbers: [String]) {
|
|
let messageComposeViewController = MFMessageComposeViewController()
|
|
messageComposeViewController.messageComposeDelegate = self
|
|
messageComposeViewController.recipients = phoneNumbers
|
|
|
|
let inviteText = OWSLocalizedString("SMS_INVITE_BODY", comment: "body sent to contacts when inviting to Install Signal")
|
|
messageComposeViewController.body = inviteText.appending(" \(self.installUrl)")
|
|
presentingViewController?.present(messageComposeViewController, animated: true)
|
|
}
|
|
|
|
// MARK: MessageComposeViewControllerDelegate
|
|
|
|
public func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
|
|
presentingViewController?.dismiss(animated: true) {
|
|
switch result {
|
|
case .failed:
|
|
let warning = ActionSheetController(title: nil, message: OWSLocalizedString("SEND_INVITE_FAILURE", comment: "Alert body after invite failed"))
|
|
warning.addAction(OWSActionSheets.dismissAction)
|
|
self.presentingViewController?.present(warning, animated: true, completion: nil)
|
|
case .sent:
|
|
Logger.debug("user successfully invited their friends via SMS.")
|
|
case .cancelled:
|
|
Logger.debug("user cancelled message invite")
|
|
@unknown default:
|
|
owsFailDebug("unknown MessageComposeResult: \(result)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Mail
|
|
|
|
private func mailChannel() -> Channel? {
|
|
guard MFMailComposeViewController.canSendMail() else {
|
|
Logger.info("Device cannot send mail")
|
|
return nil
|
|
}
|
|
return .mail
|
|
}
|
|
|
|
private func sendMailTo(emails recipientEmails: [String]) {
|
|
let mailComposeViewController = MFMailComposeViewController()
|
|
mailComposeViewController.mailComposeDelegate = self
|
|
mailComposeViewController.setBccRecipients(recipientEmails)
|
|
|
|
let subject = OWSLocalizedString("EMAIL_INVITE_SUBJECT", comment: "subject of email sent to contacts when inviting to install Signal")
|
|
let bodyFormat = OWSLocalizedString("EMAIL_INVITE_BODY", comment: "body of email sent to contacts when inviting to install Signal. Embeds {{link to install Signal}} and {{link to the Signal home page}}")
|
|
let body = String.init(format: bodyFormat, installUrl, homepageUrl)
|
|
mailComposeViewController.setSubject(subject)
|
|
mailComposeViewController.setMessageBody(body, isHTML: false)
|
|
|
|
popToPresentingViewController(animated: true) {
|
|
self.presentingViewController?.present(mailComposeViewController, animated: true)
|
|
}
|
|
}
|
|
|
|
// MARK: MailComposeViewControllerDelegate
|
|
|
|
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
|
presentingViewController?.dismiss(animated: true) {
|
|
switch result {
|
|
case .failed:
|
|
let warning = ActionSheetController(title: nil, message: OWSLocalizedString("SEND_INVITE_FAILURE", comment: "Alert body after invite failed"))
|
|
warning.addAction(OWSActionSheets.dismissAction)
|
|
self.presentingViewController?.present(warning, animated: true, completion: nil)
|
|
case .sent:
|
|
Logger.debug("user successfully invited their friends via mail.")
|
|
case .saved:
|
|
Logger.debug("user saved mail invite.")
|
|
case .cancelled:
|
|
Logger.debug("user cancelled mail invite.")
|
|
@unknown default:
|
|
owsFailDebug("unknown MFMailComposeResult: \(result)")
|
|
}
|
|
}
|
|
}
|
|
}
|