From 839954e7a11ac72b670010dbe4eb1e0f894e6f66 Mon Sep 17 00:00:00 2001 From: Paul Bunkham Date: Mon, 13 May 2024 06:50:12 +0100 Subject: [PATCH] Social: Create a social service connect button (#37196) * Social: Create a social service connect button This is currently a proof of concept where we get the available external services and render a connect button, which in turn triggers the authentication flow for each service. To test it, the site must be sandboxed with D146389-code applied. * Integrate the button with the add connection modal * Fix naming * Filter services from the backend * Clean up * More cleanup * Move constants logic to hook * Create a shared ConnectForm component * Use the updated hook and form component * Add mastodon validation * Add changelog * OK Phan, here you go * No phan, not OK * Fix notices z-index when a modal is open * Fix up versions * Return only the ones that are present in the available services. * Always return an empty array for connections --------- Co-authored-by: Manzoor Wani --- pnpm-lock.yaml | 3 + .../changelog/add-social-connect-button | 4 + .../global-notices/styles.module.scss | 2 + .../changelog/add-social-connect-button | 4 + .../publicize-components/package.json | 1 + .../add-connection-modal/connect-form.tsx | 109 ++++++++++++++++++ .../connect-page/connect-page.tsx | 48 +++++--- .../connect-page/style.module.scss | 14 --- .../components/add-connection-modal/index.tsx | 37 ++++-- .../add-connection-modal/style.module.scss | 14 +++ ...nstants.tsx => use-supported-services.tsx} | 54 ++++++--- .../connection-management/index.tsx | 11 +- .../social-store/selectors/connection-data.js | 11 ++ .../src/social-store/types.ts | 9 ++ .../publicize-components/src/types/types.ts | 1 + .../publicize-components/src/utils/index.js | 1 + .../src/utils/request-external-access.js | 34 ++++++ .../changelog/add-social-connect-button | 4 + .../publicize/src/class-publicize.php | 7 +- .../class-settings.php | 31 +++++ .../changelog/add-social-connect-button | 4 + .../social/src/class-jetpack-social.php | 4 - 22 files changed, 336 insertions(+), 71 deletions(-) create mode 100644 projects/js-packages/components/changelog/add-social-connect-button create mode 100644 projects/js-packages/publicize-components/changelog/add-social-connect-button create mode 100644 projects/js-packages/publicize-components/src/components/add-connection-modal/connect-form.tsx rename projects/js-packages/publicize-components/src/components/add-connection-modal/{constants.tsx => use-supported-services.tsx} (87%) create mode 100644 projects/js-packages/publicize-components/src/utils/request-external-access.js create mode 100644 projects/packages/publicize/changelog/add-social-connect-button create mode 100644 projects/plugins/social/changelog/add-social-connect-button diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c83f75a32869c..8552b9506d76c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -888,6 +888,9 @@ importers: '@automattic/jetpack-shared-extension-utils': specifier: workspace:* version: link:../shared-extension-utils + '@automattic/popup-monitor': + specifier: 1.0.2 + version: 1.0.2 '@automattic/social-previews': specifier: 2.0.1-beta.13 version: 2.0.1-beta.13(@babel/runtime@7.24.0)(@types/react@18.3.1)(react-dom@18.2.0)(react@18.2.0) diff --git a/projects/js-packages/components/changelog/add-social-connect-button b/projects/js-packages/components/changelog/add-social-connect-button new file mode 100644 index 0000000000000..979138de2705f --- /dev/null +++ b/projects/js-packages/components/changelog/add-social-connect-button @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fixed notices z-index for global notices when modal is open diff --git a/projects/js-packages/components/components/global-notices/styles.module.scss b/projects/js-packages/components/components/global-notices/styles.module.scss index 730d1391d3229..98c297670d5d8 100644 --- a/projects/js-packages/components/components/global-notices/styles.module.scss +++ b/projects/js-packages/components/components/global-notices/styles.module.scss @@ -6,6 +6,8 @@ inset-block-start: auto; // top inset-block-end: 0; // bottom inset-inline: 0; // left and right + // Modals have 100000, so this needs to be above them + z-index: 100001; @include break-small { width: auto; diff --git a/projects/js-packages/publicize-components/changelog/add-social-connect-button b/projects/js-packages/publicize-components/changelog/add-social-connect-button new file mode 100644 index 0000000000000..766e0ff8c9e9d --- /dev/null +++ b/projects/js-packages/publicize-components/changelog/add-social-connect-button @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Add connect form/button for connection management diff --git a/projects/js-packages/publicize-components/package.json b/projects/js-packages/publicize-components/package.json index ba5e622ec4be4..3f876c970245d 100644 --- a/projects/js-packages/publicize-components/package.json +++ b/projects/js-packages/publicize-components/package.json @@ -22,6 +22,7 @@ "@automattic/jetpack-components": "workspace:*", "@automattic/jetpack-connection": "workspace:*", "@automattic/jetpack-shared-extension-utils": "workspace:*", + "@automattic/popup-monitor": "1.0.2", "@automattic/social-previews": "2.0.1-beta.13", "@wordpress/annotations": "2.56.0", "@wordpress/api-fetch": "6.53.0", diff --git a/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-form.tsx b/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-form.tsx new file mode 100644 index 0000000000000..51f699a14d396 --- /dev/null +++ b/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-form.tsx @@ -0,0 +1,109 @@ +import { Button, useGlobalNotices } from '@automattic/jetpack-components'; +import { __ } from '@wordpress/i18n'; +import classNames from 'classnames'; +import { useCallback } from 'react'; +import { requestExternalAccess } from '../../utils'; +import styles from './style.module.scss'; +import type { SupportedService } from './use-supported-services'; + +type ConnectFormProps = { + service: SupportedService; + isSmall?: boolean; + onConfirm: ( data: unknown ) => void; + onSubmit?: VoidFunction; + displayInputs?: boolean; + isMastodonAlreadyConnected?: ( username: string ) => boolean; +}; + +const isValidMastodonUsername = ( username: string ) => + /^@?\b([A-Z0-9_]+)@([A-Z0-9.-]+\.[A-Z]{2,})$/gi.test( username ); + +/** + * Connect form component + * + * @param {ConnectFormProps} props - Component props + * + * @returns {import('react').ReactNode} Connect form component + */ +export function ConnectForm( { + service, + isSmall, + onConfirm, + onSubmit, + displayInputs, + isMastodonAlreadyConnected, +}: ConnectFormProps ) { + const { createErrorNotice } = useGlobalNotices(); + + const onSubmitForm = useCallback( + ( event: React.FormEvent ) => { + event.preventDefault(); + // Prevent Jetpack settings from being submitted + event.stopPropagation(); + + if ( onSubmit ) { + return onSubmit(); + } + const formData = new FormData( event.target as HTMLFormElement ); + const url = new URL( service.connect_URL ); + + switch ( service.ID ) { + case 'mastodon': { + const instance = formData.get( 'instance' ).toString().trim(); + + if ( ! isValidMastodonUsername( instance ) ) { + createErrorNotice( __( 'Invalid Mastodon username', 'jetpack' ) ); + + return; + } + + if ( isMastodonAlreadyConnected?.( instance ) ) { + createErrorNotice( __( 'This Mastodon account is already connected', 'jetpack' ) ); + + return; + } + + url.searchParams.set( 'instance', formData.get( 'instance' ) as string ); + break; + } + + default: + break; + } + + requestExternalAccess( url.toString(), onConfirm ); + }, + [ + createErrorNotice, + isMastodonAlreadyConnected, + onConfirm, + onSubmit, + service.ID, + service.connect_URL, + ] + ); + + return ( +
+ { displayInputs ? ( + <> + { 'mastodon' === service.ID ? ( + + ) : null } + + ) : null } + +
+ ); +} diff --git a/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-page/connect-page.tsx b/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-page/connect-page.tsx index 4b2423d91e25c..45d7d3de8f64a 100644 --- a/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-page/connect-page.tsx +++ b/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-page/connect-page.tsx @@ -1,17 +1,39 @@ import { Button, useBreakpointMatch } from '@automattic/jetpack-components'; +import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import classNames from 'classnames'; -import { SupportedService } from '../constants'; +import { useCallback } from 'react'; +import { store as socialStore } from '../../../social-store'; +import { ConnectForm } from '../connect-form'; import styles from './style.module.scss'; +import type { SupportedService } from '../use-supported-services'; type ConnectPageProps = { service: SupportedService; onBackClicked: VoidFunction; + onConfirm: ( data: unknown ) => void; }; -export const ConnectPage: React.FC< ConnectPageProps > = ( { service, onBackClicked } ) => { +export const ConnectPage: React.FC< ConnectPageProps > = ( { + service, + onBackClicked, + onConfirm, +} ) => { const [ isSmall ] = useBreakpointMatch( 'sm' ); + const connections = useSelect( select => { + return select( socialStore ).getConnections(); + }, [] ); + + const isMastodonAlreadyConnected = useCallback( + ( username: string ) => { + return connections.some( connection => { + return connection.service_name === 'mastodon' && connection.external_display === username; + } ); + }, + [ connections ] + ); + return ( <>
= ( { service, onBackClic } ) } > { service.examples.map( ( Example, idx ) => ( -
+
) ) } @@ -33,19 +55,13 @@ export const ConnectPage: React.FC< ConnectPageProps > = ( { service, onBackClic > { __( 'Back', 'jetpack' ) } -
- { 'mastodon' === service.name ? ( - - ) : null } - -
+
); diff --git a/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-page/style.module.scss b/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-page/style.module.scss index b07292d0e8970..3596ccfb140e5 100644 --- a/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-page/style.module.scss +++ b/projects/js-packages/publicize-components/src/components/add-connection-modal/connect-page/style.module.scss @@ -37,17 +37,3 @@ .chevron-back { display: block; } - -.connect-form { - display: grid; - gap: 0.5rem; - - & > input { - height: 100%; - width: 18rem; - } - - &:not(.small) { - grid-auto-flow: column; - } -} \ No newline at end of file diff --git a/projects/js-packages/publicize-components/src/components/add-connection-modal/index.tsx b/projects/js-packages/publicize-components/src/components/add-connection-modal/index.tsx index 965d8d007dc6d..43ec87163a9c3 100644 --- a/projects/js-packages/publicize-components/src/components/add-connection-modal/index.tsx +++ b/projects/js-packages/publicize-components/src/components/add-connection-modal/index.tsx @@ -1,12 +1,13 @@ import { Button, useBreakpointMatch } from '@automattic/jetpack-components'; import { Modal } from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { Icon, chevronDown } from '@wordpress/icons'; import classNames from 'classnames'; -import { useCallback } from 'react'; +import { ConnectForm } from './connect-form'; import { ConnectPage } from './connect-page/connect-page'; -import { SupportedService, getSupportedServices } from './constants'; import styles from './style.module.scss'; +import { SupportedService, useSupportedServices } from './use-supported-services'; type AddConnectionModalProps = { onCloseModal: VoidFunction; @@ -19,6 +20,8 @@ const AddConnectionModal = ( { currentService, setCurrentService, }: AddConnectionModalProps ) => { + const supportedServices = useSupportedServices(); + const [ isSmall ] = useBreakpointMatch( 'sm' ); const onServiceSelected = useCallback( @@ -32,6 +35,11 @@ const AddConnectionModal = ( { setCurrentService( null ); }, [ setCurrentService ] ); + const onConfirm = useCallback( ( data: unknown ) => { + // eslint-disable-next-line no-console + console.log( data ); + }, [] ); + return ( { currentService ? ( - + ) : ( @@ -61,8 +73,8 @@ const AddConnectionModal = ( { - { getSupportedServices().map( service => ( - + { supportedServices.map( service => ( + @@ -71,16 +83,21 @@ const AddConnectionModal = ( { [ styles.small ]: ! isSmall, } ) } > -

{ service.title }

+

{ service.label }

{ ! isSmall ? (

{ service.description }

) : null }
- +