Add: mobile app welcome modal and magic link (#34637)

This commit is contained in:
RJ 2022-09-15 11:58:47 +08:00 committed by GitHub
parent a0768a086c
commit 6e2ada3706
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1116 additions and 5 deletions

View File

@ -0,0 +1,6 @@
Significance: patch
Type: fix
Export StepperProps for external usage

View File

@ -45,7 +45,7 @@ export { MenuItem as __experimentalSelectControlMenuItem } from './experimental-
export { default as ScrollTo } from './scroll-to';
export { Sortable } from './sortable';
export { default as Spinner } from './spinner';
export { default as Stepper } from './stepper';
export { default as Stepper, StepperProps } from './stepper';
export { default as SummaryList } from './summary';
export { default as SummaryListPlaceholder } from './summary/placeholder';
export { SummaryNumberPlaceholder } from './summary/placeholder';

View File

@ -11,7 +11,7 @@ import type React from 'react';
import Spinner from '../spinner';
import CheckIcon from './check-icon';
interface StepperProps {
export interface StepperProps {
/** Additional class name to style the component. */
className?: string;
/** The current step's key. */

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Added code to retrieve jetpack connection data

View File

@ -5,6 +5,7 @@ export enum ACTION_TYPES {
SET_ERROR = 'SET_ERROR',
UPDATE_JETPACK_CONNECTION = 'UPDATE_JETPACK_CONNECTION',
UPDATE_JETPACK_CONNECT_URL = 'UPDATE_JETPACK_CONNECT_URL',
UPDATE_JETPACK_CONNECTION_DATA = 'UPDATE_JETPACK_CONNECTION_DATA',
SET_PAYPAL_ONBOARDING_STATUS = 'SET_PAYPAL_ONBOARDING_STATUS',
SET_RECOMMENDED_PLUGINS = 'SET_RECOMMENDED_PLUGINS',
}

View File

@ -25,6 +25,7 @@ import {
ActivatePluginsResponse,
PluginsResponse,
PluginNames,
JetpackConnectionDataResponse,
} from './types';
// Can be removed in WP 5.9, wp.data is supported in >5.7.
@ -116,6 +117,15 @@ export function updateIsJetpackConnected( jetpackConnection: boolean ) {
};
}
export function updateJetpackConnectionData(
results: JetpackConnectionDataResponse
) {
return {
type: TYPES.UPDATE_JETPACK_CONNECTION_DATA as const,
results,
};
}
export function updateJetpackConnectUrl(
redirectUrl: string,
jetpackConnectUrl: string
@ -374,6 +384,7 @@ export type Actions = ReturnType<
| typeof setError
| typeof updateIsJetpackConnected
| typeof updateJetpackConnectUrl
| typeof updateJetpackConnectionData
| typeof setPaypalOnboardingStatus
| typeof setRecommendedPlugins
| typeof createErrorNotice

View File

@ -94,6 +94,12 @@ const reducer: Reducer< PluginsState, Actions > = (
jetpackConnection: payload.jetpackConnection,
};
break;
case TYPES.UPDATE_JETPACK_CONNECTION_DATA:
state = {
...state,
jetpackConnectionData: payload.results,
};
break;
case TYPES.UPDATE_JETPACK_CONNECT_URL:
state = {
...state,

View File

@ -18,10 +18,15 @@ import {
updateInstalledPlugins,
updateIsJetpackConnected,
updateJetpackConnectUrl,
updateJetpackConnectionData,
setPaypalOnboardingStatus,
setRecommendedPlugins,
} from './actions';
import { PaypalOnboardingStatus, RecommendedTypes } from './types';
import {
PaypalOnboardingStatus,
RecommendedTypes,
JetpackConnectionDataResponse,
} from './types';
// Can be removed in WP 5.9, wp.data is supported in >5.7.
const resolveSelect =
@ -89,6 +94,25 @@ export function* isJetpackConnected() {
yield setIsRequesting( 'isJetpackConnected', false );
}
export function* getJetpackConnectionData() {
yield setIsRequesting( 'getJetpackConnectionData', true );
try {
const url = JETPACK_NAMESPACE + '/connection/data';
const results: JetpackConnectionDataResponse = yield apiFetch( {
path: url,
method: 'GET',
} );
yield updateJetpackConnectionData( results );
} catch ( error ) {
yield setError( 'getJetpackConnectionData', error );
}
yield setIsRequesting( 'getJetpackConnectionData', false );
}
export function* getJetpackConnectUrl( query: { redirect_url: string } ) {
yield setIsRequesting( 'getJetpackConnectUrl', true );

View File

@ -33,6 +33,9 @@ export const getPluginsError = (
export const isJetpackConnected = ( state: PluginsState ) =>
state.jetpackConnection;
export const getJetpackConnectionData = ( state: PluginsState ) =>
state.jetpackConnectionData;
export const getJetpackConnectUrl = (
state: PluginsState,
query: { redirect_url: string }
@ -68,6 +71,7 @@ export type PluginSelectors = {
getActivePlugins: WPDataSelector< typeof getActivePlugins >;
getInstalledPlugins: WPDataSelector< typeof getInstalledPlugins >;
getRecommendedPlugins: WPDataSelector< typeof getRecommendedPlugins >;
getJetpackConnectionData: WPDataSelector< typeof getJetpackConnectionData >;
isJetpackConnected: WPDataSelector< typeof isJetpackConnected >;
isPluginsRequesting: WPDataSelector< typeof isPluginsRequesting >;
} & WPDataSelectors;

View File

@ -15,6 +15,7 @@ export type SelectorKeysWithActions =
| 'installPlugins'
| 'activatePlugins'
| 'isJetpackConnected'
| 'getJetpackConnectionData'
| 'getJetpackConnectUrl'
| 'getPaypalOnboardingStatus';
@ -24,6 +25,7 @@ export type PluginsState = {
requesting: Partial< Record< SelectorKeysWithActions, boolean > >;
jetpackConnectUrls: Record< string, unknown >;
jetpackConnection?: boolean;
jetpackConnectionData?: JetpackConnectionDataResponse;
recommended: Partial< Record< RecommendedTypes, Plugin[] > >;
paypalOnboardingStatus?: Partial< PaypalOnboardingStatus >;
// TODO clarify what the error record's type is
@ -80,3 +82,40 @@ export type ActivatePluginsResponse = PluginsResponse< {
activated: string[];
active: string[];
} >;
export type JetpackConnectionDataResponse = {
currentUser: {
isConnected: boolean;
isMaster: boolean;
username: string;
id: number;
wpcomUser: {
ID: number;
login: string;
email: string;
display_name: string;
text_direction: string;
site_count: number;
jetpack_connect: string;
color_scheme: string;
sidebar_collapsed: boolean;
user_locale: string;
avatar: string;
};
gravatar: string;
permissions: {
connect: boolean;
connect_user: boolean;
disconnect: boolean;
admin_page: boolean;
manage_modules: boolean;
network_admin: boolean;
network_sites_page: boolean;
edit_posts: boolean;
publish_posts: boolean;
manage_options: boolean;
view_stats: boolean;
manage_plugins: boolean;
};
};
} & Response;

View File

@ -16,6 +16,7 @@ import {
} from '@woocommerce/data';
import { compose } from 'redux';
import { recordEvent } from '@woocommerce/tracks';
import { getAdminLink } from '@woocommerce/settings';
/**
* Internal dependencies
@ -36,6 +37,13 @@ function getHomeItems() {
title: __( 'Home Screen', 'woocommerce' ),
link: 'https://woocommerce.com/document/home-screen/?utm_medium=product',
},
{
title: __( 'Get the WooCommerce app', 'woocommerce' ),
link: getAdminLink(
'./admin.php?page=wc-admin&mobileAppModal=true'
),
linkType: 'wc-admin',
},
{
title: __( 'Inbox', 'woocommerce' ),
link: 'https://woocommerce.com/document/home-screen/?utm_medium=product#section-2',
@ -342,8 +350,8 @@ function getListItems( props ) {
),
before: <Icon icon={ page } />,
after: <Icon icon={ chevronRight } />,
linkType: 'external',
target: '_blank',
linkType: item.linkType ?? 'external',
target: item.target ?? '_blank',
href: item.link,
onClick,
} ) );

View File

@ -38,6 +38,7 @@ import {
} from './constants';
import { WelcomeFromCalypsoModal } from './welcome-from-calypso-modal';
import { WelcomeModal } from './welcome-modal';
import { MobileAppModal } from './mobile-app-modal';
import './style.scss';
import '../dashboard/style.scss';
import { getAdminSetting } from '~/utils/admin-settings';
@ -93,6 +94,7 @@ export const Layout = ( {
}, [ maybeToggleColumns ] );
const shouldStickColumns = isWideViewport.current && twoColumns;
const shouldShowMobileAppModal = query.mobileAppModal ?? false;
const renderColumns = () => {
return (
@ -158,6 +160,7 @@ export const Layout = ( {
} }
/>
) }
{ shouldShowMobileAppModal && <MobileAppModal /> }
{ window.wcAdminFeatures.navigation && (
<NavigationIntroModal />
) }

View File

@ -0,0 +1,140 @@
/**
* External dependencies
*/
import React, { useState, useEffect } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { sprintf, __ } from '@wordpress/i18n';
import { Spinner, Stepper, StepperProps } from '@woocommerce/components';
/**
* Internal dependencies
*/
import {
SendMagicLinkButton,
useJetpackPluginState,
JetpackPluginStates,
} from './';
export const JetpackInstallationStepper = ( {
step,
sendMagicLinkHandler,
}: {
step: 'first' | 'second';
sendMagicLinkHandler: () => void;
} ) => {
const { installHandler, jetpackConnectionData, state } =
useJetpackPluginState();
const [ isWaitingForRedirect, setIsWaitingForRedirect ] = useState( false );
const [ stepsToDisplay, setStepsToDisplay ] = useState<
StepperProps[ 'steps' ] | undefined
>( undefined );
// we need to generate one set of steps for the first step, and another set for the second step
// because the texts are different after progressing from the first step to the second step
useEffect( () => {
const isInstalling =
state === JetpackPluginStates.INSTALLING || isWaitingForRedirect;
if ( step === 'first' ) {
setStepsToDisplay( [
{
key: 'first',
label: __( 'Connect to Jetpack', 'woocommerce' ),
description: __(
'To get started, install Jetpack - our free tool required to sync your store with the WooCommerce mobile app',
'woocommerce'
),
content: (
<>
<Button
className="install-jetpack-button"
onClick={ () => {
setIsWaitingForRedirect( true );
installHandler();
} }
>
{ isInstalling && (
<Spinner className="install-jetpack-spinner" />
) }
<div
style={ {
visibility: isInstalling
? 'hidden'
: 'visible',
} }
className="install-jetpack-button-contents"
>
<div className="jetpack-icon" />
<div className="install-jetpack-button-text">
{ __(
'Install and Connect',
'woocommerce'
) }
</div>
</div>
</Button>
</>
),
},
{
key: 'second',
label: __( 'Sign into the app', 'woocommerce' ),
description: '',
content: <></>,
},
] );
} else if ( step === 'second' ) {
// this step is shown on the return callback from the WordPress.com user connection
const wordpressAccountEmailAddress =
jetpackConnectionData?.currentUser.wpcomUser.email;
setStepsToDisplay( [
{
key: 'first',
label: sprintf(
/* translators: Reflecting to the user what their WordPress account email address is */
__( 'Connected as %s', 'woocommerce' ),
wordpressAccountEmailAddress
),
description: '',
content: <></>,
},
{
key: 'second',
label: 'Sign into the app',
description: sprintf(
/* translators: Reflecting to the user that the magic link has been sent to their WordPress account email address */
__(
'Well send a magic link to %s. Open it on your smartphone or tablet to sign into your store instantly.',
'woocommerce'
),
wordpressAccountEmailAddress
),
content: (
<SendMagicLinkButton
onClickHandler={ sendMagicLinkHandler }
/>
),
},
] );
}
}, [
step,
installHandler,
state,
isWaitingForRedirect,
jetpackConnectionData?.currentUser.wpcomUser.email,
sendMagicLinkHandler,
] );
return (
<div className="jetpack-stepper-wrapper">
{ stepsToDisplay && (
<Stepper
isVertical={ true }
currentStep={ step }
steps={ stepsToDisplay }
/>
) }
</div>
);
};

View File

@ -0,0 +1,15 @@
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
export const SendMagicLinkButton = ( {
onClickHandler,
}: {
onClickHandler: () => void;
} ) => (
<Button className="send-magic-link-button" onClick={ onClickHandler }>
{ __( '✨️ Send the sign-in link', 'woocommerce' ) }
</Button>
);

View File

@ -0,0 +1,7 @@
export { JetpackInstallationStepper } from './JetpackInstallationStepper';
export {
useJetpackPluginState,
JetpackPluginStates,
} from './useJetpackPluginState';
export { useSendMagicLink } from './useSendMagicLink';
export { SendMagicLinkButton } from './SendMagicLinkButton';

View File

@ -0,0 +1,102 @@
/**
* External dependencies
*/
import { useState, useEffect, useCallback } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { PLUGINS_STORE_NAME, useUser } from '@woocommerce/data';
import { createErrorNotice } from '@woocommerce/data/src/plugins/actions';
export const JetpackPluginStates = {
/** Jetpack plugin is not installed, can use installHandler() to install */
NOT_INSTALLED: 'not-installed',
/** Installing Jetpack plugin on the WordPress installation */
INSTALLING: 'installing',
/** User doesn't have permissions to install plugins on this site */
USER_CANNOT_INSTALL: 'user-cannot-install',
/** Weird edge case where the plugin is installed but not activated */
NOT_ACTIVATED: 'not-activated',
/** Jetpack plugin is installed but not connected to a WordPress.com user */
USERLESS_CONNECTION: 'userless-connection',
/** Jetpack Plugin installed and WordPress.com user connected */
FULL_CONNECTION: 'full-connection',
/** Still retrieving Jetpack state from Wordpress Installation */
INITIALIZING: 'initializing',
} as const;
export type JetpackPluginStates =
typeof JetpackPluginStates[ keyof typeof JetpackPluginStates ];
/**
* Utility hook to determine and manipulate the state of the Jetpack plugin on the WordPress installation
*/
export const useJetpackPluginState = () => {
const { currentUserCan } = useUser();
const {
canUserInstallPlugins,
jetpackInstallState,
jetpackConnectionData,
} = useSelect( ( select ) => {
const { getPluginInstallState, getJetpackConnectionData } =
select( PLUGINS_STORE_NAME );
const installState = getPluginInstallState( 'jetpack' );
return {
jetpackConnectionData: getJetpackConnectionData(),
jetpackInstallState: installState,
canUserInstallPlugins: currentUserCan( 'install_plugins' ),
};
} );
const { installJetpackAndConnect } = useDispatch( PLUGINS_STORE_NAME );
const [ pluginState, setPluginState ] = useState< JetpackPluginStates >(
JetpackPluginStates.INITIALIZING
);
/**
* Installs, Activates, and Connects Jetpack - starting wherever hasn't been completed
*/
const onClickInstall = useCallback( () => {
const thisUrl = window.location.href;
installJetpackAndConnect(
createErrorNotice,
() => thisUrl + '&jetpackState=returning'
);
setPluginState( JetpackPluginStates.INSTALLING );
}, [ installJetpackAndConnect ] );
useEffect( () => {
if ( ! canUserInstallPlugins ) {
setPluginState( JetpackPluginStates.USER_CANNOT_INSTALL );
}
if ( jetpackInstallState === 'unavailable' ) {
setPluginState( JetpackPluginStates.NOT_INSTALLED );
}
if ( jetpackInstallState === 'installed' ) {
setPluginState( JetpackPluginStates.NOT_ACTIVATED );
}
if ( jetpackInstallState === 'activated' ) {
if (
// Jetpack can be installed and activated but not connected to a WordPress.com user account, this handles that
jetpackConnectionData &&
! jetpackConnectionData.currentUser.isConnected
) {
setPluginState( JetpackPluginStates.USERLESS_CONNECTION );
} else if (
jetpackConnectionData &&
jetpackConnectionData.currentUser.isConnected
) {
setPluginState( JetpackPluginStates.FULL_CONNECTION );
}
}
}, [ canUserInstallPlugins, jetpackInstallState, jetpackConnectionData ] );
return {
state: pluginState,
installHandler: onClickInstall,
jetpackConnectionData,
};
};

View File

@ -0,0 +1,82 @@
/**
* External dependencies
*/
import { useState, useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { WC_ADMIN_NAMESPACE } from '@woocommerce/data';
import { useDispatch } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
const SendMagicLinkStates = {
INIT: 'initializing',
FETCHING: 'fetching',
SUCCESS: 'success',
ERROR: 'error',
} as const;
type SendMagicLinkStates =
typeof SendMagicLinkStates[ keyof typeof SendMagicLinkStates ];
export type MagicLinkResponse = {
data: unknown;
code: string;
message: string;
} & Response;
export const sendMagicLink = () => {
return apiFetch< MagicLinkResponse >( {
path: `${ WC_ADMIN_NAMESPACE }/mobile-app/send-magic-link`,
} );
};
export const useSendMagicLink = () => {
const [ requestState, setRequestState ] = useState< SendMagicLinkStates >(
SendMagicLinkStates.INIT
);
const { createNotice } = useDispatch( 'core/notices' );
const fetchMagicLinkApiCall = useCallback( () => {
setRequestState( SendMagicLinkStates.FETCHING );
sendMagicLink()
.then( ( response ) => {
if ( response.code === 'success' ) {
setRequestState( SendMagicLinkStates.SUCCESS );
} else {
setRequestState( SendMagicLinkStates.ERROR );
createNotice(
'error',
__( 'Sorry, an unknown error occured.', 'woocommerce' )
);
}
} )
.catch( ( response ) => {
setRequestState( SendMagicLinkStates.ERROR );
if ( response.code === 'error_sending_mobile_magic_link' ) {
createNotice(
'error',
__(
'Sorry, there was an error trying to request for a magic link',
'woocommerce'
)
);
} else if (
response.code === 'invalid_user_permission_view_admin'
) {
createNotice(
'error',
__(
"Sorry, your account doesn't have sufficient permission.",
'woocommerce'
)
);
} else if ( response.code === 'jetpack_not_connected' ) {
createNotice( 'error', response.message );
} else {
createNotice( 'error', response.message );
}
} );
}, [ createNotice ] );
return { requestState, fetchMagicLinkApiCall };
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,124 @@
/**
* External dependencies
*/
import { useState, useEffect, useCallback } from '@wordpress/element';
import { Guide } from '@wordpress/components';
import { useSearchParams } from 'react-router-dom';
import { updateQueryString } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { ModalIllustrationLayout } from './layouts/ModalIllustrationLayout';
import {
useJetpackPluginState,
JetpackPluginStates,
useSendMagicLink,
} from './components';
import {
EmailSentPage,
JetpackInstallStepperPage,
JetpackAlreadyInstalledPage,
} from './pages';
import './style.scss';
export const MobileAppModal = () => {
const [ guideIsOpen, setGuideIsOpen ] = useState( true );
const { state, jetpackConnectionData } = useJetpackPluginState();
const [ pageContent, setPageContent ] = useState< React.ReactNode >();
const [ searchParams ] = useSearchParams();
useEffect( () => {
if ( searchParams.get( 'mobileAppModal' ) ) {
setGuideIsOpen( true );
} else {
setGuideIsOpen( false );
}
}, [ searchParams ] );
const isReturningFromWordpressConnection =
searchParams.get( 'jetpackState' ) === 'returning';
const [ hasSentEmail, setHasSentEmail ] = useState( false );
const { fetchMagicLinkApiCall } = useSendMagicLink();
const sendMagicLink = useCallback( () => {
fetchMagicLinkApiCall();
setHasSentEmail( true );
}, [ fetchMagicLinkApiCall ] );
useEffect( () => {
if ( hasSentEmail ) {
setPageContent(
<EmailSentPage
hasSentEmailHandler={ () => setHasSentEmail( false ) }
/>
);
} else if (
state === JetpackPluginStates.NOT_INSTALLED ||
state === JetpackPluginStates.NOT_ACTIVATED ||
state === JetpackPluginStates.USERLESS_CONNECTION ||
isReturningFromWordpressConnection
) {
setPageContent(
<JetpackInstallStepperPage
isReturningFromWordpressConnection={
isReturningFromWordpressConnection
}
sendMagicLinkHandler={ sendMagicLink }
/>
);
} else if (
state === JetpackPluginStates.FULL_CONNECTION &&
! hasSentEmail
) {
const wordpressAccountEmailAddress =
jetpackConnectionData?.currentUser.wpcomUser.email;
setPageContent(
<JetpackAlreadyInstalledPage
wordpressAccountEmailAddress={
wordpressAccountEmailAddress
}
sendMagicLinkHandler={ sendMagicLink }
/>
);
}
}, [
sendMagicLink,
hasSentEmail,
isReturningFromWordpressConnection,
jetpackConnectionData?.currentUser.wpcomUser.email,
state,
] );
return (
<>
{ guideIsOpen && (
<Guide
onFinish={ () => {
// clear the search params that we use so that the URL is clean
updateQueryString(
{
jetpackState: undefined,
mobileAppModal: undefined,
},
undefined,
Object.fromEntries( searchParams.entries() )
);
} }
className={ 'woocommerce__mobile-app-welcome-modal' }
pages={ [
{
content: (
<ModalIllustrationLayout body={ pageContent } />
),
},
] }
/>
) }
</>
);
};

View File

@ -0,0 +1,39 @@
/**
* External dependencies
*/
import React from 'react';
import { __ } from '@wordpress/i18n';
export const ModalContentLayoutWithTitle = ( {
children,
}: {
children: React.ReactNode;
} ) => (
<div className="jetpack-installation-content">
<div className="modal-layout-header">
<div className="woo-icon"></div>
<div className="modal-header">
<h1>
{ __(
'Manage orders and track sales in real-time with the free mobile app',
'woocommerce'
) }
</h1>
</div>
</div>
<div className="modal-layout-body">{ children }</div>
<div className="modal-layout-footer">
<div className="mobile-footer-icons">
<div className="apple-icon"></div>
<div className="android-icon"></div>
</div>
<div className="mobile-footer-blurb">
{ __(
'The WooCommerce Mobile App is available on iOS and Android',
'woocommerce'
) }
</div>
</div>
</div>
);

View File

@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import Illustration from '../illustrations/intro-devices-desktop.png';
export const ModalIllustrationLayout = ( {
body,
}: {
body: React.ReactNode;
} ) => {
return (
<div className="mobile-app-modal-layout">
<div className="mobile-app-modal-content">{ body }</div>
<div className="mobile-app-modal-illustration">
<img
src={ Illustration }
alt={ __(
'Screen captures of the WooCommerce mobile app',
'woocommerce'
) }
/>
</div>
</div>
);
};

View File

@ -0,0 +1,56 @@
/**
* External dependencies
*/
import interpolateComponents from '@automattic/interpolate-components';
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
interface EmailSentProps {
hasSentEmailHandler: () => void;
}
export const EmailSentPage: React.FC< EmailSentProps > = ( {
hasSentEmailHandler,
} ) => {
return (
<div className="email-sent-modal-body">
<div className="email-sent-illustration"></div>
<div className="email-sent-title">
<h1>{ __( 'Check your email!', 'woocommerce' ) }</h1>
</div>
<div className="email-sent-subheader-spacer">
<div className="email-sent-subheader">
{ __(
'We just sent you the magic link. Open it on your mobile device and follow the instructions.',
'woocommerce'
) }
</div>
</div>
<div className="email-sent-footer">
<div className="email-sent-footer-prompt">
{ __( 'DIDNT GET IT?', 'woocommerce' ) }
</div>
<div className="email-sent-footer-text">
{ interpolateComponents( {
mixedString: __(
'Check your spam/junk email folder or {{ sendAnotherLink /}}.',
'woocommerce'
),
components: {
sendAnotherLink: (
<Button
className="email-sent-send-another-link"
onClick={ () => {
hasSentEmailHandler();
} }
>
{ __( 'send another link', 'woocommerce' ) }
</Button>
),
},
} ) }
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,39 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { ModalContentLayoutWithTitle } from '../layouts/ModalContentLayoutWithTitle';
import { SendMagicLinkButton } from '../components';
interface JetpackAlreadyInstalledPageProps {
wordpressAccountEmailAddress: string | undefined;
sendMagicLinkHandler: () => void;
}
export const JetpackAlreadyInstalledPage: React.FC<
JetpackAlreadyInstalledPageProps
> = ( { wordpressAccountEmailAddress, sendMagicLinkHandler } ) => {
return (
<ModalContentLayoutWithTitle>
<>
<div className="modal-subheader jetpack-already-installed">
<h3>
{ sprintf(
/* translators: %s: user's WordPress.com account email address */
__(
'Well send a magic link to %s. Open it on your smartphone or tablet to sign into your store instantly.',
'woocommerce'
),
wordpressAccountEmailAddress
) }
</h3>
</div>
<SendMagicLinkButton onClickHandler={ sendMagicLinkHandler } />
</>
</ModalContentLayoutWithTitle>
);
};

View File

@ -0,0 +1,36 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { JetpackInstallationStepper } from '../components/JetpackInstallationStepper';
import { ModalContentLayoutWithTitle } from '../layouts/ModalContentLayoutWithTitle';
interface JetpackInstallStepperPageProps {
isReturningFromWordpressConnection: boolean;
sendMagicLinkHandler: () => void;
}
export const JetpackInstallStepperPage: React.FC<
JetpackInstallStepperPageProps
> = ( { isReturningFromWordpressConnection, sendMagicLinkHandler } ) => {
return (
<ModalContentLayoutWithTitle>
<div className="modal-subheader">
<h3>
{ __(
'Run your store from anywhere in two easy steps.',
'woocommerce'
) }
</h3>
</div>
<JetpackInstallationStepper
step={ isReturningFromWordpressConnection ? 'second' : 'first' }
sendMagicLinkHandler={ sendMagicLinkHandler }
/>
</ModalContentLayoutWithTitle>
);
};

View File

@ -0,0 +1,3 @@
export { EmailSentPage } from './EmailSentPage';
export { JetpackAlreadyInstalledPage } from './JetpackAlreadyInstalledPage';
export { JetpackInstallStepperPage } from './JetpackInstallStepperPage';

View File

@ -0,0 +1,226 @@
/* stylelint-disable function-url-quotes */
/* quotes necessary for the data urls in url(), will shift these resources out to their own files when we enable asset modules */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
// The welcome modal is presented outside the homescreen class namespace
.components-guide.woocommerce__mobile-app-welcome-modal {
max-width: 964px;
min-width: 534px;
height: 600px;
.components-modal__header {
height: 0;
.components-button {
color: $white;
}
}
.components-guide__container {
margin-top: 0;
}
.components-guide__page {
min-height: 575px;
.mobile-app-modal-layout {
display: grid;
grid-template-columns: 3fr 2fr;
min-height: 575px;
.mobile-app-modal-content {
display: flex;
.jetpack-installation-content {
animation: fadeIn 0.7s ease-in-out; // the jetpack data takes a moment to load, so we animate it to make it less jarring
padding: 48px;
display: flex;
flex-direction: column;
.modal-layout-header {
.modal-header > h1 {
line-height: normal;
}
}
.modal-layout-body {
flex-grow: 1;
.modal-subheader {
margin-top: -16px;
margin-bottom: 24px;
h3 {
font-weight: 400;
color: #757575;
line-height: 1.6rem;
}
}
.modal-subheader.jetpack-already-installed {
margin-bottom: 48px; // there's a bit of space so space it out
}
}
.modal-layout-footer {
.mobile-footer-icons {
display: flex;
flex-direction: row;
margin-bottom: 16px;
.apple-icon {
width: 24px;
height: 24px;
margin-inline: 4px;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 21 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M19.7928 18.4328C19.4445 19.2374 19.0323 19.978 18.5546 20.659C17.9035 21.5873 17.3704 22.2298 16.9596 22.5866C16.3227 23.1723 15.6404 23.4723 14.9097 23.4893C14.3851 23.4893 13.7525 23.3401 13.0162 23.0373C12.2774 22.7359 11.5984 22.5866 10.9776 22.5866C10.3266 22.5866 9.62829 22.7359 8.8814 23.0373C8.13337 23.3401 7.53077 23.4979 7.07004 23.5135C6.36935 23.5433 5.67093 23.2349 4.97379 22.5866C4.52884 22.1985 3.9723 21.5332 3.30558 20.5907C2.59025 19.5843 2.00215 18.4172 1.54142 17.0866C1.04799 15.6494 0.800637 14.2577 0.800637 12.9103C0.800637 11.3669 1.13414 10.0357 1.80213 8.92023C2.32712 8.02421 3.02553 7.31741 3.89966 6.79854C4.77378 6.27967 5.71827 6.01525 6.7354 5.99834C7.29195 5.99834 8.02178 6.17049 8.92874 6.50882C9.83314 6.84829 10.4139 7.02044 10.6685 7.02044C10.8588 7.02044 11.5039 6.81915 12.5975 6.41784C13.6317 6.04568 14.5046 5.89158 15.2196 5.95228C17.1572 6.10865 18.6129 6.87246 19.581 8.24854C17.8481 9.29851 16.9909 10.7691 17.0079 12.6557C17.0236 14.1252 17.5567 15.348 18.6044 16.3189C19.0792 16.7696 19.6094 17.1178 20.1994 17.3652C20.0714 17.7362 19.9364 18.0916 19.7928 18.4328ZM15.349 0.946082C15.349 2.09785 14.9282 3.17325 14.0895 4.16864C13.0773 5.35195 11.853 6.03572 10.5254 5.92783C10.5085 5.78965 10.4987 5.64422 10.4987 5.49141C10.4987 4.38571 10.9801 3.2024 11.8349 2.23488C12.2616 1.745 12.8044 1.33768 13.4625 1.01275C14.1193 0.692673 14.7405 0.515659 15.3248 0.485352C15.3419 0.639322 15.349 0.793306 15.349 0.946068V0.946082Z' fill='%23BBBBBB'/%3E%3C/svg%3E%0A");
}
.android-icon {
width: 24px;
height: 24px;
margin-inline: 4px;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 20 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M14.83 0.86L13.53 2.16C15.03 3.25 16 5.01 16 7H4C4 5.01 4.97 3.26 6.45 2.17L5.14 0.86C4.94 0.66 4.94 0.35 5.14 0.15C5.34 -0.05 5.65 -0.05 5.85 0.15L7.34 1.63C8.14 1.23 9.04 1 10 1C10.95 1 11.85 1.23 12.64 1.63L14.12 0.15C14.32 -0.05 14.63 -0.05 14.83 0.15C15.03 0.35 15.03 0.66 14.83 0.86ZM0 9.5C0 8.67 0.67 8 1.5 8C2.33 8 3 8.67 3 9.5V16.5C3 17.33 2.33 18 1.5 18C0.67 18 0 17.33 0 16.5V9.5ZM5 19C4.45 19 4 18.55 4 18V8H16V18C16 18.55 15.55 19 15 19H14V22.5C14 23.33 13.33 24 12.5 24C11.67 24 11 23.33 11 22.5V19H9V22.5C9 23.33 8.33 24 7.5 24C6.67 24 6 23.33 6 22.5V19H5ZM18.5 8C17.67 8 17 8.67 17 9.5V16.5C17 17.33 17.67 18 18.5 18C19.33 18 20 17.33 20 16.5V9.5C20 8.67 19.33 8 18.5 8ZM8 5H7V4H8V5ZM12 5H13V4H12V5Z' fill='%23BBBBBB'/%3E%3C/svg%3E%0A");
}
}
.mobile-footer-blurb {
font-weight: 400;
color: #757575;
}
}
}
}
.mobile-app-modal-illustration {
background-color: #674399;
background-repeat: no-repeat;
background-image: url("data:image/svg+xml,%3Csvg width='341' height='421' viewBox='0 0 341 421' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M-258.536 396.856L-329.719 373.412C-338.484 370.525 -343.249 361.08 -340.362 352.315L-271.987 144.717C-269.1 135.952 -259.655 131.187 -250.89 134.074L-179.707 157.519C-69.5553 193.799 49.1512 133.914 85.431 23.7615L99.9575 -20.3434C102.844 -29.1081 112.29 -33.8731 121.054 -30.9864L328.652 37.3885C337.417 40.2752 342.182 49.7205 339.295 58.4852L324.769 102.59C244.953 344.925 -16.2016 476.672 -258.536 396.856Z' fill='%237F54B3'/%3E%3C/svg%3E");
padding-top: 36px;
overflow: clip;
img {
margin-right: -24px;
}
}
}
}
.components-guide__footer {
display: none; // hide the guide component's 'ok, got it' button
}
.fill-theme-color {
fill: var(--wp-admin-theme-color);
}
}
.woo-icon {
height: 48px;
width: 48px;
background-image: url("data:image/svg+xml,%3Csvg width='48' height='48' viewBox='0 0 48 48' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 15.936C0 2.8127 2.8127 0 15.936 0H32.064C45.1873 0 48 2.8127 48 15.936V32.064C48 45.1873 45.1873 48 32.064 48H15.936C2.8127 48 0 45.1873 0 32.064V15.936Z' fill='%237F54B3'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.3042 14.666H37.5304C39.2534 14.666 40.6482 16.0683 40.6482 17.8006V28.2493C40.6482 29.9816 39.2534 31.3839 37.5304 31.3839H27.7667L29.1068 34.6835L23.213 31.3839H10.3179C8.59486 31.3839 7.20005 29.9816 7.20005 28.2493V17.8006C7.18638 16.0821 8.58119 14.666 10.3042 14.666Z' fill='white'/%3E%3Cpath d='M8.86843 17.5121C9.05987 17.2509 9.34704 17.1134 9.72993 17.0859C10.4273 17.0309 10.8239 17.3609 10.9196 18.0758C11.3435 20.9492 11.8085 23.3827 12.3008 25.3762L15.2955 19.6431C15.569 19.1207 15.9109 18.8457 16.3211 18.8182C16.9228 18.777 17.292 19.1619 17.4424 19.9731C17.7843 21.8016 18.2219 23.3552 18.7415 24.675C19.097 21.1829 19.6987 18.667 20.5466 17.1134C20.7517 16.7285 21.0525 16.536 21.4491 16.5085C21.7636 16.481 22.0508 16.5772 22.3106 16.7835C22.5704 16.9897 22.7071 17.2509 22.7345 17.5671C22.7482 17.8146 22.7071 18.0208 22.5978 18.227C22.0644 19.2169 21.6269 20.8805 21.2713 23.1902C20.9294 25.4311 20.8064 27.1772 20.8884 28.4283C20.9158 28.772 20.8611 29.0744 20.7243 29.3357C20.5602 29.6381 20.3141 29.8031 19.9996 29.8306C19.644 29.8581 19.2748 29.6931 18.9193 29.3219C17.6475 28.0158 16.6356 26.0636 15.8972 23.4651C15.0083 25.2249 14.352 26.5448 13.928 27.4247C13.1212 28.9782 12.4375 29.7756 11.8632 29.8169C11.494 29.8443 11.1794 29.5281 10.9059 28.8682C10.2085 27.0672 9.45644 23.5889 8.64963 18.4333C8.60861 18.0758 8.67698 17.7596 8.86843 17.5121Z' fill='%237F54B3'/%3E%3Cpath d='M38.2689 19.6705C37.7766 18.8043 37.0519 18.2819 36.081 18.0757C35.8211 18.0207 35.575 17.9932 35.3425 17.9932C34.0298 17.9932 32.9632 18.6806 32.129 20.0554C31.4179 21.224 31.0624 22.5164 31.0624 23.9324C31.0624 24.991 31.2812 25.8984 31.7188 26.6546C32.211 27.5207 32.9358 28.0432 33.9067 28.2494C34.1665 28.3044 34.4127 28.3319 34.6451 28.3319C35.9716 28.3319 37.0382 27.6445 37.8587 26.2696C38.5697 25.0873 38.9253 23.7949 38.9253 22.3789C38.9253 21.3065 38.7065 20.4129 38.2689 19.6705ZM36.5459 23.4787C36.3545 24.3861 36.0126 25.0598 35.5066 25.5135C35.1101 25.8709 34.7409 26.0222 34.399 25.9534C34.0708 25.8847 33.7973 25.596 33.5922 25.0598C33.4281 24.6336 33.346 24.2074 33.346 23.8087C33.346 23.465 33.3734 23.1213 33.4418 22.8051C33.5648 22.2414 33.7973 21.6915 34.1665 21.169C34.6178 20.4954 35.0964 20.2204 35.5887 20.3166C35.9169 20.3854 36.1904 20.6741 36.3955 21.2103C36.5596 21.6365 36.6416 22.0627 36.6416 22.4614C36.6416 22.8188 36.6006 23.1625 36.5459 23.4787Z' fill='%237F54B3'/%3E%3Cpath d='M29.6948 19.6705C29.2025 18.8043 28.4641 18.2819 27.5069 18.0757C27.2471 18.0207 27.0009 17.9932 26.7684 17.9932C25.4557 17.9932 24.3891 18.6806 23.5549 20.0554C22.8438 21.224 22.4883 22.5164 22.4883 23.9324C22.4883 24.991 22.7071 25.8984 23.1447 26.6546C23.6369 27.5207 24.3617 28.0432 25.3326 28.2494C25.5924 28.3044 25.8386 28.3319 26.071 28.3319C27.3975 28.3319 28.4641 27.6445 29.2846 26.2696C29.9957 25.0873 30.3512 23.7949 30.3512 22.3789C30.3512 21.3065 30.1324 20.4129 29.6948 19.6705ZM27.9718 23.4787C27.7804 24.3861 27.4385 25.0598 26.9325 25.5135C26.536 25.8709 26.1668 26.0222 25.8249 25.9534C25.4967 25.8847 25.2232 25.596 25.0181 25.0598C24.854 24.6336 24.7719 24.2074 24.7719 23.8087C24.7719 23.465 24.7993 23.1213 24.8677 22.8051C24.9907 22.2414 25.2232 21.6915 25.5924 21.169C26.0437 20.4954 26.5223 20.2204 27.0146 20.3166C27.3428 20.3854 27.6163 20.6741 27.8214 21.2103C27.9855 21.6365 28.0675 22.0627 28.0675 22.4614C28.0675 22.8188 28.0402 23.1625 27.9718 23.4787Z' fill='%237F54B3'/%3E%3C/svg%3E%0A");
}
.jetpack-stepper-wrapper {
button.install-jetpack-button {
// the positioning shenanigans in this button is to keep the button size constant after switching the text to spinner
background: #069e08;
color: #fff;
position: relative;
.install-jetpack-spinner {
position: absolute;
background-color: transparent; // default background is blue for some reason
// below rules are to center the spinner
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
}
.install-jetpack-button-contents {
visibility: hidden;
.jetpack-icon {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 8px;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M8 1.33334C4.33333 1.33334 1.33333 4.33334 1.33333 8.00001C1.33333 11.6667 4.33333 14.6667 8 14.6667C11.6667 14.6667 14.6667 11.6667 14.6667 8.00001C14.6667 4.33334 11.6667 1.33334 8 1.33334ZM7.33333 9.33334H4L7.33333 2.66668V9.33334ZM8.66667 13.3333V6.66668H12L8.66667 13.3333Z' fill='white'/%3E%3C/svg%3E%0A");
}
.install-jetpack-button-text {
display: inline-block;
vertical-align: top;
}
}
}
}
.woocommerce-stepper.is-vertical .woocommerce-stepper__step.is-complete {
// default vertical step distance is 36px which is too much and doesn't match the design
padding-bottom: 16px;
}
button.components-button.send-magic-link-button {
background: #007cba;
color: #fff;
:hover {
// the hover state makes the text invisible so we need to override it
color: #fff;
}
}
.email-sent-modal-body {
padding-block: 40px;
padding-inline: 56px;
margin: auto; // center the content both vertically and horizontally
display: flex;
flex-direction: column;
align-items: center;
height: 495px;
animation: fadeIn 0.7s ease-in-out; // fade in the modal content for consistency because the previous content fades in
.email-sent-title {
h1 {
font-weight: 400;
}
}
.email-sent-illustration {
margin-top: 95px;
margin-bottom: 40px;
background-image: url("data:image/svg+xml,%3Csvg width='84' height='80' viewBox='0 0 84 80' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.3876 45.6191L0.525879 29.8193L42 0L83.4741 29.8193L69.3907 45.6191H14.3876Z' fill='%23BBBBBB'/%3E%3Cpath d='M81.4781 31.8226H0.525879V67.3165H81.4781V31.8226Z' fill='%23757575'/%3E%3Cpath d='M81.4781 31.8226H0.525879V67.3165H81.4781V31.8226Z' fill='url(%23paint0_linear_5_40)'/%3E%3Cpath d='M83.4741 80H0.525879V29.8193L42 54.9651L83.4741 29.8193V80Z' fill='%23949494'/%3E%3Cpath d='M0.87554 18.5686L0.545743 18.7302L0.583669 18.6625L0.554725 18.6719L0.595312 18.6417L5.03405 10.716L7.30304 12.6571L9.24955 14.4653L8.69065 14.7392L9.31897 15.8512L0.87554 18.5686Z' fill='%23BBBBBB'/%3E%3Cpath d='M7.0782 12.8635L8.85737 14.5187L0.793213 18.5413L5.08467 13.0225L7.0782 12.8635Z' fill='%23BBBBBB'/%3E%3Cpath d='M7.0782 12.8635L8.85737 14.5187L0.793213 18.5413L5.08467 13.0225L7.0782 12.8635Z' fill='%23BBBBBB'/%3E%3Cpath d='M5.00295 11.0889L0.793213 18.5413L7.0782 12.8635L5.00295 11.0889Z' fill='%23949494'/%3E%3Cpath d='M8.9094 15.8008L0.80198 18.4878L7.57923 13.4118L8.9094 15.8008Z' fill='%23949494'/%3E%3Cpath d='M68.3945 4.30889L66.6183 7.39864L79.6416 9.52639L71.2536 3.66566L68.3945 4.30889Z' fill='%23BBBBBB'/%3E%3Cpath d='M68.3945 4.30889L66.6183 7.39864L79.6416 9.52639L71.2536 3.66566L68.3945 4.30889Z' fill='%23BBBBBB'/%3E%3Cpath d='M70.5334 0.922852L79.6416 9.52639L68.3945 4.30889L70.5334 0.922852Z' fill='%23949494'/%3E%3Cpath d='M67.0988 9.21499L79.6063 9.45477L67.93 5.29341L67.0988 9.21499Z' fill='%23949494'/%3E%3Cdefs%3E%3ClinearGradient id='paint0_linear_5_40' x1='42' y1='80' x2='42' y2='0' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-opacity='0.12'/%3E%3Cstop offset='0.55135' stop-opacity='0.09'/%3E%3Cstop offset='1' stop-opacity='0.02'/%3E%3C/linearGradient%3E%3C/defs%3E%3C/svg%3E%0A");
background-repeat: no-repeat;
opacity: 0.5;
width: 83px;
height: 80px;
}
.email-sent-subheader-spacer {
flex-grow: 1;
padding-inline: 32px;
margin-top: $gap-smallest;
.email-sent-subheader {
font-weight: 400;
font-size: 16px;
line-height: 1.5rem;
color: #757575;
text-align: center;
}
}
.email-sent-footer {
text-align: center;
.email-sent-footer-prompt {
font-size: 11px;
font-weight: 500;
color: #757575;
}
.email-sent-footer-text {
.email-sent-send-another-link {
color: #007cba;
text-decoration: none;
padding: 0;
cursor: pointer;
}
}
}
}

View File

@ -28,6 +28,7 @@ declare global {
'transient-notices': boolean;
'wc-pay-promotion': boolean;
'wc-pay-welcome-page': boolean;
'woo-mobile-welcome': boolean;
'shipping-smart-defaults': boolean;
'shipping-setting-tour': boolean;
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Woo Mobile Welcome Page with Magic Link feature

View File

@ -27,6 +27,7 @@
"subscriptions": true,
"store-alerts": true,
"transient-notices": true,
"woo-mobile-welcome": true,
"wc-pay-promotion": true,
"wc-pay-welcome-page": true
}

View File

@ -27,6 +27,7 @@
"subscriptions": true,
"store-alerts": true,
"transient-notices": true,
"woo-mobile-welcome": true,
"wc-pay-promotion": true,
"wc-pay-welcome-page": true
}

View File

@ -85,6 +85,7 @@ class Init {
'Automattic\WooCommerce\Admin\API\OnboardingThemes',
'Automattic\WooCommerce\Admin\API\NavigationFavorites',
'Automattic\WooCommerce\Admin\API\Taxes',
'Automattic\WooCommerce\Admin\API\MobileAppMagicLink',
);
if ( Features::is_enabled( 'analytics' ) ) {

View File

@ -0,0 +1,98 @@
<?php
/**
* REST API Data countries controller.
*
* Handles requests to the /mobile-app endpoint.
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
use Automattic\Jetpack\Connection\Manager as Jetpack_Connection_Manager;
/**
* REST API Data countries controller class.
*
* @internal
* @extends WC_REST_Data_Controller
*/
class MobileAppMagicLink extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'mobile-app';
/**
* Register routes.
*
* @since 7.0.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/send-magic-link',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'send_magic_link' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
parent::register_routes();
}
/**
* Sends request to generate magic link email.
*
* @return \WP_REST_Response|\WP_Error
*/
public function send_magic_link() {
// Attempt to get email from Jetpack.
if ( class_exists( Jetpack_Connection_Manager::class ) ) {
$jetpack_connection_manager = new Jetpack_Connection_Manager();
if ( $jetpack_connection_manager->is_active() ) {
if ( class_exists( 'Jetpack_IXR_Client' ) ) {
$xml = new \Jetpack_IXR_Client(
array(
'user_id' => get_current_user_id(),
)
);
$xml->query( 'jetpack.sendMobileMagicLink', array( 'app' => 'woocommerce' ) );
if ( $xml->isError() ) {
return new \WP_Error(
'error_sending_mobile_magic_link',
sprintf(
'%s: %s',
$xml->getErrorCode(),
$xml->getErrorMessage()
)
);
}
return rest_ensure_response(
array(
'code' => 'success',
)
);
}
}
}
return new \WP_Error( 'jetpack_not_connected', __( 'Jetpack is not connected.', 'woocommerce' ) );
}
}