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

Update/split site user connection mgt my jetpack #41520

Open
wants to merge 22 commits into
base: fix/my-jetpack-for-non-admins
Choose a base branch
from

Conversation

jboland88
Copy link
Contributor

@jboland88 jboland88 commented Feb 3, 2025

Proposed changes:

  • This PR allows users to break user connections separately from the connection management window in My Jetpack
  • This allows non-admin users to manage their own user connection from My Jetpack
  • Some messaging improvements are also made in the connection messages shown to non-admins

Does this pull request change what data or activity we track or use?

Yes, this PR adds one new click event for when someone clicks to disconnect their user account: jetpack_manage_connection_dialog_disconnect_user_click

Testing instructions:

  • To test this PR fully, you will need a test site with a non admin user and an admin user along with two separate WPCOM accounts that you can use to connect these users individually.

  • Start by connecting both the admin and non-admin users with their respective WPCOM accounts

  • As the admin user, navigate to My Jetpack (Jetpack in the admin left nav)

  • Your connection area should look like below, showing both your wpcom username and email:
    Screenshot 2025-02-04 at 4 04 29 PM

  • Now, check My Jetpack as the non-admin user. You should see your wpcom username and email as well as the local username of the connected site owner
    Screenshot 2025-02-04 at 4 06 42 PM

  • As the non-admin user, click "Manage" next to the connection icons. You should get the manage connection dialog with only one option - disconnecting your user account:
    Screenshot 2025-02-04 at 4 07 42 PM

  • Click on "Disconnect My User account". The modal should show in a "pending" state while the user disconnects and then close itself. The page will also reload after the modal closes
    Screenshot 2025-02-04 at 4 10 56 PM

  • Now that the non-admin user has been disconnected, the connection area should no longer show the "Manage" link next to the connection icons. In place of the user information, there should be a prompt to sign back in.
    Screenshot 2025-02-04 at 4 11 07 PM

  • Test that you can sign in again as the non-admin user. Leave the non-admin user connected for further testing later on.

  • Now, return to the site as the admin user

  • Click on "Manage" next to the connection icons to open the connection management modal

  • The admin user should have three options (assuming they are also the connection owner) - transfer ownership, disconnect user account, and Disconnect Jetpack
    Screenshot 2025-02-04 at 4 19 07 PM

  • Go ahead and disconnect the admin user. The page will reload after disconnection. After reload, the connection area should look something like this (the message can differ depending on whether you had products that were previously relying on the user connection to work)
    Screenshot 2025-02-04 at 4 19 28 PM

  • If you click "Manage" again as the admin user, there should only be one option, to Disconnect Jetpack. This will disconnect the site from Jetpack (removes the site token)
    Screenshot 2025-02-04 at 4 19 51 PM

  • Go ahead and disconnect the site from Jetpack. The connection area should prompt for re-connection of the site after disconnection:
    Screenshot 2025-02-04 at 4 20 52 PM

  • Re-connect the site by clicking "Connect"

  • After the site connects, before re-connecting the admin user, visit the site as the non-admin user (who we should have left in a connected state in our previous test)

  • For the connected non-admin, now that the admin user and connection owner has disconnected, the connection area should no longer show the connection owner as being "also connected"
    Screenshot 2025-02-04 at 4 27 48 PM

  • The option to "Manage" and disconnect the non-admin user is still present - make sure that you can still disconnect the non-admin user here with no admin connection owner.

  • After the non-admin user disconnects, without the presence of a connection owner, the connection area should now indicate that an admin needs to connect before they can sign in again
    Screenshot 2025-02-04 at 4 30 17 PM

  • Fiddle around with the different users and connection states and confirm that the copy and messaging makes sense for different states and roles.


Testing with an older version of the Jetpack plugin

Because this PR moves the /connection/user API endpoint from the Jetpack plugin to the connection package, there are scenarios where both the Jetpack plugin and the connection package will register the same endpoint. To mitigate this, this PR makes sure the API endpoint registration for the connection package runs later than the registration for the Jetpack plugin by running on priority 11 for the rest_api_init hook.

To test that the new endpoint it still called when an older version of Jetpack is active:

  • Get a new test site with Jetpack Beta (works better if it's not your local dev environment)
  • Checkout this branch on the Jetpack Boost plugin using the Jetpack Beta plugin
  • In the Jetpack Beta plugin, set the version of the main Jetpack plugin to 14.2
  • Confirm that you are still able to disconnect your user account from My Jetpack.

Copy link
Contributor

github-actions bot commented Feb 3, 2025

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WordPress.com Simple site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin, and enable the update/split-site-user-connection-mgt-my-jetpack branch.

    • For jetpack-mu-wpcom changes, also add define( 'JETPACK_MU_WPCOM_LOAD_VIA_BETA_PLUGIN', true ); to your wp-config.php file.
  • To test on Simple, run the following command on your sandbox:

    bin/jetpack-downloader test jetpack update/split-site-user-connection-mgt-my-jetpack
    
    bin/jetpack-downloader test jetpack-mu-wpcom-plugin update/split-site-user-connection-mgt-my-jetpack
    

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@jboland88 jboland88 marked this pull request as ready for review February 4, 2025 21:36
@jboland88
Copy link
Contributor Author

Here's an open question @jeherve and @bindlegirl

This PR moves an API endpoint from the Jetpack plugin to the connection package (connection/user). The endpoint in the package has a version check so that it won't double-register the endpoint if an older version of Jetpack has already registered it.

But this raises the issue - if a standalone plugin using the newer version of My Jetpack and newer connection package is installed on a site with an older version of the Jetpack plugin, the way My Jetpack is going to expect the connection/user endpoint to respond is not going to align with what the old version of the Jetpack plugin will return.

I've tested and can verify this behavior with the Jetpack Beta plugin.

An alternative could be to register the endpoint in the connection package as a "new" endpoint with a different name - but that feels counterproductive. Any ideas for the best way forward here?

@bindlegirl
Copy link
Contributor

Any ideas for the best way forward here?

Would skipping the version check and setting the override parameter to true help solve the problem?

I haven't checked the differences in the updated endpoint just yet so I am throwing out that suggestion.

@jboland88
Copy link
Contributor Author

jboland88 commented Feb 5, 2025

Would skipping the version check and setting the override parameter to true help solve the problem?

@bindlegirl - that sounds like it would be a good option. I've overlooked the override parameter on register_rest_route until now - TIL! As long as the route for the connection package is registered later than Jetpack's that would allow us to replace it 👍

The new endpoint should be compatible if called from the older version of the Jetpack plugin, the old behavior and arguments are still present, the updated version just adds a new optional parameter and uses a new mapped capability to allow for unlinking of non-admins w/o a connection owner.

@jeherve
Copy link
Member

jeherve commented Feb 5, 2025

This PR moves an API endpoint from the Jetpack plugin to the connection package (connection/user). The endpoint in the package has a version check so that it won't double-register the endpoint if an older version of Jetpack has already registered it.

What if the new endpoint were to have a different slug? We'd keep the old Jetpack-only endpoint around, but stop using it in new versions of My Jetpack. Then in a few versions, we could get rid of the Jetpack-only version entirely.

Copy link
Contributor

@CodeyGuyDylan CodeyGuyDylan left a comment

Choose a reason for hiding this comment

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

I tested the changes thoroughly with both the Jetpack main plugin and then again with a standalone plugin (I did Protect) and it all works as expected. I couldn't find any issues!

I do have one question about the user disconnection. Is a page reload required for this? Or could we update all the data on the screen async? You obviously know more about this flow than me so I assume there probably needs to be one for some reason, but wanted to bring it up just in case 😄

I left some comments. None major, but there are enough I think it warrants requesting changes 😄

{ isCurrentUserAdmin &&
connectedUser.currentUser?.isConnected &&
connectedUser.currentUser?.isMaster && (
<ManageConnectionActionCard
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this is the same functionality as before, but I was wondering if we should open a new window when the user clicks this link? The "External link" icon generally means a new window will open as well in my experience

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good thought, yes I think that makes sense

onClick = () => null,
link = '#',
action,
disabled,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we have disabled be false by default? I guess an omission leaving it null would most likely have the same behavior, I guess I just prefer explicit defaults 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, I agree adding false is more clear 👍

*/
public static function unlink_user( $request ) {

if ( ! isset( $request['linked'] ) || false !== $request['linked'] ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not running into issues while testing, so I assume this works correctly. I'm just having a hard time understanding why the correct thing to do is return an error if the user is linked in this case 🤔

Wouldn't false !== $request['linked'] be true if the user is linked? Or am I misunderstanding the linked parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's kind of an odd parameter - if it's false it means we want the user to be unlinked from what I understand. At least that's the effect. It doesn't have to do with checking that the current user is connected already or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for explaining! It is a very confusingly named param 😅 But it makes sense why it works correctly


// Allow admins to force a disconnect by passing the "force" parameter
// This allows an admin to disconnect themselves
if ( isset( $request['force'] ) && false !== $request['force'] && current_user_can( 'manage_options' ) && ( new Manager( 'jetpack' ) )->disconnect_user_force( get_current_user_id() ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm finding it a bit weird to have the actual disconnect function be invoked in the if statement checks. I guess it works correctly, and I can see the benefit of a simpler hierarchy of if statements but I don't think it's best practices and, imo, it makes it harder to read and understand what's happening 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I disagree on this one - since the function returns a boolean, I think It's OK to run in as part of the if statement, given that the return value of the function is intended to be a rest_response object or WP_Error.

The first couple of checks in the if statement are used to bail early before trying the disconnect.

The second else if condition follows the same pattern to determine if the disconnect operation worked or failed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is a big enough issue to push back too hard, so I won't block approval for it again 😄 However I just wanted to share an article I found that explains, in better words than I could, why I am on the other side of this debate: https://www.teamten.com/lawrence/programming/keep-if-clauses-side-effect-free.html

@@ -63,7 +63,7 @@ const ConnectionListItem: ConnectionListItemType = ( {
<div className={ styles[ 'list-item' ] }>
<Text className={ clsx( styles[ 'list-item-text' ], statusStyles ) }>
{ icon && <Icon icon={ icon } /> }
{ text }
<span>{ text }</span>
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this for some sort of styling change? I don't see any CSS changes related to this so I just wanted to make sure it wasn't leftover from a previous implementation

Copy link
Contributor Author

@jboland88 jboland88 Feb 7, 2025

Choose a reason for hiding this comment

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

It was to appease the tests 😅 They were having trouble locating an element with the specified text on the screen, adding a wrapping element helped the matching

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense 😆

__( 'Connected as %1$s (Owner).', 'jetpack-my-jetpack' ),
userConnectionData.currentUser?.wpcomUser?.display_name
/* translators: %1$s is user name, %2$s is the user email */
__( 'Connected as %1$s (Owner) ( %2$s ).', 'jetpack-my-jetpack' ),
Copy link
Contributor

Choose a reason for hiding this comment

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

I noticed the email here has spaces around it whereas the connected owner does not. Was this intentional?

Screenshot 2025-02-05 at 11 43 46 AM

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not intentional, I'll remove those spaces!

@CodeyGuyDylan
Copy link
Contributor

In addition to the comments in my review, I just noticed it looks like a few of the connection tests are not passing

@bindlegirl
Copy link
Contributor

bindlegirl commented Feb 6, 2025

@jeherve @jboland88

I discussed this case with @fgiannar, and she shared some previous PRs where endpoints were moved to connection package #16384 and #20440

I think it would be best to follow the same procedure we used there (moving the endpoint, deprecating methods). The workings of the override parameter are explained in #20440

It seems the safest route would be to make sure the UI can support the edge case where the old endpoint is registered last.

@bindlegirl
Copy link
Contributor

Another suggestion is to do the endpoint changes in a separate PR.
Hat tip @fgiannar

@jboland88
Copy link
Contributor Author

Thanks for the review @CodeyGuyDylan

I do have one question about the user disconnection. Is a page reload required for this? Or could we update all the data on the screen async? You obviously know more about this flow than me so I assume there probably needs to be one for some reason, but wanted to bring it up just in case 😄

Sort of. After the user is disconnected, we can update the connection state on the front end but the red bubble alerts and the evaluation of "broken" modules that required a user connection are currently pulled from the initial react state - which is loaded in from PHP.

The page "works" without reloading, the connection area still shows the user as disconnected, but there are a few things that are not fully up to date - card statuses, alert messages, etc. I do like the experience better without the reload, but I am in favor of accepting the tradeoff of reloading in order to update the product and alert information for now. Once we re-fetch that data async and aren't pulling it from the initial react state then the reload could be removed here.

@CodeyGuyDylan
Copy link
Contributor

The page "works" without reloading, the connection area still shows the user as disconnected, but there are a few things that are not fully up to date - card statuses, alert messages, etc. I do like the experience better without the reload, but I am in favor of accepting the tradeoff of reloading in order to update the product and alert information for now. Once we re-fetch that data async and aren't pulling it from the initial react state then the reload could be removed here.

That makes sense, I guess it'd be quite a bit more work to make sure everything is in sync after that (if it's even possible to get all of it). I think it's okay in this case 😄

Copy link
Contributor

@CodeyGuyDylan CodeyGuyDylan left a comment

Choose a reason for hiding this comment

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

Looks like you have one linting issue that's causing a test to fail, but other than that I think the changes look great, LGTM

'jetpack-connection-js'
);
}
setUnlinkError( errorMessage );
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice addition with the error message 😄

Copy link
Contributor

@bindlegirl bindlegirl left a comment

Choose a reason for hiding this comment

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

Tested it all and it works well. Nice job!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants