Skip to content

Commit

Permalink
✨(frontend) create SaleTunnelNotValidated step
Browse files Browse the repository at this point in the history
Currently, after payment success, if we reach the poll limit, we only display
an error message to explain to the user it has to wait as its order
validation its too long. It works but it's good for UX as user has to close
the payment modal at the payment step. That's why we decided to create a
SaleTunnelNotValidated. This component is shown when the poll limit is reach
and it helps the user to understand that it has to wait a little bit to start
its training.
  • Loading branch information
jbpenrath committed Jun 3, 2024
1 parent 018ca10 commit 21b0b84
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 99 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- Add `SaleTunnelNotValidated` step into `SaleTunnel`
- Add WarningIcon component

### Changed
Expand Down
1 change: 0 additions & 1 deletion src/frontend/js/components/PaymentInterfaces/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export enum PaymentErrorMessageId {
ERROR_DEFAULT = 'errorDefault',
ERROR_FULL_PRODUCT = 'errorFullProduct',
ERROR_TERMS = 'errorTerms',
ERROR_POLLING_LIMIT = 'errorPollingLimit',
}

export enum PaymentProviders {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ const messages = defineMessages({
description: 'Error message shown when payment creation request failed.',
id: 'components.PaymentButton.errorDefault',
},
errorPollingLimit: {
defaultMessage:
'Your payment has succeeded but your order validation is taking too long, you can close this dialog and come back later.',
description:
'Error message shown when the polling limit has been reached and the order is not yet validated.',
id: 'components.PaymentButton.errorPollingLimit',
},
errorFullProduct: {
defaultMessage: 'There are no more places available for this product.',
description:
Expand Down Expand Up @@ -246,9 +239,7 @@ export const GenericPaymentButton = ({ buildOrderPayload }: Props) => {
const checkOrderValidity = async () => {
if (round >= PAYMENT_SETTINGS.pollLimit) {
timeoutRef.current = undefined;
// @TODO Instead of displaying a raw error message, we could call onSuccess with a "special" argument
// to display a different message in the SaleTunnelSuccess component.
handleError(PaymentErrorMessageId.ERROR_POLLING_LIMIT);
onPaymentSuccessRef.current(false);
} else {
const isValidated = await isOrderValidated(payment!.order_id);
if (isValidated) {
Expand Down Expand Up @@ -298,7 +289,7 @@ export const GenericPaymentButton = ({ buildOrderPayload }: Props) => {
<>
{renderTermsCheckbox()}
<Button
disabled={isBusy || error === PaymentErrorMessageId.ERROR_POLLING_LIMIT}
disabled={isBusy}
onClick={createOrder}
data-testid={order && 'payment-button-order-loaded'}
fullWidth={isMobile}
Expand Down
24 changes: 20 additions & 4 deletions src/frontend/js/components/SaleTunnel/GenericSaleTunnel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CourseProductEvent } from 'types/web-analytics';
import { useOmniscientOrders, useOrders } from 'hooks/useOrders';
import { SaleTunnelInformation } from 'components/SaleTunnel/SaleTunnelInformation';
import { useEnrollments } from 'hooks/useEnrollments';
import SaleTunnelNotValidated from './SaleTunnelNotValidated';

export interface SaleTunnelContextType {
props: SaleTunnelProps;
Expand All @@ -18,7 +19,7 @@ export interface SaleTunnelContextType {
webAnalyticsEventKey: string;

// internal
onPaymentSuccess: () => void;
onPaymentSuccess: (validated?: boolean) => void;
step: SaleTunnelStep;

// meta
Expand Down Expand Up @@ -46,6 +47,7 @@ export const useSaleTunnelContext = () => {
export enum SaleTunnelStep {
PAYMENT,
SUCCESS,
NOT_VALIDATED,
}

interface GenericSaleTunnelProps extends SaleTunnelProps {
Expand Down Expand Up @@ -89,8 +91,12 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
setBillingAddress,
creditCard,
setCreditCard,
onPaymentSuccess: () => {
setStep(SaleTunnelStep.SUCCESS);
onPaymentSuccess: (validated: boolean = true) => {
if (validated) {
setStep(SaleTunnelStep.SUCCESS);
} else {
setStep(SaleTunnelStep.NOT_VALIDATED);
}
WebAnalyticsAPIHandler()?.sendCourseProductEvent(
CourseProductEvent.PAYMENT_SUCCEED,
props.eventKey,
Expand Down Expand Up @@ -143,6 +149,8 @@ export const GenericSaleTunnelInner = (props: GenericSaleTunnelProps) => {
return <GenericSaleTunnelPaymentStep {...props} />;
case SaleTunnelStep.SUCCESS:
return <GenericSaleTunnelSuccessStep {...props} />;
case SaleTunnelStep.NOT_VALIDATED:
return <GenericSaleTunnelNotValidatedStep {...props} />;
}
throw new Error('Invalid step: ' + step);
};
Expand Down Expand Up @@ -187,7 +195,15 @@ export const GenericSaleTunnelPaymentStep = (props: GenericSaleTunnelProps) => {
export const GenericSaleTunnelSuccessStep = (props: SaleTunnelProps) => {
return (
<Modal {...props} size={ModalSize.MEDIUM}>
<SaleTunnelSuccess closeModal={() => props.onClose()} />
<SaleTunnelSuccess closeModal={props.onClose} />
</Modal>
);
};

export const GenericSaleTunnelNotValidatedStep = (props: SaleTunnelProps) => {
return (
<Modal {...props} size={ModalSize.MEDIUM}>
<SaleTunnelNotValidated closeModal={props.onClose} />
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Button } from '@openfun/cunningham-react';
import { generatePath } from 'react-router-dom';
import WarningIcon from 'components/WarningIcon';
import { getDashboardBasename } from 'widgets/Dashboard/hooks/useDashboardRouter/getDashboardBasename';
import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
import { ProductType } from 'types/Joanie';

const messages = defineMessages({
apology: {
defaultMessage: "Sorry, you'll have to wait a little longer!",
description: 'Text displayed to thank user for his order',
id: 'components.SaleTunnelSuccessNotValidated.apology',
},
title: {
defaultMessage: 'It takes too long to validate your order.',
description: 'Message to confirm that order has been created',
id: 'components.SaleTunnelSuccessNotValidated.title',
},
description: {
defaultMessage:
'Your payment has succeeded but your order validation is taking too long, you can close this dialog and come back later. You will receive your invoice by email in a few moments.',
description: "Text to remind that order's invoice will be send by email soon",
id: 'components.SaleTunnelSuccessNotValidated.description',
},
cta: {
defaultMessage: 'Close',
description: 'Label to the call to action to close sale tunnel',
id: 'components.SaleTunnelSuccessNotValidated.cta',
},
});

const SaleTunnelNotValidated = ({ closeModal }: { closeModal: () => void }) => {
const intl = useIntl();
const { order, product } = useSaleTunnelContext();
return (
<section className="sale-tunnel-end" data-testid="generic-sale-tunnel-not-validated-step">
<header className="sale-tunnel-end__header">
<WarningIcon />
<h3 className="sale-tunnel-end__title">
<FormattedMessage {...messages.apology} />
</h3>
</header>
<p className="sale-tunnel-end__content">
<FormattedMessage {...messages.title} />
<br />
<FormattedMessage {...messages.description} />
</p>
<footer className="sale-tunnel-end__footer">
{product.type === ProductType.CREDENTIAL ? (
<Button
href={
getDashboardBasename(intl.locale) +
generatePath(LearnerDashboardPaths.ORDER, { orderId: order!.id })
}
>
<FormattedMessage {...messages.cta} />
</Button>
) : (
<Button onClick={closeModal}>
<FormattedMessage {...messages.cta} />
</Button>
)}
</footer>
</section>
);
};

export default SaleTunnelNotValidated;

This file was deleted.

22 changes: 11 additions & 11 deletions src/frontend/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,48 @@ const messages = defineMessages({
congratulations: {
defaultMessage: 'Congratulations!',
description: 'Text displayed to thank user for his order',
id: 'components.SaleTunnelStepResume.congratulations',
id: 'components.SaleTunnelSuccess.congratulations',
},
successMessage: {
defaultMessage: 'Your order has been successfully created.',
description: 'Message to confirm that order has been created',
id: 'components.SaleTunnelStepResume.successMessage',
id: 'components.SaleTunnelSuccess.successMessage',
},
successDetailMessage: {
defaultMessage: 'You will receive your invoice by email in a few moments.',
description: "Text to remind that order's invoice will be send by email soon",
id: 'components.SaleTunnelStepResume.successDetailMessage',
id: 'components.SaleTunnelSuccess.successDetailMessage',
},
successDetailSignatureMessage: {
defaultMessage:
'In order to enroll to course runs you first need to sign the training contract.',
description: 'Text to remind that order needs to be signed',
id: 'components.SaleTunnelStepResume.successDetailSignatureMessage',
id: 'components.SaleTunnelSuccess.successDetailSignatureMessage',
},
cta: {
defaultMessage: 'Start this course now!',
description: 'Label to the call to action to close sale tunnel',
id: 'components.SaleTunnelStepResume.cta',
id: 'components.SaleTunnelSuccess.cta',
},
ctaSignature: {
defaultMessage: 'Sign the training contract',
description: 'Label to the call to action to close sale tunnel if there is a pending signature',
id: 'components.SaleTunnelStepResume.ctaSignature',
id: 'components.SaleTunnelSuccess.ctaSignature',
},
});

export const SaleTunnelSuccess = ({ closeModal }: { closeModal: () => void }) => {
const intl = useIntl();
const { order, product } = useSaleTunnelContext();
return (
<section className="SaleTunnelSuccess" data-testid="generic-sale-tunnel-success-step">
<header className="SaleTunnelSuccess__header">
<section className="sale-tunnel-end" data-testid="generic-sale-tunnel-success-step">
<header className="sale-tunnel-end__header">
<SuccessIcon />
<h3 className="SaleTunnelSuccess__title">
<h3 className="sale-tunnel-end__title">
<FormattedMessage {...messages.congratulations} />
</h3>
</header>
<p className="SaleTunnelSuccess__content">
<p className="sale-tunnel-end__content">
<FormattedMessage {...messages.successMessage} />
<br />
<FormattedMessage {...messages.successDetailMessage} />
Expand All @@ -62,7 +62,7 @@ export const SaleTunnelSuccess = ({ closeModal }: { closeModal: () => void }) =>
</>
)}
</p>
<footer className="SaleTunnelSuccess__footer">
<footer className="sale-tunnel-end__footer">
{product.contract_definition ? (
<Button
href={
Expand Down
29 changes: 29 additions & 0 deletions src/frontend/js/components/SaleTunnel/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,32 @@
font-weight: var(--c--theme--font--weights--bold);
text-align: left;
}

.sale-tunnel-end {
align-items: center;
display: flex;
flex-direction: column;

&__header {
margin-bottom: 0;
text-align: center;
}

&__title {
color: r-theme-val(steps-content, title-color);
font-size: 1.5rem;
}

&__content {
color: r-theme-val(steps-content, content-color);
font-size: 0.875rem;
line-height: 1.3em;
margin: 0;
padding: 0;
text-align: center;
}

&__footer {
margin-top: 2.5rem;
}
}
Loading

0 comments on commit 21b0b84

Please sign in to comment.