Skip to content

Commit

Permalink
Social: Create a social service connect button (#37196)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
pablinos and manzoorwanijk authored May 13, 2024
1 parent 2d24fc4 commit 839954e
Show file tree
Hide file tree
Showing 22 changed files with 336 additions and 71 deletions.
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Fixed notices z-index for global notices when modal is open
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Add connect form/button for connection management
1 change: 1 addition & 0 deletions projects/js-packages/publicize-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<form
className={ classNames( styles[ 'connect-form' ], { [ styles.small ]: isSmall } ) }
onSubmit={ onSubmitForm }
>
{ displayInputs ? (
<>
{ 'mastodon' === service.ID ? (
<input
required
type="text"
name="instance"
aria-label={ __( 'Mastodon username', 'jetpack' ) }
placeholder={ '@[email protected]' }
/>
) : null }
</>
) : null }
<Button variant="primary" type="submit" size={ isSmall ? 'small' : 'normal' }>
{ __( 'Connect', 'jetpack' ) }
</Button>
</form>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div
Expand All @@ -20,7 +42,7 @@ export const ConnectPage: React.FC< ConnectPageProps > = ( { service, onBackClic
} ) }
>
{ service.examples.map( ( Example, idx ) => (
<div key={ service.name + idx } className={ styles.example }>
<div key={ service.ID + idx } className={ styles.example }>
<Example />
</div>
) ) }
Expand All @@ -33,19 +55,13 @@ export const ConnectPage: React.FC< ConnectPageProps > = ( { service, onBackClic
>
{ __( 'Back', 'jetpack' ) }
</Button>
<form className={ classNames( styles[ 'connect-form' ], { [ styles.small ]: isSmall } ) }>
{ 'mastodon' === service.name ? (
<input
required
type="text"
aria-label={ __( 'Mastodon username', 'jetpack' ) }
placeholder={ '@[email protected]' }
/>
) : null }
<Button type="submit" variant="primary">
{ __( 'Connect', 'jetpack' ) }
</Button>
</form>
<ConnectForm
service={ service }
isSmall={ isSmall }
onConfirm={ onConfirm }
displayInputs
isMastodonAlreadyConnected={ isMastodonAlreadyConnected }
/>
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,6 +20,8 @@ const AddConnectionModal = ( {
currentService,
setCurrentService,
}: AddConnectionModalProps ) => {
const supportedServices = useSupportedServices();

const [ isSmall ] = useBreakpointMatch( 'sm' );

const onServiceSelected = useCallback(
Expand All @@ -32,6 +35,11 @@ const AddConnectionModal = ( {
setCurrentService( null );
}, [ setCurrentService ] );

const onConfirm = useCallback( ( data: unknown ) => {
// eslint-disable-next-line no-console
console.log( data );
}, [] );

return (
<Modal
className={ classNames( styles.modal, {
Expand All @@ -44,13 +52,17 @@ const AddConnectionModal = ( {
? sprintf(
// translators: %s: Name of the service the user connects to.
__( 'Connecting a new %s account', 'jetpack' ),
currentService.title
currentService.label
)
: __( 'Add a new connection to Jetpack Social', 'jetpack' )
}
>
{ currentService ? (
<ConnectPage service={ currentService } onBackClicked={ onBackClicked } />
<ConnectPage
service={ currentService }
onBackClicked={ onBackClicked }
onConfirm={ onConfirm }
/>
) : (
<table>
<thead>
Expand All @@ -61,8 +73,8 @@ const AddConnectionModal = ( {
</tr>
</thead>
<tbody>
{ getSupportedServices().map( service => (
<tr key={ service.name }>
{ supportedServices.map( service => (
<tr key={ service.ID }>
<td>
<service.icon iconSize={ isSmall ? 36 : 48 } />
</td>
Expand All @@ -71,16 +83,21 @@ const AddConnectionModal = ( {
[ styles.small ]: ! isSmall,
} ) }
>
<h2 className={ styles.title }>{ service.title }</h2>
<h2 className={ styles.title }>{ service.label }</h2>
{ ! isSmall ? (
<p className={ styles.description }>{ service.description }</p>
) : null }
</td>
<td>
<div className={ styles[ 'column-actions' ] }>
<Button type="submit" variant="primary" size={ isSmall ? 'small' : 'normal' }>
{ __( 'Connect', 'jetpack' ) }
</Button>
<ConnectForm
service={ service }
isSmall={ isSmall }
onConfirm={ onConfirm }
onSubmit={
service.needsCustomInputs ? onServiceSelected( service ) : undefined
}
/>
<Button
size={ isSmall ? 'small' : 'normal' }
className={ styles[ 'chevron-button' ] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,17 @@ tr:not(:last-child) > td {
padding-inline-end: 0.5rem;
margin-inline-start: 0.4rem;
}

.connect-form {
display: grid;
gap: 0.5rem;

& > input {
height: 100%;
width: 18rem;
}

&:not(.small) {
grid-auto-flow: column;
}
}
Loading

0 comments on commit 839954e

Please sign in to comment.