-
Notifications
You must be signed in to change notification settings - Fork 295
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
Conversation
'site_id' => $credentials['oauth2_client_id'], | ||
'site_secret' => $credentials['oauth2_client_secret'], | ||
'notification_id' => $data['notificationID'], | ||
'notification_state' => $data['notificationState'], |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'],
There was a problem hiding this comment.
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)
if ( ! empty( $decoded['error'] ) ) { | ||
throw new Exception( $decoded['error'] ); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
'/' . REST_Routes::REST_ROOT . '/core/site/data/notifications', | ||
'/' . REST_Routes::REST_ROOT . '/core/site/data/mark-notification', |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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 ) | ||
); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
CLAs look good, thanks! ℹ️ Googlers: Go here for more info. |
There was a problem hiding this 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:
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 ) ); |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ) => { |
There was a problem hiding this comment.
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?
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 |
There was a problem hiding this comment.
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' ) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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' => '', |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
'site_id' => $credentials['oauth2_client_id'], | ||
'site_secret' => $credentials['oauth2_client_secret'], | ||
'notification_id' => $data['notificationID'], | ||
'notification_state' => $data['notificationState'], |
There was a problem hiding this comment.
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'],
if ( ! empty( $decoded['error'] ) ) { | ||
throw new Exception( $decoded['error'] ); | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@felixarntz
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
@tofumatt They are different, but not unexpected. Like I said, I think they're an improvement but we can revert that if necessary. |
Yeah I thought about that too, let's use |
There was a problem hiding this 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 ); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
onCTAClick( e, dismiss ); | ||
} else { | ||
dismiss( e ); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this for?
There was a problem hiding this comment.
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.
@felixarntz I've simplified the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
These changes were reverted anyway.
Summary
Addresses issue #1110
Relevant technical choices
dompurify
to allow for limited HTML in content passed to notificationdescription
Updated VRT due to visual changes (see Add support for remote notifications #1170 (comment))Checklist