255 lines
9.8 KiB
Swift
255 lines
9.8 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import SignalServiceKit
|
|
import MessageUI
|
|
import PromiseKit
|
|
|
|
struct SupportEmailModel {
|
|
|
|
enum LogPolicy {
|
|
/// Do not upload logs
|
|
case none
|
|
|
|
/// Attempt to upload the logs and include the resulting URL in the email body
|
|
/// If the upload fails for one reason or another, continue anyway
|
|
case attemptUpload
|
|
|
|
/// Upload the logs. If they fail to upload, fail the operation
|
|
case requireUpload
|
|
|
|
/// Don't upload new logs, instead use the provided link
|
|
case link(URL)
|
|
}
|
|
|
|
public static let supportFilterDefault = "Signal iOS Support Request"
|
|
public static let supportFilterPayments = "Signal iOS Support Request - Payments"
|
|
|
|
/// An unlocalized string used for filtering by support
|
|
var supportFilter: String = SupportEmailModel.supportFilterDefault
|
|
|
|
var localizedSubject: String = NSLocalizedString(
|
|
"SUPPORT_EMAIL_SUBJECT",
|
|
comment: "Localized subject for support request emails"
|
|
)
|
|
var deviceType: String = UIDevice.current.model
|
|
var deviceIdentifier: String = String(sysctlKey: "hw.machine")?.replacingOccurrences(of: UIDevice.current.model, with: "") ?? "Unknown"
|
|
var iosVersion: String = AppVersion.iOSVersionString
|
|
var signalVersion: String = AppVersion.shared().currentAppVersionLong
|
|
var locale: String = NSLocale.current.identifier
|
|
|
|
var userDescription: String? = NSLocalizedString(
|
|
"SUPPORT_EMAIL_DEFAULT_DESCRIPTION",
|
|
comment: "Default prompt for user description in support email requests"
|
|
)
|
|
var emojiMood: EmojiMoodPickerView.Mood?
|
|
var debugLogPolicy: LogPolicy = .none
|
|
fileprivate var resolvedDebugString: String?
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
final class ComposeSupportEmailOperation: NSObject {
|
|
|
|
enum EmailError: LocalizedError {
|
|
case logUploadTimedOut
|
|
case logUploadFailure(underlyingError: LocalizedError?)
|
|
case invalidURL
|
|
case failedToOpenURL
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .logUploadTimedOut:
|
|
return NSLocalizedString("ERROR_DESCRIPTION_REQUEST_TIMED_OUT",
|
|
comment: "Error indicating that a socket request timed out.")
|
|
case let .logUploadFailure(underlyingError):
|
|
return underlyingError?.errorDescription ??
|
|
NSLocalizedString("ERROR_DESCRIPTION_LOG_UPLOAD_FAILED",
|
|
comment: "Generic error indicating that log upload failed")
|
|
case .invalidURL:
|
|
return NSLocalizedString("ERROR_DESCRIPTION_INVALID_SUPPORT_EMAIL",
|
|
comment: "Error indicating that a support mailto link could not be created.")
|
|
case .failedToOpenURL:
|
|
return NSLocalizedString("ERROR_DESCRIPTION_COULD_NOT_LAUNCH_EMAIL",
|
|
comment: "Error indicating that openURL for a mailto: URL failed.")
|
|
}
|
|
}
|
|
}
|
|
|
|
static var canSendEmails: Bool {
|
|
return UIApplication.shared.canOpenURL(MailtoLink(to: "", subject: "", body: "").url!)
|
|
}
|
|
|
|
private var model: SupportEmailModel
|
|
private var isCancelled: Bool = false
|
|
|
|
@objc
|
|
class func sendEmailWithDefaultErrorHandling(supportFilter: String, logUrl: URL? = nil) {
|
|
sendEmail(supportFilter: supportFilter, logUrl: logUrl).catch { error in
|
|
OWSActionSheets.showErrorAlert(message: error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
class func sendEmail(supportFilter: String, logUrl: URL? = nil) -> Promise<Void> {
|
|
var model = SupportEmailModel()
|
|
model.supportFilter = supportFilter
|
|
if let logUrl = logUrl { model.debugLogPolicy = .link(logUrl) }
|
|
return sendEmail(model: model)
|
|
}
|
|
|
|
class func sendEmail(model: SupportEmailModel) -> Promise<Void> {
|
|
let operation = ComposeSupportEmailOperation(model: model)
|
|
return operation.perform(on: .sharedUserInitiated)
|
|
}
|
|
|
|
init(model: SupportEmailModel) {
|
|
self.model = model
|
|
super.init()
|
|
}
|
|
|
|
func perform(on workQueue: DispatchQueue = .sharedUtility) -> Promise<Void> {
|
|
guard !isCancelled else {
|
|
// If we're cancelled, return an empty success
|
|
return Promise()
|
|
}
|
|
guard Self.canSendEmails else {
|
|
// If we can't send emails, fail early
|
|
return Promise(error: EmailError.failedToOpenURL)
|
|
}
|
|
|
|
return firstly { () -> Promise<String?> in
|
|
// Returns an appropriate string for the debug logs
|
|
// If we're not uploading, returns nil
|
|
switch model.debugLogPolicy {
|
|
case .none:
|
|
return .value(nil)
|
|
case let .link(url):
|
|
return .value(url.absoluteString)
|
|
case .attemptUpload, .requireUpload:
|
|
return Pastelog.uploadLog().map { $0.absoluteString }
|
|
}
|
|
|
|
}.timeout(seconds: 60, timeoutErrorBlock: { () -> Error in
|
|
// If we haven't finished uploading logs in 10s, give up
|
|
return EmailError.logUploadTimedOut
|
|
|
|
})
|
|
.recover(on: workQueue) { error -> Promise<String?> in
|
|
// Suppress the error unless we're required to provide logs
|
|
if case .requireUpload = self.model.debugLogPolicy {
|
|
let emailError = EmailError.logUploadFailure(underlyingError: (error as? LocalizedError))
|
|
return Promise(error: emailError)
|
|
} else {
|
|
return .value("[Support note: Log upload failed — \(error.localizedDescription)]")
|
|
}
|
|
|
|
}.then(on: workQueue) { (debugURLString) -> Promise<URL> in
|
|
self.model.resolvedDebugString = debugURLString
|
|
|
|
if let url = self.emailURL {
|
|
return .value(url)
|
|
} else {
|
|
return Promise(error: EmailError.invalidURL)
|
|
}
|
|
|
|
}.then(on: .main) { (emailURL: URL) -> Promise<Void> in
|
|
(self.isCancelled == false) ? self.open(mailURL: emailURL) : Promise()
|
|
}
|
|
}
|
|
|
|
/// If invoked before the operation completes, will prevent the operation from opening email
|
|
/// Must be called from main queue. Note: This doesn't really *stop* the operation so much as
|
|
/// render it invisible to the user.
|
|
func cancel() {
|
|
AssertIsOnMainThread()
|
|
isCancelled = true
|
|
}
|
|
|
|
private func open(mailURL url: URL) -> Promise<Void> {
|
|
Promise { (resolver) in
|
|
UIApplication.shared.open(url, options: [:]) { (success) in
|
|
if success {
|
|
resolver.fulfill_()
|
|
} else {
|
|
resolver.reject(EmailError.failedToOpenURL)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var emailURL: URL? {
|
|
let linkBuilder = MailtoLink(to: SupportConstants.supportEmail,
|
|
subject: model.localizedSubject,
|
|
body: emailBody)
|
|
return linkBuilder.url
|
|
}
|
|
|
|
private var emailBody: String {
|
|
|
|
// Items in this array will be separated by newlines
|
|
// Return nil to omit the item
|
|
let bodyComponents: [String?] = [
|
|
model.userDescription,
|
|
"",
|
|
NSLocalizedString(
|
|
"SUPPORT_EMAIL_INFO_DIVIDER",
|
|
comment: "Localized divider for support request emails internal information"
|
|
),
|
|
String(
|
|
format: NSLocalizedString(
|
|
"SUPPORT_EMAIL_FILTER_LABEL_FORMAT",
|
|
comment: "Localized label for support request email filter string. Embeds {{filter text}}."
|
|
), model.supportFilter
|
|
),
|
|
String(
|
|
format: NSLocalizedString(
|
|
"SUPPORT_EMAIL_HARDWARE_LABEL_FORMAT",
|
|
comment: "Localized label for support request email hardware string (e.g. iPhone or iPad). Embeds {{hardware text}}."
|
|
), model.deviceType
|
|
),
|
|
String(
|
|
format: NSLocalizedString(
|
|
"SUPPORT_EMAIL_HID_LABEL_FORMAT",
|
|
comment: "Localized label for support request email HID string (e.g. 12,1). Embeds {{hid text}}."
|
|
), model.deviceIdentifier
|
|
),
|
|
String(
|
|
format: NSLocalizedString(
|
|
"SUPPORT_EMAIL_IOS_VERSION_LABEL_FORMAT",
|
|
comment: "Localized label for support request email iOS Version string (e.g. 13.4). Embeds {{ios version}}."
|
|
), model.iosVersion
|
|
),
|
|
String(
|
|
format: NSLocalizedString(
|
|
"SUPPORT_EMAIL_SIGNAL_VERSION_LABEL_FORMAT",
|
|
comment: "Localized label for support request email signal version string. Embeds {{signal version}}."
|
|
), model.signalVersion
|
|
), {
|
|
if let debugURLString = model.resolvedDebugString {
|
|
return String(
|
|
format: NSLocalizedString(
|
|
"SUPPORT_EMAIL_LOG_URL_LABEL_FORMAT",
|
|
comment: "Localized label for support request email debug log URL. Embeds {{debug log url}}."
|
|
), debugURLString
|
|
)
|
|
} else { return nil }
|
|
}(),
|
|
String(
|
|
format: NSLocalizedString(
|
|
"SUPPORT_EMAIL_LOCALE_LABEL_FORMAT",
|
|
comment: "Localized label for support request email locale string. Embeds {{locale}}."
|
|
), model.locale
|
|
),
|
|
"",
|
|
model.emojiMood?.stringRepresentation,
|
|
model.emojiMood?.emojiRepresentation
|
|
]
|
|
|
|
return bodyComponents
|
|
.compactMap { $0 }
|
|
.joined(separator: "\r\n")
|
|
}
|
|
}
|