summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/View controllers/ProblemReport
diff options
context:
space:
mode:
authorJon Petersson <jon.petersson@mullvad.net>2025-04-25 08:11:02 +0200
committerJon Petersson <jon.petersson@mullvad.net>2025-05-30 11:32:54 +0200
commit83effc10b5cb3f26ca1c65d30ae62baf10f495d3 (patch)
tree59b470da0d74cf1311ccd53787df47ccdca854d4 /ios/MullvadVPN/View controllers/ProblemReport
parent42b53b4bcd3d3155d42ebca7e33c9450ca5dab73 (diff)
downloadmullvadvpn-83effc10b5cb3f26ca1c65d30ae62baf10f495d3.tar.xz
mullvadvpn-83effc10b5cb3f26ca1c65d30ae62baf10f495d3.zip
Let users cancel sending a problem report
Diffstat (limited to 'ios/MullvadVPN/View controllers/ProblemReport')
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift37
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift22
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportSubmissionOverlayView.swift166
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController+ViewManagement.swift18
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift20
-rw-r--r--ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewModel.swift18
6 files changed, 203 insertions, 78 deletions
diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift
index 98746416dc..400db47dd1 100644
--- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift
@@ -16,6 +16,7 @@ final class ProblemReportInteractor: @unchecked Sendable {
private let tunnelManager: TunnelManager
private let consolidatedLog: ConsolidatedApplicationLog
private var reportedString = ""
+ private var requestCancellable: Cancellable?
init(apiProxy: APIQuerying, tunnelManager: TunnelManager) {
self.apiProxy = apiProxy
@@ -28,13 +29,10 @@ final class ProblemReportInteractor: @unchecked Sendable {
)
}
- func fetchReportString(completion: @escaping @Sendable (String) -> Void) {
+ func fetchReportString(completion: @escaping @Sendable (Result<String, Error>) -> Void) {
consolidatedLog.addLogFiles(fileURLs: ApplicationTarget.allCases.flatMap {
ApplicationConfiguration.logFileURLs(for: $0, in: ApplicationConfiguration.containerURL)
- }) { [weak self] in
- guard let self else { return }
- completion(consolidatedLog.string)
- }
+ }, completion: completion)
}
func sendReport(
@@ -43,15 +41,19 @@ final class ProblemReportInteractor: @unchecked Sendable {
completion: @escaping @Sendable (Result<Void, Error>) -> Void
) {
let logString = self.consolidatedLog.string
-
if logString.isEmpty {
- fetchReportString { [weak self] updatedLogString in
- self?.sendProblemReport(
- email: email,
- message: message,
- logString: updatedLogString,
- completion: completion
- )
+ fetchReportString { [weak self] result in
+ switch result {
+ case let .success(logString):
+ self?.sendProblemReport(
+ email: email,
+ message: message,
+ logString: logString,
+ completion: completion
+ )
+ case let .failure(error):
+ completion(.failure(error))
+ }
}
} else {
sendProblemReport(
@@ -63,6 +65,11 @@ final class ProblemReportInteractor: @unchecked Sendable {
}
}
+ func cancelSendingReport() {
+ consolidatedLog.cancel()
+ requestCancellable?.cancel()
+ }
+
private func sendProblemReport(
email: String,
message: String,
@@ -80,10 +87,10 @@ final class ProblemReportInteractor: @unchecked Sendable {
metadata: metadataDict
)
- _ = self.apiProxy.sendProblemReport(request, retryStrategy: .default, completionHandler: { result in
+ requestCancellable = self.apiProxy.sendProblemReport(request, retryStrategy: .default) { result in
DispatchQueue.main.async {
completion(result)
}
- })
+ }
}
}
diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift
index 01d7afb09c..3ba1ca5f58 100644
--- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift
@@ -95,20 +95,28 @@ class ProblemReportReviewViewController: UIViewController {
private func loadLogs() {
spinnerView.startAnimating()
- interactor.fetchReportString { [weak self] reportString in
+ interactor.fetchReportString { [weak self] result in
guard let self else { return }
- Task { @MainActor in
- textView.text = reportString
- spinnerView.stopAnimating()
- spinnerContainerView.isHidden = true
+
+ if case let .success(reportString) = result {
+ Task { @MainActor in
+ textView.text = reportString
+ spinnerView.stopAnimating()
+ spinnerContainerView.isHidden = true
+ }
}
}
}
#if DEBUG
private func share() {
- interactor.fetchReportString { [weak self] reportString in
- guard let self,!reportString.isEmpty else { return }
+ interactor.fetchReportString { [weak self] result in
+ guard
+ let self,
+ case let .success(reportString) = result,
+ !reportString.isEmpty
+ else { return }
+
Task { @MainActor in
let activityController = UIActivityViewController(
activityItems: [reportString],
diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportSubmissionOverlayView.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportSubmissionOverlayView.swift
index e4b68a4cea..0d7148380f 100644
--- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportSubmissionOverlayView.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportSubmissionOverlayView.swift
@@ -11,6 +11,8 @@ import MullvadREST
import UIKit
class ProblemReportSubmissionOverlayView: UIView {
+ var viewLogsButtonAction: (() -> Void)?
+ var cancelButtonAction: (() -> Void)?
var editButtonAction: (() -> Void)?
var retryButtonAction: (() -> Void)?
@@ -19,24 +21,28 @@ class ProblemReportSubmissionOverlayView: UIView {
case sent(_ email: String)
case failure(Error)
+ var supportEmail: String {
+ "support@mullvadvpn.net"
+ }
+
var title: String? {
switch self {
case .sending:
- return NSLocalizedString(
+ NSLocalizedString(
"SUBMISSION_STATUS_SENDING",
tableName: "ProblemReport",
value: "Sending...",
comment: ""
)
case .sent:
- return NSLocalizedString(
+ NSLocalizedString(
"SUBMISSION_STATUS_SENT",
tableName: "ProblemReport",
value: "Sent",
comment: ""
)
case .failure:
- return NSLocalizedString(
+ NSLocalizedString(
"SUBMISSION_STATUS_FAILURE",
tableName: "ProblemReport",
value: "Failed to send",
@@ -45,7 +51,7 @@ class ProblemReportSubmissionOverlayView: UIView {
}
}
- var body: NSAttributedString? {
+ var body: [NSAttributedString]? {
switch self {
case .sending:
return nil
@@ -93,14 +99,44 @@ class ProblemReportSubmissionOverlayView: UIView {
combinedAttributedString.append(emailAttributedString)
}
- return combinedAttributedString
+ return [combinedAttributedString]
- case let .failure(error):
- if let error = error as? REST.Error {
- return error.displayErrorDescription.flatMap { NSAttributedString(string: $0) }
- } else {
- return NSAttributedString(string: error.localizedDescription)
- }
+ case .failure:
+ return [
+ NSAttributedString(
+ string: NSLocalizedString(
+ "MESSAGE_FAILED_PART_1",
+ tableName: "ProblemReport",
+ value:
+ """
+ If you exit the form and try again later, the information you already entered will still \
+ be here.
+ """,
+ comment: ""
+ )
+ ),
+ NSAttributedString(
+ markdownString: NSLocalizedString(
+ "MESSAGE_FAILED_PART_2",
+ tableName: "ProblemReport",
+ value:
+ """
+ If you still experience issues you can email our support directly at \
+ **\(supportEmail)**. Please attach your app log to your email.
+ """,
+ comment: ""
+ ),
+ options: MarkdownStylingOptions(
+ font: .preferredFont(forTextStyle: .body)
+ ), applyEffect: { _, _ in
+ [
+ // Setting font again to circumvent bold weight.
+ .font: UIFont.preferredFont(forTextStyle: .body),
+ .foregroundColor: UIColor.white,
+ ]
+ }
+ ),
+ ]
}
}
}
@@ -127,24 +163,54 @@ class ProblemReportSubmissionOverlayView: UIView {
return textLabel
}()
- let bodyLabel: UILabel = {
- let textLabel = UILabel()
- textLabel.font = UIFont.systemFont(ofSize: 17)
- textLabel.textColor = .white
- textLabel.numberOfLines = 0
- return textLabel
+ let bodyLabelContainer: UIStackView = {
+ let stackView = UIStackView()
+ stackView.translatesAutoresizingMaskIntoConstraints = false
+ stackView.axis = .vertical
+ stackView.spacing = 24
+ return stackView
}()
- /// Footer stack view that contains action buttons
- private lazy var buttonsStackView: UIStackView = {
- let stackView = UIStackView(arrangedSubviews: [self.editMessageButton, self.tryAgainButton])
+ /// Footer stack view that contains action buttons.
+ private lazy var buttonContainer: UIStackView = {
+ let stackView = UIStackView(arrangedSubviews: [cancelButton, failedToSendButtons])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 18
+ return stackView
+ }()
+ /// Footer stack view that contains action buttons when sending failed.
+ private lazy var failedToSendButtons: UIStackView = {
+ let stackView = UIStackView(arrangedSubviews: [editMessageButton, viewLogsButton, tryAgainButton])
+ stackView.translatesAutoresizingMaskIntoConstraints = false
+ stackView.axis = .vertical
+ stackView.spacing = 18
return stackView
}()
+ private lazy var viewLogsButton: AppButton = {
+ let button = AppButton(style: .default)
+ button.setAccessibilityIdentifier(.problemReportAppLogsButton)
+ button.translatesAutoresizingMaskIntoConstraints = false
+ button.setTitle(ProblemReportViewModel.viewLogsButtonTitle, for: .normal)
+ button.addTarget(self, action: #selector(handleViewLogsButton), for: .touchUpInside)
+ return button
+ }()
+
+ private lazy var cancelButton: AppButton = {
+ let button = AppButton(style: .default)
+ button.translatesAutoresizingMaskIntoConstraints = false
+ button.setTitle(NSLocalizedString(
+ "CANCEL_BUTTON",
+ tableName: "ProblemReport",
+ value: "Cancel",
+ comment: ""
+ ), for: .normal)
+ button.addTarget(self, action: #selector(handleCancelButton), for: .touchUpInside)
+ return button
+ }()
+
private lazy var editMessageButton: AppButton = {
let button = AppButton(style: .default)
button.translatesAutoresizingMaskIntoConstraints = false
@@ -189,10 +255,10 @@ class ProblemReportSubmissionOverlayView: UIView {
private func addSubviews() {
for subview in [
titleLabel,
- bodyLabel,
+ bodyLabelContainer,
activityIndicator,
statusImageView,
- buttonsStackView,
+ buttonContainer,
] {
subview.translatesAutoresizingMaskIntoConstraints = false
addSubview(subview)
@@ -212,49 +278,81 @@ class ProblemReportSubmissionOverlayView: UIView {
titleLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
- bodyLabel.topAnchor.constraint(
+ bodyLabelContainer.topAnchor.constraint(
equalToSystemSpacingBelow: titleLabel.bottomAnchor,
multiplier: 1
),
- bodyLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
- bodyLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
- buttonsStackView.topAnchor.constraint(
- greaterThanOrEqualTo: bodyLabel.bottomAnchor,
+ bodyLabelContainer.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
+ bodyLabelContainer.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
+ buttonContainer.topAnchor.constraint(
+ greaterThanOrEqualTo: bodyLabelContainer.bottomAnchor,
constant: 18
),
- buttonsStackView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
- buttonsStackView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
- buttonsStackView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
+ buttonContainer.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
+ buttonContainer.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
+ buttonContainer.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
])
}
private func transitionToState(_ state: State) {
titleLabel.text = state.title
- bodyLabel.attributedText = state.body
+
+ bodyLabelContainer.subviews.forEach { $0.removeFromSuperview() }
+ state.body?.forEach { attributedString in
+ let textLabel = UILabel()
+ textLabel.font = UIFont.systemFont(ofSize: 17)
+ textLabel.textColor = .white.withAlphaComponent(0.6)
+ textLabel.numberOfLines = 0
+ textLabel.attributedText = attributedString
+
+ if attributedString.string.contains(state.supportEmail) {
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleEmailLabelTap))
+ textLabel.addGestureRecognizer(tapGesture)
+ textLabel.isUserInteractionEnabled = true
+ }
+
+ bodyLabelContainer.addArrangedSubview(textLabel)
+ }
switch state {
case .sending:
activityIndicator.startAnimating()
statusImageView.isHidden = true
- buttonsStackView.isHidden = true
+ cancelButton.isHidden = false
+ failedToSendButtons.isHidden = true
case .sent:
activityIndicator.stopAnimating()
statusImageView.style = .success
statusImageView.isHidden = false
- buttonsStackView.isHidden = true
+ buttonContainer.isHidden = true
case .failure:
activityIndicator.stopAnimating()
statusImageView.style = .failure
statusImageView.isHidden = false
- buttonsStackView.isHidden = false
+ cancelButton.isHidden = true
+ failedToSendButtons.isHidden = false
}
}
// MARK: - Actions
+ @objc private func handleEmailLabelTap() {
+ if let url = URL(string: "mailto:\(state.supportEmail)") {
+ UIApplication.shared.open(url)
+ }
+ }
+
+ @objc private func handleViewLogsButton() {
+ viewLogsButtonAction?()
+ }
+
+ @objc private func handleCancelButton() {
+ cancelButtonAction?()
+ }
+
@objc private func handleEditButton() {
editButtonAction?()
}
diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController+ViewManagement.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController+ViewManagement.swift
index 484c60ef8f..c5370188eb 100644
--- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController+ViewManagement.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController+ViewManagement.swift
@@ -30,7 +30,7 @@ extension ProblemReportViewController {
textLabel.translatesAutoresizingMaskIntoConstraints = false
textLabel.numberOfLines = 0
textLabel.textColor = .white
- textLabel.text = Self.persistentViewModel.subheadLabelText
+ textLabel.text = ProblemReportViewModel.subheadLabelText
return textLabel
}
@@ -48,7 +48,7 @@ extension ProblemReportViewController {
textField.backgroundColor = .white
textField.inputAccessoryView = emailAccessoryToolbar
textField.font = UIFont.systemFont(ofSize: 17)
- textField.placeholder = Self.persistentViewModel.emailPlaceholderText
+ textField.placeholder = ProblemReportViewModel.emailPlaceholderText
return textField
}
@@ -58,7 +58,7 @@ extension ProblemReportViewController {
textView.backgroundColor = .white
textView.inputAccessoryView = messageAccessoryToolbar
textView.font = UIFont.systemFont(ofSize: 17)
- textView.placeholder = Self.persistentViewModel.messageTextViewPlaceholder
+ textView.placeholder = ProblemReportViewModel.messageTextViewPlaceholder
textView.contentInsetAdjustmentBehavior = .never
return textView
@@ -90,7 +90,7 @@ extension ProblemReportViewController {
let button = AppButton(style: .default)
button.setAccessibilityIdentifier(.problemReportAppLogsButton)
button.translatesAutoresizingMaskIntoConstraints = false
- button.setTitle(Self.persistentViewModel.viewLogsButtonTitle, for: .normal)
+ button.setTitle(ProblemReportViewModel.viewLogsButtonTitle, for: .normal)
button.addTarget(self, action: #selector(handleViewLogsButtonTap), for: .touchUpInside)
return button
}
@@ -99,7 +99,7 @@ extension ProblemReportViewController {
let button = AppButton(style: .success)
button.setAccessibilityIdentifier(.problemReportSendButton)
button.translatesAutoresizingMaskIntoConstraints = false
- button.setTitle(Self.persistentViewModel.sendLogsButtonTitle, for: .normal)
+ button.setTitle(ProblemReportViewModel.sendLogsButtonTitle, for: .normal)
button.addTarget(self, action: #selector(handleSendButtonTap), for: .touchUpInside)
return button
}
@@ -108,6 +108,14 @@ extension ProblemReportViewController {
let overlay = ProblemReportSubmissionOverlayView()
overlay.translatesAutoresizingMaskIntoConstraints = false
+ overlay.viewLogsButtonAction = { [weak self] in
+ self?.handleViewLogsButtonTap()
+ }
+
+ overlay.cancelButtonAction = { [weak self] in
+ self?.interactor.cancelSendingReport()
+ }
+
overlay.editButtonAction = { [weak self] in
self?.hideSubmissionOverlay()
}
diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift
index 0e7cda5f44..e1b37598cb 100644
--- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift
@@ -12,8 +12,8 @@ import Operations
import UIKit
final class ProblemReportViewController: UIViewController, UITextFieldDelegate {
- private let interactor: ProblemReportInteractor
private let alertPresenter: AlertPresenter
+ let interactor: ProblemReportInteractor
var textViewKeyboardResponder: AutomaticKeyboardResponder?
var scrollViewKeyboardResponder: AutomaticKeyboardResponder?
@@ -77,7 +77,7 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate {
view.backgroundColor = .secondaryColor
view.setAccessibilityIdentifier(.problemReportView)
- navigationItem.title = Self.persistentViewModel.navigationTitle
+ navigationItem.title = ProblemReportViewModel.navigationTitle
textViewKeyboardResponder = AutomaticKeyboardResponder(targetView: messageTextView)
scrollViewKeyboardResponder = AutomaticKeyboardResponder(targetView: scrollView)
@@ -170,17 +170,17 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate {
let presentation = AlertPresentation(
id: "problem-report-alert",
icon: .alert,
- message: Self.persistentViewModel.emptyEmailAlertWarning,
+ message: ProblemReportViewModel.emptyEmailAlertWarning,
buttons: [
AlertAction(
- title: Self.persistentViewModel.confirmEmptyEmailTitle,
+ title: ProblemReportViewModel.confirmEmptyEmailTitle,
style: .destructive,
handler: {
completion(true)
}
),
AlertAction(
- title: Self.persistentViewModel.cancelEmptyEmailTitle,
+ title: ProblemReportViewModel.cancelEmptyEmailTitle,
style: .default,
handler: {
completion(false)
@@ -245,7 +245,11 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate {
clearPersistentViewModel()
case let .failure(error):
- submissionOverlayView.state = .failure(error)
+ if let error = error as? OperationError, error == .cancelled {
+ hideSubmissionOverlay()
+ } else {
+ submissionOverlayView.state = .failure(error)
+ }
}
navigationItem.setHidesBackButton(false, animated: true)
@@ -261,9 +265,9 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate {
interactor.sendReport(
email: viewModel.email,
message: viewModel.message
- ) { completion in
+ ) { [weak self] completion in
Task { @MainActor in
- self.didSendProblemReport(viewModel: viewModel, completion: completion)
+ self?.didSendProblemReport(viewModel: viewModel, completion: completion)
}
}
}
diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewModel.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewModel.swift
index 6805b97b6c..e6537ec46b 100644
--- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewModel.swift
+++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewModel.swift
@@ -12,14 +12,14 @@ struct ProblemReportViewModel {
let email: String
let message: String
- let navigationTitle = NSLocalizedString(
+ static let navigationTitle = NSLocalizedString(
"NAVIGATION_TITLE",
tableName: "ProblemReport",
value: "Report a problem",
comment: ""
)
- let subheadLabelText = NSLocalizedString(
+ static let subheadLabelText = NSLocalizedString(
"SUBHEAD_LABEL",
tableName: "ProblemReport",
value: """
@@ -30,14 +30,14 @@ struct ProblemReportViewModel {
comment: ""
)
- let emailPlaceholderText = NSLocalizedString(
+ static let emailPlaceholderText = NSLocalizedString(
"EMAIL_TEXTFIELD_PLACEHOLDER",
tableName: "ProblemReport",
value: "Your email (optional)",
comment: ""
)
- let messageTextViewPlaceholder = NSLocalizedString(
+ static let messageTextViewPlaceholder = NSLocalizedString(
"DESCRIPTION_TEXTVIEW_PLACEHOLDER",
tableName: "ProblemReport",
value: """
@@ -47,21 +47,21 @@ struct ProblemReportViewModel {
comment: ""
)
- let viewLogsButtonTitle = NSLocalizedString(
+ static let viewLogsButtonTitle = NSLocalizedString(
"VIEW_APP_LOGS_BUTTON_TITLE",
tableName: "ProblemReport",
value: "View app logs",
comment: ""
)
- let sendLogsButtonTitle = NSLocalizedString(
+ static let sendLogsButtonTitle = NSLocalizedString(
"SEND_BUTTON_TITLE",
tableName: "ProblemReport",
value: "Send",
comment: ""
)
- let emptyEmailAlertWarning = NSLocalizedString(
+ static let emptyEmailAlertWarning = NSLocalizedString(
"EMPTY_EMAIL_ALERT_MESSAGE",
tableName: "ProblemReport",
value: """
@@ -71,14 +71,14 @@ struct ProblemReportViewModel {
comment: ""
)
- let confirmEmptyEmailTitle = NSLocalizedString(
+ static let confirmEmptyEmailTitle = NSLocalizedString(
"EMPTY_EMAIL_ALERT_SEND_ANYWAY_ACTION",
tableName: "ProblemReport",
value: "Send anyway",
comment: ""
)
- let cancelEmptyEmailTitle = NSLocalizedString(
+ static let cancelEmptyEmailTitle = NSLocalizedString(
"EMPTY_EMAIL_ALERT_CANCEL_ACTION",
tableName: "ProblemReport",
value: "Cancel",