TM-SGNL-iOS/SignalUI/RecipientPickers/InviteFlow.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

267 lines
10 KiB
Swift

//
// Copyright 2016 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import ContactsUI
import Foundation
public import MessageUI
public import SignalServiceKit
public class InviteFlow: NSObject {
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?
public init(presentingViewController: UIViewController) {
self.presentingViewController = presentingViewController
super.init()
}
// 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: -
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)
}
}
private func presentViewController(_ vc: UIViewController, animated: Bool, completion: (() -> Void)? = nil) {
let navController = OWSNavigationController(rootViewController: vc)
presentingViewController?.presentFormSheet(navController, animated: true)
modalPresentationViewController = navController
}
private 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: 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
SUIEnvironment.shared.contactsViewHelperRef.checkReadAuthorization(
purpose: .invite,
performWhenAllowed: {
let picker = ContactPickerViewController(allowsMultipleSelection: true, subtitleCellType: channel.cellSubtitleType)
picker.delegate = self
picker.title = OWSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
self.presentViewController(picker, animated: true)
},
presentErrorFrom: presentingViewController
)
}
private 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)
}
}
}
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: 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)
}
}
}
extension InviteFlow: ContactPickerDelegate {
public func contactPicker(_: ContactPickerViewController, didSelectMultiple systemContacts: [SystemContact]) {
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] = systemContacts.map { $0.phoneNumbers.first?.value }.compacted()
dismissAndSendSMSTo(phoneNumbers: phoneNumbers)
case .mail:
let recipients: [String] = systemContacts.map { $0.emailAddresses.first }.compacted()
sendMailTo(emails: recipients)
}
}
public func contactPicker(_: ContactPickerViewController, shouldSelect systemContact: SystemContact) -> Bool {
guard let inviteChannel = channel else {
Logger.error("unexpected nil channel in contact picker.")
return true
}
switch inviteChannel {
case .message:
return systemContact.phoneNumbers.count > 0
case .mail:
return systemContact.emailAddresses.count > 0
}
}
public func contactPickerDidCancel(_: ContactPickerViewController) {
Logger.debug("")
popToPresentingViewController(animated: true)
}
public func contactPicker(_: ContactPickerViewController, didSelect systemContact: SystemContact) {
owsFailDebug("InviteFlow only supports multi-select")
popToPresentingViewController(animated: true)
}
}
extension InviteFlow: MFMessageComposeViewControllerDelegate {
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)")
}
}
}
}
extension InviteFlow: MFMailComposeViewControllerDelegate {
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)")
}
}
}
}