Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/RecipientPickers/InviteFlow.swift
1 views
//
// 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
        case 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 weak 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) { _ 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) {
        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
                ObjectRetainer.retainObject(self, forLifetimeOf: picker)
                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: { _ 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
        ObjectRetainer.retainObject(self, forLifetimeOf: messageComposeViewController)
        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
        ObjectRetainer.retainObject(self, forLifetimeOf: mailComposeViewController)
        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.nonPluralLocalizedStringWithFormat(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)")
            }
        }
    }
}