Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for remote notifications #1170

Merged
merged 55 commits into from
Feb 27, 2020
Merged

Conversation

aaemnnosttv
Copy link
Collaborator

@aaemnnosttv aaemnnosttv commented Feb 25, 2020

Summary

Addresses issue #1110

Relevant technical choices

Checklist

  • My code is tested and passes existing unit tests.
  • My code has an appropriate set of unit tests which all pass.
  • My code is backward-compatible with WordPress 4.7 and PHP 5.6.
  • My code follows the WordPress coding standards.
  • My code has proper inline documentation.
  • I have signed the Contributor License Agreement (see https://cla.developers.google.com/).

Comment on lines +155 to +158
'site_id' => $credentials['oauth2_client_id'],
'site_secret' => $credentials['oauth2_client_secret'],
'notification_id' => $data['notificationID'],
'notification_state' => $data['notificationState'],
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixarntz I'm not sure why, but PHPCS says that this alignment is correct.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it wants to => to be aligned?

'site_id'            => $credentials['oauth2_client_id'],
'site_secret'        => $credentials['oauth2_client_secret'],
'notification_id'    => $data['notificationID'],
'notification_state' => $data['notificationState'],

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should want that, but it automatically fixes it as highlighted above (which should be wrong)

Comment on lines +209 to +211
if ( ! empty( $decoded['error'] ) ) {
throw new Exception( $decoded['error'] );
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixarntz Should we ever display an error message returned by the remote? Currently it's logged in the console only.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A generic error message (like a 500, if the proxy's notification endpoint was down/erroring) seems wrong to display, because it's not very actionable/helpful to the user. I'd think an allow-list of errors to display would work if wanted, but otherwise it seems best to log them to the console for debugging/support. Otherwise hide 'em?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we should stick to only logging and don't render any user-facing messages. It should just silently error then.

Comment on lines +56 to +57
'/' . REST_Routes::REST_ROOT . '/core/site/data/notifications',
'/' . REST_Routes::REST_ROOT . '/core/site/data/mark-notification',
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixarntz I noticed that without these the test failed because they're hooked in during the main bootstrapping process. We could (and probably should) move the route tests to their respective classes that add them, and only test for the routes that are added in this class specifically. We just need to clear the filter at the beginning like we do in others (i.e. remove_all_filters( 'googlesitekit_rest_routes' )). This was the simpler change to make for now though. Let me know if you'd like me to make that change also here or we could do later in another issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense. Could you do that in a follow-up PR? If we can get it into this release, we can even just connect it to the same issue. I just don't want that work to block this PR.

Comment on lines +254 to +261
private function missing_required_param( $param ) {
return new WP_Error(
'missing_required_param',
/* translators: %s: Missing parameter name */
sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), $param ),
array( 'status' => 400 )
);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixarntz I've thought about refactoring with something like this before since there is quite a bit of duplication for this kind of thing in the datapoint handlers. Ideally we would use core's args definitions and leverage the native validation the API already provides. The problem is that our data requests stuff all the parameters into one data parameter (except for a few outliers like module activation), and the REST API doesn't support nested validation except for collections. This may not be the best place to discuss this but just thought I'd raise it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I thought about this initially too, but our support for batch requests would make use of that core feature challenging. I think for now we should keep this internal to the respective handlers (although adding utility methods, potentially even a validation class or trait for common validation features is certainly worth considering).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I thought about this initially too, but our support for batch requests would make use of that core feature challenging.

It would require that we change the way batch requests are handled to dispatch the individual REST requests (internally-w/o HTTP) in the batch handler rather than the way it is now but I think it's possible. Better to discuss this in a new issue though.

@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

Copy link
Collaborator

@tofumatt tofumatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These visual changes seem off to me:

Screenshot 2020-02-26 13 23 44

Screenshot 2020-02-26 13 23 39

Screenshot 2020-02-26 13 23 50

Approach here looks good. I know I helped write it, but the HTML sanitizing here is safe (it's the same approach I've used on many projects and trusted on some giant sites) which is good. I left thoughts on some of the code but I don't think any comments are blocking/much to fix.

But those visual changes strike me as unintentional.

async componentDidMount() {
const notifications = await getNotifications().catch( ( e ) => {
// eslint-disable-next-line no-console
console.warn( 'Error caught while fetching notifications', JSON.stringify( e ) );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I often use global.console for these. It probably doesn't matter? I dunno!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: why stringify the error? console can output objects.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I know but it doesn't work when logging from E2E tests.

https://travis-ci.com/google/site-kit-wp/jobs/291425794#L1104

I didn't mean to keep it like that though so I'll update it.

}

async componentDidMount() {
const notifications = await getNotifications().catch( ( e ) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For async/await code it's usually the norm to do try/catch blocks instead of catch. Any reason to prefer catch here?

Suggested change
const notifications = await getNotifications().catch( ( e ) => {
try {
const notifications = await getNotifications();
this.setState( { notifications } );
} catch ( err ) {
// eslint-disable-next-line no-console
console.warn( 'Error caught while fetching notifications', JSON.stringify( err ) );
}

I dunno what notifications get set to when this fails, but it seems off to set notifications if there was an error fetching them too.

}

return notifications.map(
( notification ) => <Notification
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nitpick, but I find this a bit tougher to read than:

( notification ) => (
  <Notification
    {…props}
  />
)

ctaLink={ notification.ctaURL || '' }
ctaLabel={ notification.ctaLabel || '' }
ctaTarget={ notification.ctaTarget || '' }
dismiss={ notification.dismissLabel || __( 'OK, Got it!', 'google-site-kit' ) }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dismiss={ notification.dismissLabel || __( 'OK, Got it!', 'google-site-kit' ) }
dismiss={ notification.dismissLabel || __( 'OK, got it!', 'google-site-kit' ) }

The "G" here shouldn't be capitalised if not a new sentence.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a bunch of these in the codebase. I've updated them all so let's see how that goes...

$this->args = array_merge(
array(
'title' => '',
'content' => '',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are always empty-string by default, any reason the component does notification.content || '' when passing props? Seems unneeded. 🤷‍♂ Belt and braces I guess?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures that the keys are always set on the PHP side.

Comment on lines +155 to +158
'site_id' => $credentials['oauth2_client_id'],
'site_secret' => $credentials['oauth2_client_secret'],
'notification_id' => $data['notificationID'],
'notification_state' => $data['notificationState'],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it wants to => to be aligned?

'site_id'            => $credentials['oauth2_client_id'],
'site_secret'        => $credentials['oauth2_client_secret'],
'notification_id'    => $data['notificationID'],
'notification_state' => $data['notificationState'],

Comment on lines +209 to +211
if ( ! empty( $decoded['error'] ) ) {
throw new Exception( $decoded['error'] );
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A generic error message (like a 500, if the proxy's notification endpoint was down/erroring) seems wrong to display, because it's not very actionable/helpful to the user. I'd think an allow-list of errors to display would work if wanted, but otherwise it seems best to log them to the console for debugging/support. Otherwise hide 'em?

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aaemnnosttv

One byproduct of this change is that we needed to separate the description into its own element

Can you clarify why this is necessary? Wouldn't it be possible to use Fragment dangerouslySetInnerHTML=... instead? It would be great if we could avoid that change, so that it doesn't result in additional paragraphs just for the learn more link.

Otherwise, this looks good to me once @tofumatt's comments are addressed.

@aaemnnosttv
Copy link
Collaborator Author

Can you clarify why this is necessary? Wouldn't it be possible to use Fragment dangerouslySetInnerHTML=... instead? It would be great if we could avoid that change, so that it doesn't result in additional paragraphs just for the learn more link.

@felixarntz Fragments don't support using dangerouslySetInnerHTML 😞
See facebook/react#12014

It would be great if we could avoid that change, so that it doesn't result in additional paragraphs just for the learn more link.

I think the UI is a bit cleaner and more consistent with the separation myself, but if we wanted to keep them, we could wrap description and the learn more with spans instead and then wrap that with a <p> tag to let them render inline.

These visual changes seem off to me:

@tofumatt They are different, but not unexpected. Like I said, I think they're an improvement but we can revert that if necessary.
See #1170 (comment)

@felixarntz
Copy link
Member

@aaemnnosttv

I think the UI is a bit cleaner and more consistent with the separation myself, but if we wanted to keep them, we could wrap description and the learn more with spans instead and then wrap that with a <p> tag to let them render inline.

Yeah I thought about that too, let's use spans then and maintain the original appearance.

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aaemnnosttv Testing this again, there's just one more thing that needs to be updated here.

dismiss={ notification.dismissLabel || __( 'OK, Got it!', 'google-site-kit' ) }
isDismissable={ notification.dismissible }
onCTAClick={ async () => {
await acceptNotification( notification.id );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make the notice dismiss here, also when the user accepts it. It could either be through manual logic in this callback here, or alternatively we could introduce some kind of dismissOnCTA flag prop in Notification to generally support that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is right now, it opens the link in a new tab (which is what should happen). The problem is then the notice still remains open in the other window. We need to make sure it dismissed on the JS side.

Copy link
Collaborator Author

@aaemnnosttv aaemnnosttv Feb 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed a small update to address this although it needs testing still.

Basically, if no onCTAClick handler is passed, the notification will dismiss on CTA click if dismissible.

If an onCTAClick handler is passed then that callback is responsible for calling dismiss or not, which is passed to the callback as the second argument (first being the event). If the event isn't dismissible, the dismiss function is a no-op.

The callback highlighted above has then been updated to call dismiss after acceptNotification 😄

To do this, I had to separate the handleDismiss event handler from the main dismiss logic to prevent calling dismissNotification after dismissing it from the onCTAClick handler 😄 - onDismiss is only called by the event handler (when clicking "dismiss"), not when called by an onCTAClick callback.

Note this also implies that all existing dismissible notifications with CTA links will be dismissed when clicked, although I think that's what should probably happen anyways?

https://github.com/google/site-kit-wp/pull/1170/files/4c4e0bc4feabb77eb739a9a822db342da08f054a..cd676504fd1ff30843177b17be636ac05e06c2b6

onCTAClick( e, dismiss );
} else {
dismiss( e );
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisiting this, wouldn't it make more sense to make handleCTAClick async and then await onCTAClick, without passing dismiss to it? Then we would always call dismiss at the end. This would make it more similar to handleDismiss and avoid the extra parameter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure we could do that. My initial approach here assumes that there might be cases where we don't want to dismiss on CTA click, even if the notification is dismissible, but that does sound like a bit of an edge case 😄

This looks cleaner indeed though so assuming the above isn't a concern, let's go with it 👍

const card = this.cardRef.current;

async handleDismiss( e ) {
e.persist();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these events are actually SyntheticEvents from React, we need to persist them using this method to ensure they are not disposed of to make them available to any subsequent callbacks. We don't actually use the events right now, but if we're going to pass it, it should be persisted.

@aaemnnosttv
Copy link
Collaborator Author

@felixarntz I've simplified the handleCTAClick as you suggested and testing has been going well.
Ready for another pass 👍

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

@felixarntz felixarntz dismissed tofumatt’s stale review February 27, 2020 15:21

These changes were reverted anyway.

@felixarntz felixarntz merged commit dee2ef6 into develop Feb 27, 2020
@felixarntz felixarntz deleted the add/1110-remote-notifications branch February 27, 2020 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants