Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Display Pay component when fee is zero #4067

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions editor.planx.uk/pnpm-lock.yaml

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

298 changes: 184 additions & 114 deletions editor.planx.uk/src/@planx/components/Pay/Public/Confirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ import { FeeBreakdown } from "./FeeBreakdown/FeeBreakdown";
import InviteToPayForm, { InviteToPayFormProps } from "./InviteToPayForm";
import { PAY_API_ERROR_UNSUPPORTED_TEAM } from "./Pay";

type ComponentState =
| "error"
| "informationOnly"
| "inviteToPay"
| "zeroFee"
| "pay";

export interface Props extends Omit<Pay, "title" | "fn" | "govPayMetadata"> {
title?: string;
fee: number;
Expand All @@ -48,88 +55,142 @@ const PayText = styled(Box)(({ theme }) => ({
},
}));

const PayBody: React.FC<PayBodyProps> = (props) => {
const path = useStore((state) => state.path);
const isSaveReturn = path === ApplicationPath.SaveAndReturn;
const defaults = getDefaultContent();

if (props.error) {
if (props.error.startsWith(PAY_API_ERROR_UNSUPPORTED_TEAM)) {
return (
<Card handleSubmit={props.onConfirm} isValid>
<ErrorSummary
format="error"
heading={props.error}
message="Click continue to skip payment and proceed with your application for testing."
/>
</Card>
);
} else {
return (
<Card>
<ErrorSummary
format="error"
heading={props.error}
message="This error has been logged and our team will see it soon. You can safely close this tab and try resuming again soon by returning to this URL."
/>
</Card>
);
}
/**
* Displayed when component is in an error state
* Relies on props.error being set
*/
const Error: React.FC<Props> = ({ onConfirm, error }) => {
if (error?.startsWith(PAY_API_ERROR_UNSUPPORTED_TEAM)) {
return (
<Card handleSubmit={onConfirm} isValid>
<ErrorSummary
format="error"
heading={error}
message="Click continue to skip payment and proceed with your application for testing."
/>
</Card>
);
}

return (
<Card>
<PayText>
<Typography variant="h2" component={props.hideFeeBanner ? "h2" : "h3"}>
{props.instructionsTitle || defaults.instructionsTitle}
</Typography>
<ReactMarkdownOrHtml
source={
props.instructionsDescription || defaults.instructionsDescription
}
openLinksOnNewTab
/>
<ErrorSummary
format="error"
heading={error}
message="This error has been logged and our team will see it soon. You can safely close this tab and try resuming again soon by returning to this URL."
/>
</Card>
);
};

/**
* Main functionality
* Allows users to make a payment via GovUK Pay
*/
const PayBody: React.FC<PayBodyProps> = (props) => {
const defaults = getDefaultContent();

return (
<>
<Typography variant="h2" component={props.hideFeeBanner ? "h2" : "h3"}>
{props.instructionsTitle || defaults.instructionsTitle}
</Typography>
<ReactMarkdownOrHtml
source={
props.instructionsDescription || defaults.instructionsDescription
}
openLinksOnNewTab
/>
<Button
variant="contained"
color="primary"
size="large"
onClick={props.onConfirm}
>
{props.buttonTitle || "Pay now using GOV.UK Pay"}
</Button>
{props.showInviteToPay && (
<Button
variant="contained"
color="primary"
color="secondary"
size="large"
onClick={props.onConfirm}
onClick={props.changePage}
disabled={Boolean(props?.paymentStatus)}
data-testid="invite-page-link"
>
{props.hidePay
? "Continue"
: props.buttonTitle || "Pay now using GOV.UK Pay"}
{"Invite someone else to pay for this application"}
</Button>
{!props.hidePay && props.showInviteToPay && (
<Button
variant="contained"
color="secondary"
size="large"
onClick={props.changePage}
disabled={Boolean(props?.paymentStatus)}
data-testid="invite-page-link"
>
{"Invite someone else to pay for this application"}
</Button>
)}
{isSaveReturn && <SaveResumeButton />}
</PayText>
</Card>
)}
</>
);
};

/**
* Display information only - does not allow users to pay
* Generally used at the end of guidance services as an illustrative example of what you could pay
*/
const InformationOnly: React.FC<Props> = (props) => {
const defaults = getDefaultContent();

return (
<>
<Typography variant="h2" component={props.hideFeeBanner ? "h2" : "h3"}>
{props.instructionsTitle || defaults.instructionsTitle}
</Typography>
<ReactMarkdownOrHtml
source={
props.instructionsDescription || defaults.instructionsDescription
}
openLinksOnNewTab
/>
<Button
variant="contained"
color="primary"
size="large"
onClick={props.onConfirm}
>
Continue
</Button>
</>
);
};

/**
* Displayed if the fee is calculated as £0
* Still displays component and fee breakdown, but allows user to continue without making a payment
*/
const ZeroFee: React.FC<Props> = (props) => (
<Button
variant="contained"
color="primary"
size="large"
onClick={props.onConfirm}
>
Continue
</Button>
);

const getInitialState = (props: Props): ComponentState => {
if (props.error) return "error";
if (props.hidePay) return "informationOnly";
if (props.fee === 0) return "zeroFee";

return "pay";
};

export default function Confirm(props: Props) {
const theme = useTheme();
const [page, setPage] = useState<"Pay" | "InviteToPay">("Pay");
const [componentState, setComponentState] = useState<ComponentState>(
getInitialState(props),
);

const toggleToPayPage = () => setComponentState("pay");
const toggleToInviteToPayPage = () => setComponentState("inviteToPay");

const defaults = getDefaultContent();

const changePage = () => {
if (page === "Pay" && !props.paymentStatus) {
setPage("InviteToPay");
} else {
setPage("Pay");
}
};
const path = useStore((state) => state.path);
const isSaveReturn = path === ApplicationPath.SaveAndReturn;

const inviteToPayFormProps: InviteToPayFormProps = {
nomineeTitle: props.nomineeTitle,
Expand All @@ -138,59 +199,68 @@ export default function Confirm(props: Props) {
yourDetailsDescription: props.yourDetailsDescription,
yourDetailsLabel: props.yourDetailsLabel,
paymentStatus: props.paymentStatus,
changePage,
changePage: toggleToPayPage,
};

return (
<Box textAlign="left" width="100%">
<>
<Container maxWidth="contentWrap">
<Typography variant="h2" component="h1" align="left" pb={3}>
{page === "Pay" ? props.title : props.secondaryPageTitle}
</Typography>
</Container>
{page === "Pay" && !props.hideFeeBanner && (
<Banner
color={{
background: theme.palette.info.light,
text: theme.palette.text.primary,
}}
>
<FormWrapper>
<Typography
variant="h3"
gutterBottom
className="marginBottom"
component="h2"
>
{props.bannerTitle || defaults.bannerTitle}
</Typography>
<Typography
variant="h1"
gutterBottom
className="marginBottom"
component="span"
>
{isNaN(props.fee)
? "Unknown"
: formattedPriceWithCurrencySymbol(props.fee)}
</Typography>
<Typography variant="subtitle1" component="span" color="inherit">
<ReactMarkdownOrHtml
source={props.description}
openLinksOnNewTab
/>
</Typography>
</FormWrapper>
{hasFeatureFlag("FEE_BREAKDOWN") && <FeeBreakdown />}
</Banner>
)}
{page === "Pay" ? (
<PayBody changePage={changePage} {...props} />
) : (
<InviteToPayForm {...inviteToPayFormProps} />
)}
</>
<Container maxWidth="contentWrap">
<Typography variant="h2" component="h1" align="left" pb={3}>
{componentState === "inviteToPay"
? props.secondaryPageTitle
: props.title}
</Typography>
</Container>
{componentState !== "inviteToPay" && !props.hideFeeBanner && (
<Banner
color={{
background: theme.palette.info.light,
text: theme.palette.text.primary,
}}
>
<FormWrapper>
<Typography
variant="h3"
gutterBottom
className="marginBottom"
component="h2"
>
{props.bannerTitle || defaults.bannerTitle}
</Typography>
<Typography
variant="h1"
gutterBottom
className="marginBottom"
component="span"
>
{isNaN(props.fee)
? "Unknown"
: formattedPriceWithCurrencySymbol(props.fee)}
</Typography>
<Typography variant="subtitle1" component="span" color="inherit">
<ReactMarkdownOrHtml
source={props.description}
openLinksOnNewTab
/>
</Typography>
</FormWrapper>
{hasFeatureFlag("FEE_BREAKDOWN") && <FeeBreakdown />}
</Banner>
)}
<Card>
<PayText>
{
{
pay: <PayBody {...props} changePage={toggleToInviteToPayPage} />,
informationOnly: <InformationOnly {...props} />,
inviteToPay: <InviteToPayForm {...inviteToPayFormProps} />,
error: <Error {...props} />,
zeroFee: <ZeroFee {...props} />,
}[componentState]
}
{isSaveReturn && <SaveResumeButton />}
</PayText>
</Card>
</Box>
);
}
Loading
Loading