Repository: Automattic/wp-calypso
Pull request #101482: User invites: Redirect to /sites after a user accepts the invitation
Author: login: BogdanUngureanu
Created: 2025-03-17T21:05:51Z | Merged: 2025-03-18T16:22:50Z

<!--
Link a related issue to this PR. If the PR does not immediately resolve the issue,
for example, it requires a separate deployment to production, avoid
using the "fixes" keyword and instead attach the [Status] Fix Inbound label to
the linked issue.
-->

Related to https://github.com/Automattic/wp-calypso/issues/100417

## Proposed Changes

* Redirect to /sites instead of home after the user accepts the invite
* Remove the lodash dependency
* Convert the file to tsx.
* Make the notice persistent
* Remove the descriptions since they are no longer relevant for the new path

## Why are these changes being made?
<!--
It's easy to see what a PR does but much harder to find out why it was made,
particularly when researching old changes in history. Record an explanation of
the motivation behind this change and how it will help.
-->

* For some user roles, there's no My Home. It makes more sense to redirect them to /sites

## Testing Instructions

<!--
Add as many details as possible to help others reproduce the issue and test the fix.
"Before / After" screenshots can also be very helpful when the change is visual.
-->

* Use the live link
* Go to a simple site and invite another test user (as contributor, author, admin)
* Go to the user and copy the invitation link
* Replace the domain with the .live domain: e.g. demo-calypso.live/accept-invite/238709619/2b350fdf553e9315d194cd539ba4e78e_FOnpWfp9GfhA3g8jLI64wooT
* Click Join
* Notice that you are now redirected to /sites
* Notice that the notice is displayed correctly on the top-right of the page

## Pre-merge Checklist

<!--
Complete applicable items on this checklist **before** merging into trunk. Inapplicable items can be left unchecked.

Both the PR author and reviewer are responsible for ensuring the checklist is completed.
-->

- [ ] Has the general commit checklist been followed? (PCYsg-hS-p2)
- [ ] [Have you written new tests](https://wpcalypso.wordpress.com/devdocs/docs/testing/index.md) for your changes?
- [ ] Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
- [ ] Have you checked for TypeScript, React or other console errors?
- [ ] Have you used memoizing on expensive computations? More info in [Memoizing with create-selector](https://github.com/Automattic/wp-calypso/blob/trunk/packages/state-utils/src/create-selector/README.md) and [Using memoizing selectors](https://react-redux.js.org/api/hooks#using-memoizing-selectors) and [Our Approach to Data](https://github.com/Automattic/wp-calypso/blob/trunk/docs/our-approach-to-data.md)
- [ ] Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    - [ ] For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages. 
- [ ] For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?



Diff:
diff --git a/client/my-sites/invites/utils.js b/client/my-sites/invites/utils.tsx
similarity index 65%
rename from client/my-sites/invites/utils.js
rename to client/my-sites/invites/utils.tsx
index e417844098d9..9c7c47dd47ed 100644
--- a/client/my-sites/invites/utils.js
+++ b/client/my-sites/invites/utils.tsx
@@ -1,15 +1,33 @@
 import i18n from 'i18n-calypso';
-import { get } from 'lodash';
 import { logmeinUrl } from 'calypso/lib/logmein';
 
-export function acceptedNotice( invite, displayOnNextPage = true ) {
+type InviteType = {
+	site: {
+		URL: string;
+		title: string;
+		is_wpforteams_site: boolean;
+		ID: string;
+		domain: string;
+		admin_url: string;
+		is_vip: boolean;
+	};
+	role: string;
+};
+
+export function acceptedNotice(
+	invite: InviteType,
+	displayOnNextPage = true,
+	isPersistent = false
+) {
+	const siteUrl = invite?.site?.URL ?? '';
+	const siteTitle = invite?.site?.title ?? '';
 	const site = (
-		<a href={ get( invite, 'site.URL' ) } className="invites__notice-site-link">
-			{ get( invite, 'site.title' ) }
+		<a href={ siteUrl } className="invites__notice-site-link">
+			{ siteTitle }
 		</a>
 	);
 
-	switch ( get( invite, 'role' ) ) {
+	switch ( invite?.role ) {
 		case 'follower':
 			return [
 				i18n.translate( 'You are now following {{site/}}', {
@@ -17,8 +35,9 @@ export function acceptedNotice( invite, displayOnNextPage = true ) {
 				} ),
 				{
 					button: i18n.translate( 'Visit Site' ),
-					href: get( invite, 'site.URL' ),
+					href: siteUrl,
 					displayOnNextPage,
+					isPersistent,
 				},
 			];
 
@@ -29,8 +48,9 @@ export function acceptedNotice( invite, displayOnNextPage = true ) {
 				} ),
 				{
 					button: i18n.translate( 'Visit Site' ),
-					href: get( invite, 'site.URL' ),
+					href: siteUrl,
 					displayOnNextPage,
+					isPersistent,
 				},
 			];
 
@@ -42,14 +62,6 @@ export function acceptedNotice( invite, displayOnNextPage = true ) {
 							components: { site },
 						} ) }
 					</h3>
-					<p className="invites__intro">
-						{ i18n.translate(
-							'This is your site dashboard where you will be able to manage all aspects of %(site)s',
-							{
-								args: { site: get( invite, 'site.title' ) },
-							}
-						) }
-					</p>
 					<p>
 						{ i18n.translate(
 							'Not sure where to start? Head on over to {{a}}Learn WordPress{{/a}}.',
@@ -59,7 +71,7 @@ export function acceptedNotice( invite, displayOnNextPage = true ) {
 						) }
 					</p>
 				</div>,
-				{ displayOnNextPage },
+				{ displayOnNextPage, isPersistent },
 			];
 
 		case 'editor':
@@ -70,12 +82,6 @@ export function acceptedNotice( invite, displayOnNextPage = true ) {
 							components: { site },
 						} ) }
 					</h3>
-					<p className="invites__intro">
-						{ i18n.translate(
-							'This is your site dashboard where you can publish and manage your ' +
-								'own posts and the posts of others, as well as upload media.'
-						) }
-					</p>
 					<p>
 						{ i18n.translate(
 							'Not sure where to start? Head on over to {{a}}Learn WordPress{{/a}}.',
@@ -85,7 +91,7 @@ export function acceptedNotice( invite, displayOnNextPage = true ) {
 						) }
 					</p>
 				</div>,
-				{ displayOnNextPage },
+				{ displayOnNextPage, isPersistent },
 			];
 
 		case 'author':
@@ -96,12 +102,6 @@ export function acceptedNotice( invite, displayOnNextPage = true ) {
 							components: { site },
 						} ) }
 					</h3>
-					<p className="invites__intro">
-						{ i18n.translate(
-							'This is your site dashboard where you can publish and ' +
-								'edit your own posts as well as upload media.'
-						) }
-					</p>
 					<p>
 						{ i18n.translate(
 							'Not sure where to start? Head on over to {{a}}Learn WordPress{{/a}}.',
@@ -111,7 +111,7 @@ export function acceptedNotice( invite, displayOnNextPage = true ) {
 						) }
 					</p>
 				</div>,
-				{ displayOnNextPage },
+				{ displayOnNextPage, isPersistent },
 			];
 
 		case 'contributor':
@@ -122,13 +122,8 @@ export function acceptedNotice( invite, displayOnNextPage = true ) {
 							components: { site },
 						} ) }
 					</h3>
-					<p className="invites__intro">
-						{ i18n.translate(
-							'This is your site dashboard where you can write and manage your own posts.'
-						) }
-					</p>
 				</div>,
-				{ displayOnNextPage },
+				{ displayOnNextPage, isPersistent },
 			];
 
 		case 'subscriber':
@@ -136,29 +131,30 @@ export function acceptedNotice( invite, displayOnNextPage = true ) {
 				i18n.translate( "You're now a Subscriber of: {{site/}}", {
 					components: { site },
 				} ),
-				{ displayOnNextPage },
+				{ displayOnNextPage, isPersistent },
 			];
 		default:
 			return [
 				i18n.translate( "You're now a new member of: {{site/}}", {
 					components: { site },
 				} ),
-				{ displayOnNextPage },
+				{ displayOnNextPage, isPersistent },
 			];
 	}
 }
 
-export function getRedirectAfterAccept( invite ) {
+export function getRedirectAfterAccept( invite: InviteType ) {
 	if ( invite.site.is_wpforteams_site ) {
 		return `https://${ invite.site.domain }`;
 	}
 
 	const readerPath = '/reader';
 	const postsListPath = '/posts/' + invite.site.ID;
-	const myHomePath = '/home/' + invite.site.domain;
-	const getDestinationUrl = ( redirect ) => {
+	const mySitesPath = '/sites';
+	const getDestinationUrl = ( redirect: string ) => {
 		const remoteLoginHost = `https://${ invite.site.domain }`;
-		const remoteLoginBackUrl = ( destinationPath ) => `https://wordpress.com${ destinationPath }`;
+		const remoteLoginBackUrl = ( destinationPath: string ) =>
+			`https://wordpress.com${ destinationPath }`;
 		const destination = logmeinUrl( remoteLoginHost, remoteLoginBackUrl( redirect ) );
 		const isMissingLogmein = destination === remoteLoginHost;
 		return isMissingLogmein ? redirect : destination;
@@ -181,6 +177,6 @@ export function getRedirectAfterAccept( invite ) {
 			return getDestinationUrl( readerPath );
 
 		default:
-			return getDestinationUrl( myHomePath );
+			return getDestinationUrl( mySitesPath );
 	}
 }
diff --git a/client/state/invites/actions.js b/client/state/invites/actions.js
index 91f89067f931..539a7130f481 100644
--- a/client/state/invites/actions.js
+++ b/client/state/invites/actions.js
@@ -242,8 +242,10 @@ export function acceptInvite( invite, emailVerificationSecret ) {
 				await dispatch( fetchCurrentUser() );
 			}
 
+			const acceptedNoticeResult = acceptedNotice( invite, true, true );
+
 			if ( ! invite.site.is_vip && ! invite.site.is_wpforteams_site ) {
-				dispatch( successNotice( ...acceptedNotice( invite ) ) );
+				dispatch( successNotice( acceptedNoticeResult[ 0 ], acceptedNoticeResult[ 1 ] ) );
 			}
 
 			recordTracksEvent( 'calypso_invite_accepted', {


Reviews:
user: login: xavier-lc
body: Works as described 👌
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:42:37Z
user: login: BogdanUngureanu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T16:18:12Z

Review comments:
diff_hunk: @@ -242,8 +242,10 @@ export function acceptInvite( invite, emailVerificationSecret ) {
 				await dispatch( fetchCurrentUser() );
 			}
 
+			const acceptedNoticeResult = acceptedNotice( invite, true, true );
+
 			if ( ! invite.site.is_vip && ! invite.site.is_wpforteams_site ) {
-				dispatch( successNotice( ...acceptedNotice( invite ) ) );
+				dispatch( successNotice( acceptedNoticeResult[ 0 ], acceptedNoticeResult[ 1 ] ) );
path: client/state/invites/actions.js
user: login: xavier-lc
body: Nit:

```suggestion
				dispatch( successNotice( ...acceptedNoticeResult ) );
```
created_at: 2025-03-18T14:40:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -242,8 +242,10 @@ export function acceptInvite( invite, emailVerificationSecret ) {
 				await dispatch( fetchCurrentUser() );
 			}
 
+			const acceptedNoticeResult = acceptedNotice( invite, true, true );
+
 			if ( ! invite.site.is_vip && ! invite.site.is_wpforteams_site ) {
-				dispatch( successNotice( ...acceptedNotice( invite ) ) );
+				dispatch( successNotice( acceptedNoticeResult[ 0 ], acceptedNoticeResult[ 1 ] ) );
path: client/state/invites/actions.js
user: login: BogdanUngureanu
body: It was like that but the linter didn't like it. 
```
Argument type
...[React. ReactElement<any, string | JSXElementConstructor<any>> | string | number, {button: string, displayOnNextPage: boolean, isPersistent: boolean, href: string}] | [JSX. Element, {displayOnNextPage: boolean, isPersistent: boolean}] | [React. ReactElement<any, string | JSXElementConstructor<any>> | string | number, {displayOnNextPage: boolean, isPersistent: boolean}]
is not assignable to parameter type NoticeText ...   Type
...[React. ReactElement<any, string | JSXElementConstructor<any>> | string | number, {button: string, displayOnNextPage: boolean, isPersistent: boolean, href: string}] | [JSX. Element, {displayOnNextPage: boolean, isPersistent: boolean}] | [React. ReactElement<any, string | JSXElementConstructor<any>> | string | number, {displayOnNextPage: boolean, isPersistent: boolean}]
is not assignable to type
string | ReactElement<any, string | JSXElementConstructor<any>> | number | Iterable<ReactNode> | ReactPortal | boolean | Promise<AwaitedReactNode> | bigint
     Type
...[React. ReactElement<any, string | JSXElementConstructor<any>> | string | number, {button: string, displayOnNextPage: boolean, isPersistent: boolean, href: string}] | [JSX. Element, {displayOnNextPage: boolean, isPersistent: boolean}] | [React. ReactElement<any, string | JSXElementConstructor<any>> | string | number, {displayOnNextPage: boolean, isPersistent: boolean}]
is not assignable to type bigint 
```
created_at: 2025-03-18T16:18:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: matticbot
created_at: 2025-03-17T21:05:53Z
body: <details>
<summary>Calypso Live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-137448">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-137448%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-137448">https://calypso.live?image=registry.a8c.com/calypso/app:build-137448</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Jetpack Cloud live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-137448&env=jetpack">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-137448%26env%3Djetpack%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-137448&env=jetpack">https://calypso.live?image=registry.a8c.com/calypso/app:build-137448&env=jetpack</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Automattic for Agencies live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-137448&env=a8c-for-agencies">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-137448%26env%3Da8c-for-agencies%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-137448&env=a8c-for-agencies">https://calypso.live?image=registry.a8c.com/calypso/app:build-137448&env=a8c-for-agencies</a>
</td>
</tr>
</table>
</details>
<!--calypso-live-watermark:apr@v1-->
author_association: CONTRIBUTOR
user: login: matticbot
created_at: 2025-03-17T21:16:09Z
body: <!-- icfy-watermark: c52822 -->
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

**Sections** (~135 bytes removed 📉 [gzipped])
<details>

```
name           parsed_size           gzip_size
settings            -665 B  (-0.1%)      -83 B  (-0.0%)
people              -665 B  (-0.1%)      -83 B  (-0.0%)
accept-invite       -664 B  (-0.4%)      -52 B  (-0.1%)
```

Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to.
</details>

**Legend**
<details>
<summary>What is parsed and gzip size?</summary>

**Parsed Size:** Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
**Gzip Size:** Compressed size of the JS and CSS files. This much data needs to be downloaded over network.
</details>

Generated by performance advisor bot at [iscalypsofastyet.com](http://iscalypsofastyet.com).
author_association: CONTRIBUTOR
user: login: matticbot
created_at: 2025-03-18T14:14:18Z
body: **This PR modifies the release build for the following Calypso Apps:**

_For info about this notification, see here: PCYsg-OT6-p2_

* notifications
* wpcom-block-editor

To test WordPress.com changes, run `install-plugin.sh $pluginSlug fix/update-admin-menu-after-invite` on your sandbox.
<!--calypso-app-artifacts-watermark:apr@v1-->
author_association: CONTRIBUTOR