Skip to content

Commit

Permalink
Add Jetpack and Akismet zendesk chat widgets to checkout (#76561)
Browse files Browse the repository at this point in the history
* Add Jetpack and Akismet zendesk chat widgets to checkout

* Update jpAkismet to akismet

* Add missing config var to staging

* Make display logic more clear
  • Loading branch information
CodeyGuyDylan committed May 4, 2023
1 parent 4ce5fc7 commit 70621b8
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 98 deletions.
99 changes: 24 additions & 75 deletions client/components/jetpack/jetpack-presales-chat-widget/index.tsx
Original file line number Diff line number Diff line change
@@ -1,105 +1,54 @@
import config from '@automattic/calypso-config';
import { useQuery } from '@tanstack/react-query';
import { addQueryArgs } from '@wordpress/url';
import { getLocaleSlug } from 'i18n-calypso';
import { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import ZendeskChat from 'calypso/components/presales-zendesk-chat';
import isAkismetCheckout from 'calypso/lib/akismet/is-akismet-checkout';
import isJetpackCheckout from 'calypso/lib/jetpack/is-jetpack-checkout';
import isJetpackCloud from 'calypso/lib/jetpack/is-jetpack-cloud';
import { useJpPresalesAvailabilityQuery } from 'calypso/lib/jetpack/use-jp-presales-availability-query';
import { isUserLoggedIn } from 'calypso/state/current-user/selectors';
import type { ConfigData } from '@automattic/create-calypso-config';

type PresalesChatResponse = {
is_available: boolean;
};

export type KeyType = 'jpAgency' | 'jpGeneral';
export type KeyType = 'jpAgency' | 'jpCheckout' | 'akismet' | 'jpGeneral';

export interface ZendeskJetpackChatProps {
keyType: KeyType;
}

//the API is rate limited if we hit the limit we'll back off and retry
async function fetchWithRetry(
url: string,
options: RequestInit,
retries = 3,
delay = 30000
): Promise< Response > {
try {
const response = await fetch( url, options );

if ( response.status === 429 && retries > 0 ) {
await new Promise( ( resolve ) => setTimeout( resolve, delay ) );
return await fetchWithRetry( url, options, retries - 1, delay * 2 );
}
// The akismet chat key is included here because the availability for Akismet presales is in the same group as Jetpack (jp_presales)
function get_config_chat_key( keyType: KeyType ): keyof ConfigData {
const chatWidgetKeyMap = {
jpAgency: 'zendesk_presales_chat_key_jp_agency_dashboard',
jpCheckout: 'zendesk_presales_chat_key_jp_checkout',
akismet: 'zendesk_presales_chat_key_akismet',
jpGeneral: 'zendesk_presales_chat_key',
};

return response;
} catch ( error ) {
if ( retries > 0 ) {
await new Promise( ( resolve ) => setTimeout( resolve, delay ) );
return await fetchWithRetry( url, options, retries - 1, delay * 2 );
}
throw error;
}
return chatWidgetKeyMap[ keyType ] ?? 'zendesk_presales_chat_key';
}

export const ZendeskJetpackChat: React.VFC< { keyType: KeyType } > = ( { keyType } ) => {
const [ error, setError ] = useState( false );
const { data: isStaffed } = usePresalesAvailabilityQuery();
const zendeskChatKey = useMemo( () => {
return config(
keyType === 'jpAgency'
? 'zendesk_presales_chat_key_jp_agency_dashboard'
: 'zendesk_presales_chat_key'
) as keyof ConfigData;
const { data: isStaffed } = useJpPresalesAvailabilityQuery( setError );

This comment has been minimized.

Copy link
@klimeryk

klimeryk May 17, 2023

Contributor

@CodeyGuyDylan, while working on #77024, I noticed that the presales availability is being queried always here. I've fixed this a bit in 3f18f83, but it needs more work in this context. Basically, you can pass enabled to useQuery to skip fetching it - for example, if we know the user is not in JP checkout :) Could you please look into it and fix it in this file as well? 🙏

This comment has been minimized.

Copy link
@CodeyGuyDylan

CodeyGuyDylan May 17, 2023

Author Contributor

Sure! I'll add that to our maintenance board 😄

const zendeskChatKey: string | false = useMemo( () => {
return config( get_config_chat_key( keyType ) );
}, [ keyType ] );
const isLoggedIn = useSelector( isUserLoggedIn );
const shouldShowZendeskPresalesChat = useMemo( () => {
const isEnglishLocale = ( config( 'english_locales' ) as string[] ).includes(
getLocaleSlug() ?? ''
);

return config.isEnabled( 'jetpack/zendesk-chat-for-logged-in-users' )
? isEnglishLocale && isJetpackCloud() && isStaffed
: ! isLoggedIn && isEnglishLocale && isJetpackCloud() && isStaffed;
}, [ isStaffed, isLoggedIn ] );

function usePresalesAvailabilityQuery() {
//adding a safeguard to ensure if there's an unkown error with the widget it won't crash the whole app
try {
return useQuery< boolean, Error >(
[ 'presales-availability' ],
async () => {
const url = 'https://public-api.wordpress.com/wpcom/v2/presales/chat';
const queryObject = {
group: 'jp_presales',
};

const response = await fetchWithRetry(
addQueryArgs( url, queryObject as Record< string, string > ),
{
credentials: 'same-origin',
mode: 'cors',
}
);

if ( ! response.ok ) {
throw new Error( `API request failed with status ${ response.status }` );
}
const isCorrectContext =
( isAkismetCheckout() && keyType === 'akismet' ) ||
( isJetpackCheckout() && keyType === 'jpCheckout' ) ||
( isJetpackCloud() && ( keyType === 'jpAgency' || keyType === 'jpGeneral' ) );

const data: PresalesChatResponse = await response.json();
return data.is_available;
},
{
meta: { persist: false },
}
);
} catch ( error ) {
setError( true );
return { data: false };
}
}
return config.isEnabled( 'jetpack/zendesk-chat-for-logged-in-users' )
? isEnglishLocale && isCorrectContext && isStaffed
: ! isLoggedIn && isEnglishLocale && isCorrectContext && isStaffed;
}, [ isStaffed, isLoggedIn, keyType ] );

if ( error ) {
return null;
Expand Down
68 changes: 68 additions & 0 deletions client/lib/jetpack/use-jp-presales-availability-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useQuery } from '@tanstack/react-query';
import { addQueryArgs } from '@wordpress/url';
import type { SetStateAction, Dispatch } from 'react';

type PresalesChatResponse = {
is_available: boolean;
};

//the API is rate limited if we hit the limit we'll back off and retry
async function fetchWithRetry(
url: string,
options: RequestInit,
retries = 3,
delay = 30000
): Promise< Response > {
try {
const response = await fetch( url, options );

if ( response.status === 429 && retries > 0 ) {
await new Promise( ( resolve ) => setTimeout( resolve, delay ) );
return await fetchWithRetry( url, options, retries - 1, delay * 2 );
}

return response;
} catch ( error ) {
if ( retries > 0 ) {
await new Promise( ( resolve ) => setTimeout( resolve, delay ) );
return await fetchWithRetry( url, options, retries - 1, delay * 2 );
}
throw error;
}
}

export function useJpPresalesAvailabilityQuery( setError?: Dispatch< SetStateAction< boolean > > ) {
//adding a safeguard to ensure if there's an unkown error with the widget it won't crash the whole app
try {
return useQuery< boolean, Error >(
[ 'presales-availability' ],
async () => {
const url = 'https://public-api.wordpress.com/wpcom/v2/presales/chat';
const queryObject = {
group: 'jp_presales',
};

const response = await fetchWithRetry(
addQueryArgs( url, queryObject as Record< string, string > ),
{
credentials: 'same-origin',
mode: 'cors',
}
);

if ( ! response.ok ) {
throw new Error( `API request failed with status ${ response.status }` );
}

const data: PresalesChatResponse = await response.json();
return data.is_available;
},
{
meta: { persist: false },
}
);
} catch ( error ) {
setError && setError( true );
return { data: false };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { useSelector, useDispatch } from 'react-redux';
import QuerySupportTypes from 'calypso/blocks/inline-help/inline-help-query-support-types';
import AsyncLoad from 'calypso/components/async-load';
import HappychatButtonUnstyled from 'calypso/components/happychat/button';
import { ZendeskJetpackChat } from 'calypso/components/jetpack/jetpack-presales-chat-widget';
import isAkismetCheckout from 'calypso/lib/akismet/is-akismet-checkout';
import isJetpackCheckout from 'calypso/lib/jetpack/is-jetpack-checkout';
import { useJpPresalesAvailabilityQuery } from 'calypso/lib/jetpack/use-jp-presales-availability-query';
import useCartKey from 'calypso/my-sites/checkout/use-cart-key';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import { getCurrentUserId } from 'calypso/state/current-user/selectors';
Expand Down Expand Up @@ -143,6 +147,7 @@ export default function CheckoutHelpLink() {
const translate = useTranslate();
const { setShowHelpCenter } = useDataStoreDispatch( HELP_CENTER_STORE );
const isEnglishLocale = useIsEnglishLocale();
const { data: isJpPresalesStaffed } = useJpPresalesAvailabilityQuery();

const cartKey = useCartKey();
const { responseCart } = useShoppingCart( cartKey );
Expand Down Expand Up @@ -191,38 +196,62 @@ export default function CheckoutHelpLink() {
zendeskPresalesChatKey &&
! purchasesAreBlocked;

// Show loading button if we haven't determined whether or not to show the Zendesk chat button yet.
const shouldShowLoadingButton =
! supportVariationDetermined && ! isJpPresalesStaffed && ! isPresalesZendeskChatEligible;
const isSitelessCheckout = isAkismetCheckout() || isJetpackCheckout();
const shouldShowZendeskChatWidget =
( isPresalesZendeskChatEligible && ! isSitelessCheckout ) ||
( isSitelessCheckout && isJpPresalesStaffed );

const hasDirectSupport = supportVariation !== SUPPORT_FORUM;

// For the siteless checkouts, we do not need to specifically check if isJpPresalesStaffed is true
// because if this function is being called during a siteless checkout session, shouldShowZendeskChatWidget
// already ensures that isJpPresalesStaffed is true.
const getZendeskChatWidget = () => {
if ( isAkismetCheckout() ) {
return <ZendeskJetpackChat keyType="akismet" />;
}

if ( isJetpackCheckout() ) {
return <ZendeskJetpackChat keyType="jpCheckout" />;
}

// If we're not in a siteless checkout, we can use the regular wordpress themed Zendesk chat widget.
return (
<AsyncLoad
require="calypso/components/presales-zendesk-chat"
chatKey={ zendeskPresalesChatKey }
/>
);
};

// If pre-sales chat isn't available, use the inline help button instead.
return (
<CheckoutHelpLinkWrapper>
<QuerySupportTypes />
{ ! isPresalesZendeskChatEligible && ! supportVariationDetermined && <LoadingButton /> }
{ isPresalesZendeskChatEligible ? (
<AsyncLoad
require="calypso/components/presales-zendesk-chat"
chatKey={ zendeskPresalesChatKey }
/>
) : (
supportVariationDetermined && (
<CheckoutSummaryHelpButton onClick={ handleHelpButtonClicked }>
{ hasDirectSupport
? translate( 'Questions? {{underline}}Ask a Happiness Engineer{{/underline}}', {
components: {
underline: <span />,
},
} )
: translate(
'Questions? {{underline}}Read more about plans and purchases{{/underline}}',
{
{ shouldShowLoadingButton && <LoadingButton /> }
{ shouldShowZendeskChatWidget
? getZendeskChatWidget()
: supportVariationDetermined && (
<CheckoutSummaryHelpButton onClick={ handleHelpButtonClicked }>
{ hasDirectSupport
? translate( 'Questions? {{underline}}Ask a Happiness Engineer{{/underline}}', {
components: {
underline: <span />,
},
}
) }
</CheckoutSummaryHelpButton>
)
) }
} )
: translate(
'Questions? {{underline}}Read more about plans and purchases{{/underline}}',
{
components: {
underline: <span />,
},
}
) }
</CheckoutSummaryHelpButton>
) }
</CheckoutHelpLinkWrapper>
);
}
2 changes: 2 additions & 0 deletions config/_shared.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"dsp_stripe_pub_key": "pk_live_51LYYzQF53KN4RFN0oHFYHpzmHGpuUAXZ6ygDAFxM4XkRArRsET0orrofmsynkd9DhFaOGEsDZ3RC4f8mgI0zhEyN000X8i0Mqx",
"dsp_widget_js_src": "https://widgets.wp.com/promote/widget.js",
"zendesk_presales_chat_key": false,
"zendesk_presales_chat_key_akismet": false,
"zendesk_presales_chat_key_jp_checkout": false,
"zendesk_presales_chat_key_jp_agency_dashboard": false,
"upwork_support_locales": [
"de",
Expand Down
3 changes: 3 additions & 0 deletions config/development.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"dsp_stripe_pub_key": "pk_live_51LYYzQF53KN4RFN0oHFYHpzmHGpuUAXZ6ygDAFxM4XkRArRsET0orrofmsynkd9DhFaOGEsDZ3RC4f8mgI0zhEyN000X8i0Mqx",
"dsp_widget_js_src": "https://widgets.wp.com/promote/widget.js",
"zendesk_presales_chat_key": "216bf91d-f10f-4f66-bf65-a0cba220cd38",
"zendesk_presales_chat_key_akismet": "7ce13115-7b34-4069-9d64-228d90f148d1",
"zendesk_presales_chat_key_jp_checkout": "7c42153f-f579-49ba-a33e-246b2d27fb93",
"features": {
"ad-tracking": false,
"akismet/siteless-checkout": true,
Expand Down Expand Up @@ -94,6 +96,7 @@
"jetpack/simplify-pricing-structure": false,
"jetpack/standalone-plugin-onboarding-update-v1": true,
"jetpack/offer-complete-after-activation": false,
"jetpack/zendesk-chat-for-logged-in-users": true,
"jitms": true,
"lasagna": true,
"layout/app-banner": true,
Expand Down
3 changes: 3 additions & 0 deletions config/production.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"dsp_stripe_pub_key": "pk_live_51LYYzQF53KN4RFN0oHFYHpzmHGpuUAXZ6ygDAFxM4XkRArRsET0orrofmsynkd9DhFaOGEsDZ3RC4f8mgI0zhEyN000X8i0Mqx",
"dsp_widget_js_src": "https://widgets.wp.com/promote/widget.js",
"zendesk_presales_chat_key": "216bf91d-f10f-4f66-bf65-a0cba220cd38",
"zendesk_presales_chat_key_akismet": "7ce13115-7b34-4069-9d64-228d90f148d1",
"zendesk_presales_chat_key_jp_checkout": "7c42153f-f579-49ba-a33e-246b2d27fb93",
"features": {
"ad-tracking": true,
"akismet/siteless-checkout": true,
Expand Down Expand Up @@ -67,6 +69,7 @@
"jetpack/standalone-plugin-onboarding-update-v1": true,
"jetpack-social/advanced-plan": false,
"jetpack/offer-complete-after-activation": false,
"jetpack/zendesk-chat-for-logged-in-users": true,
"jitms": true,
"lasagna": true,
"layout/app-banner": true,
Expand Down
3 changes: 3 additions & 0 deletions config/stage.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"dsp_stripe_pub_key": "pk_live_51LYYzQF53KN4RFN0oHFYHpzmHGpuUAXZ6ygDAFxM4XkRArRsET0orrofmsynkd9DhFaOGEsDZ3RC4f8mgI0zhEyN000X8i0Mqx",
"dsp_widget_js_src": "https://widgets.wp.com/promote/widget.js",
"zendesk_presales_chat_key": "216bf91d-f10f-4f66-bf65-a0cba220cd38",
"zendesk_presales_chat_key_akismet": "7ce13115-7b34-4069-9d64-228d90f148d1",
"zendesk_presales_chat_key_jp_checkout": "7c42153f-f579-49ba-a33e-246b2d27fb93",
"features": {
"ad-tracking": false,
"akismet/siteless-checkout": true,
Expand Down Expand Up @@ -64,6 +66,7 @@
"jetpack/standalone-plugin-onboarding-update-v1": true,
"jetpack-social/advanced-plan": true,
"jetpack/offer-complete-after-activation": false,
"jetpack/zendesk-chat-for-logged-in-users": true,
"jitms": true,
"lasagna": true,
"layout/app-banner": true,
Expand Down
3 changes: 3 additions & 0 deletions config/wpcalypso.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"dsp_stripe_pub_key": "pk_live_51LYYzQF53KN4RFN0oHFYHpzmHGpuUAXZ6ygDAFxM4XkRArRsET0orrofmsynkd9DhFaOGEsDZ3RC4f8mgI0zhEyN000X8i0Mqx",
"dsp_widget_js_src": "https://widgets.wp.com/promote/widget.js",
"zendesk_presales_chat_key": "216bf91d-f10f-4f66-bf65-a0cba220cd38",
"zendesk_presales_chat_key_akismet": "7ce13115-7b34-4069-9d64-228d90f148d1",
"zendesk_presales_chat_key_jp_checkout": "7c42153f-f579-49ba-a33e-246b2d27fb93",
"features": {
"ad-tracking": false,
"akismet/siteless-checkout": true,
Expand Down Expand Up @@ -71,6 +73,7 @@
"jetpack/simplify-pricing-structure": false,
"jetpack-social/advanced-plan": false,
"jetpack/offer-complete-after-activation": false,
"jetpack/zendesk-chat-for-logged-in-users": true,
"jitms": true,
"lasagna": true,
"layout/app-banner": true,
Expand Down

0 comments on commit 70621b8

Please sign in to comment.