Skip to content

Commit

Permalink
Merge pull request #11171 from wordpress-mobile/issue/11035-replace-a…
Browse files Browse the repository at this point in the history
…nnoying-alerts

Offline: Replace disruptive Alerts with Notices
  • Loading branch information
shiki authored Mar 8, 2019
2 parents abbb623 + b61284c commit fd2474a
Show file tree
Hide file tree
Showing 15 changed files with 209 additions and 71 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
12.0
-----
* Redesigned Notices
* Changed offline error messages to be less disruptive.
* Resolved a defect in the new Site Creation flow where the site preview address bar could be edited.

11.9
Expand Down
7 changes: 7 additions & 0 deletions WordPress/Classes/Stores/NoticeStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ struct Notice {
///
let cancelTitle: String?

/// An optional value that can be used as a reference by consumers.
///
/// This is not used in the Notice system at all.
let tag: String?

/// An optional handler closure that will be called when the action button
/// is tapped, if you've provided an action title
let actionHandler: ActionHandlerFunction?
Expand All @@ -45,13 +50,15 @@ struct Notice {
style: NoticeStyle = NormalNoticeStyle(),
actionTitle: String? = nil,
cancelTitle: String? = nil,
tag: String? = nil,
actionHandler: ActionHandlerFunction? = nil) {
self.title = title
self.message = message
self.feedbackType = feedbackType
self.notificationInfo = notificationInfo
self.actionTitle = actionTitle
self.cancelTitle = cancelTitle
self.tag = tag
self.actionHandler = actionHandler
self.style = style
}
Expand Down
33 changes: 28 additions & 5 deletions WordPress/Classes/Utility/ReachabilityUtils+OnlineActions.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import Foundation
import WordPressFlux

extension ReachabilityUtils {
private enum NoConnectionMessage {
static let title = NSLocalizedString("No Connection",
comment: "Title of error prompt when no internet connection is available.")
static let message = noConnectionMessage()
static let tag = "ReachabilityUtils.NoConnection"
}

/// Performs the action when an internet connection is available
/// If no internet connection is available an error message is displayed
///
@objc class func onAvailableInternetConnectionDo(_ action: () -> Void) {
guard ReachabilityUtils.isInternetReachable() else {
let title = NSLocalizedString("No Connection",
comment: "Title of error prompt when no internet connection is available.")
let message = NSLocalizedString("The Internet connection appears to be offline",
comment: "Message of error prompt shown when a user tries to perform an action without an internet connection.")
WPError.showAlert(withTitle: title, message: message)
WPError.showAlert(withTitle: NoConnectionMessage.title, message: NoConnectionMessage.message)
return
}
action()
Expand All @@ -35,4 +38,24 @@ extension ReachabilityUtils {
return notification.userInfo?[Foundation.Notification.reachabilityKey] as? Bool == true
})
}

/// Shows a generic non-blocking "No Connection" error message to the user.
///
/// We use a Snackbar instead of a literal Alert because, for internet connection errors,
/// Alerts can be disruptive.
static func showNoInternetConnectionNotice() {
let notice = Notice(title: NoConnectionMessage.title,
message: NoConnectionMessage.message,
tag: NoConnectionMessage.tag)
ActionDispatcher.dispatch(NoticeAction.post(notice))
}

/// Dismiss the currently shown Notice if it was created using showNoInternetConnectionNotice()
static func dismissNoInternetConnectionNotice() {
noticePresenter?.dismissCurrentNotice(tagged: NoConnectionMessage.tag)
}

private static var noticePresenter: NoticePresenter? {
return (UIApplication.shared.delegate as? WordPressAppDelegate)?.noticePresenter
}
}
3 changes: 2 additions & 1 deletion WordPress/Classes/Utility/ReachabilityUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ + (void)showAlertNoInternetConnectionWithRetryBlock:(void (^)(void))retryBlock

+ (NSString *)noConnectionMessage
{
return NSLocalizedString(@"The Internet connection appears to be offline.", @"");
return NSLocalizedString(@"The Internet connection appears to be offline.",
@"Message of error prompt shown when a user tries to perform an action without an internet connection.");
}

+ (BOOL)alertIsShowing
Expand Down
35 changes: 35 additions & 0 deletions WordPress/Classes/Utility/WPError+Swift.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

import Foundation
import WordPressFlux

extension WPError {
private static let noticeTag = "WPError.Networking"

/// Show a Notice with the message taken from the given `error`
///
/// This is similar to `showNetworkingAlertWithError` except this uses a Notice instead of
/// an Alert.
///
/// - parameter error: Assumed to be an error from a networking call
static func showNetworkingNotice(title: String, error: NSError) {
if showWPComSigninIfErrorIsInvalidAuth(error) {
return
}

let titleAndMessage = self.titleAndMessage(fromNetworkingError: error, desiredTitle: title)

let notice = Notice(title: titleAndMessage["title"] ?? "",
message: titleAndMessage["message"],
tag: noticeTag)
ActionDispatcher.dispatch(NoticeAction.post(notice))
}

/// Dismiss the currently shown Notice if it was created using showNetworkingNotice()
static func dismissNetworkingNotice() {
noticePresenter?.dismissCurrentNotice(tagged: noticeTag)
}

private static var noticePresenter: NoticePresenter? {
return (UIApplication.shared.delegate as? WordPressAppDelegate)?.noticePresenter
}
}
21 changes: 21 additions & 0 deletions WordPress/Classes/Utility/WPError.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@
*/
+ (void)showXMLRPCErrorAlert:(NSError *)error;

/**
* Create a suggested title and message based on the given `error`
*
* @param error Assumed to be an error from a networking call
* @param desiredTitle If given, this will be the title that will be returned.
*
* @return A dictionary with keys "title" and "message". Both values are not null.
*/
+ (nonnull NSDictionary<NSString *, NSString *> *)titleAndMessageFromNetworkingError:(nonnull NSError *)error
desiredTitle:(nullable NSString *)desiredTitle;

/**
* Shows a sign-in page if the `error`'s cause requires an authentication or authorization.
*
* This is meant to be a helper method for the other methods in this class and is only publicly
* exposed so it can be accessed in WPError.swift.
*
* @param error Assumed to be an error from a networking call.
* @returns YES if a sign-in page was shown.
*/
+ (BOOL)showWPComSigninIfErrorIsInvalidAuth:(nonnull NSError *)error;

///---------------------
/// @name General alerts
Expand Down
103 changes: 61 additions & 42 deletions WordPress/Classes/Utility/WPError.m
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,48 @@ + (void)showNetworkingAlertWithError:(NSError *)error
}

+ (void)showNetworkingAlertWithError:(NSError *)error title:(NSString *)title
{
if ([self showWPComSigninIfErrorIsInvalidAuth:error]) {
return;
}

NSDictionary *titleAndMessage = [self titleAndMessageFromNetworkingError:error desiredTitle:title];

[self showAlertWithTitle:titleAndMessage[@"title"]
message:titleAndMessage[@"message"]
withSupportButton:YES
okPressedBlock:nil];
}

+ (void)showXMLRPCErrorAlert:(NSError *)error
{
NSString *cleanedErrorMsg = [error localizedDescription];

if ([error.domain isEqualToString:WPXMLRPCFaultErrorDomain] && error.code == 401) {
cleanedErrorMsg = NSLocalizedString(@"Sorry, you cannot access this feature. Please check your User Role on this site.", @"");
}

// ignore HTTP auth canceled errors
if ([error.domain isEqual:NSURLErrorDomain] && error.code == NSURLErrorUserCancelledAuthentication) {
[WPError internalInstance].alertShowing = NO;
return;
}

if ([cleanedErrorMsg rangeOfString:@"NSXMLParserErrorDomain"].location != NSNotFound) {
cleanedErrorMsg = NSLocalizedString(@"The app can't recognize the server response. Please, check the configuration of your site.", @"");
}

[self showAlertWithTitle:NSLocalizedString(@"Error", @"Generic popup title for any type of error.") message:cleanedErrorMsg];
}

+ (NSDictionary<NSString *,NSString *> *)titleAndMessageFromNetworkingError:(NSError *)error
desiredTitle:(NSString *)desiredTitle
{
NSString *message = nil;
NSString *customTitle = nil;
NSString *title = nil;

if ([error.domain isEqual:AFURLRequestSerializationErrorDomain] ||
[error.domain isEqual:AFURLResponseSerializationErrorDomain])
[error.domain isEqual:AFURLResponseSerializationErrorDomain])
{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)[error.userInfo objectForKey:AFNetworkingOperationFailingURLResponseErrorKey];
switch (error.code) {
Expand All @@ -54,22 +90,22 @@ + (void)showNetworkingAlertWithError:(NSError *)error title:(NSString *)title
case 415:
case 416:
case 417:
customTitle = NSLocalizedString(@"Incompatible site", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error being returned from the site.");
title = NSLocalizedString(@"Incompatible site", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error being returned from the site.");
message = [NSString stringWithFormat:NSLocalizedString(@"Your site returned a %d error.\nThis is usually due to an incompatible server configuration.\nPlease contact your hosting provider, or reach out to us using our in-app support.", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error being returned from the site."), response.statusCode];
break;
case 403:
customTitle = NSLocalizedString(@"Forbidden Access", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error accessing the site.");
title = NSLocalizedString(@"Forbidden Access", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error accessing the site.");
message = NSLocalizedString(@"Received 'Forbidden Access'.\nIt seems there is a problem with your WordPress site", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error accessing the site.");
break;
case 500:
case 501:
customTitle = NSLocalizedString(@"Internal Server Error", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error accessing the site, most likely due to an error with the server hosting the WP install.");
title = NSLocalizedString(@"Internal Server Error", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error accessing the site, most likely due to an error with the server hosting the WP install.");
message = NSLocalizedString(@"Received 'Internal Server Error'.\nIt seems there is a problem with your WordPress site", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error accessing the site, most likely due to an error with the server hosting the WP install.");
break;
case 502:
case 503:
case 504:
customTitle = NSLocalizedString(@"Temporary Server Error", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error accessing the site, most likely due to an error with the server hosting the WP install, but may be a temporary issue.");
title = NSLocalizedString(@"Temporary Server Error", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error accessing the site, most likely due to an error with the server hosting the WP install, but may be a temporary issue.");
message = NSLocalizedString(@"It seems your WordPress site is not accessible at this time, please try again later", @"Error message shown in the set up process if the WP install was unable to be added to the app due to an error accessing the site, most likely due to an error with the server hosting the WP install, but may be a temporary issue.");
break;
default:
Expand All @@ -81,50 +117,33 @@ + (void)showNetworkingAlertWithError:(NSError *)error title:(NSString *)title
default:
break;
}
} else if ([error.domain isEqualToString:WordPressComRestApiErrorDomain]) {
DDLogError(@"wp.com API error: %@: %@", error.userInfo[WordPressComRestApi.ErrorKeyErrorCode],
[error localizedDescription]);
if (error.code == WordPressComRestApiErrorInvalidToken || error.code == WordPressComRestApiErrorAuthorizationRequired) {
[WordPressAuthenticationManager showSigninForWPComFixingAuthToken];
return;
}
}


if (desiredTitle != nil) {
title = desiredTitle;
} else if (title == nil) {
title = NSLocalizedString(@"Error", @"Generic error alert title");
}

if (message == nil) {
message = [error localizedDescription];
message = [NSString decodeXMLCharactersIn:message];
}

if (title == nil) {
if (customTitle == nil) {
title = NSLocalizedString(@"Error", @"Generic error alert title");
} else {
title = customTitle;
}
}

[self showAlertWithTitle:title message:message withSupportButton:YES okPressedBlock:nil];

return @{@"title": title, @"message": message};
}

+ (void)showXMLRPCErrorAlert:(NSError *)error
{
NSString *cleanedErrorMsg = [error localizedDescription];

if ([error.domain isEqualToString:WPXMLRPCFaultErrorDomain] && error.code == 401) {
cleanedErrorMsg = NSLocalizedString(@"Sorry, you cannot access this feature. Please check your User Role on this site.", @"");
}

// ignore HTTP auth canceled errors
if ([error.domain isEqual:NSURLErrorDomain] && error.code == NSURLErrorUserCancelledAuthentication) {
[WPError internalInstance].alertShowing = NO;
return;
}

if ([cleanedErrorMsg rangeOfString:@"NSXMLParserErrorDomain"].location != NSNotFound) {
cleanedErrorMsg = NSLocalizedString(@"The app can't recognize the server response. Please, check the configuration of your site.", @"");
+ (BOOL)showWPComSigninIfErrorIsInvalidAuth:(nonnull NSError *)error {
if ([error.domain isEqualToString:WordPressComRestApiErrorDomain]) {
DDLogError(@"wp.com API error: %@: %@", error.userInfo[WordPressComRestApi.ErrorKeyErrorCode],
[error localizedDescription]);
if (error.code == WordPressComRestApiErrorInvalidToken || error.code == WordPressComRestApiErrorAuthorizationRequired) {
[WordPressAuthenticationManager showSigninForWPComFixingAuthToken];
return YES;
}
}

[self showAlertWithTitle:NSLocalizedString(@"Error", @"Generic popup title for any type of error.") message:cleanedErrorMsg];
return NO;
}

+ (void)showAlertWithTitle:(NSString *)title message:(NSString *)message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ extension CommentsViewController: NetworkAwareUI {
presentNoNetworkAlert()
}
}

@objc func dismissConnectionErrorNotice() {
dismissNoNetworkAlert()
}
}

extension CommentsViewController: NetworkStatusDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,15 @@ - (void)viewWillAppear:(BOOL)animated

// Refresh the UI
[self refreshNoResultsView];
[self handleConnectionError];

[self refreshAndSyncIfNeeded];
}

- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self dismissConnectionErrorNotice];
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ class NotificationsViewController: UITableViewController, UIViewControllerRestor
super.viewWillDisappear(animated)
stopListeningToNotifications()

dismissNoNetworkAlert()

// If we're not onscreen, don't use row animations. Otherwise the fade animation might get animated incrementally
tableViewHandler.updateRowAnimation = .none
}
Expand Down
Loading

0 comments on commit fd2474a

Please sign in to comment.