From 04210abdbb05196679406ac526d849194bce5a2e Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 23 Mar 2023 15:05:48 +0800 Subject: [PATCH 01/37] feat: add new route for payment page --- frontend/src/app/AppRouter.tsx | 6 ++ frontend/src/constants/routes.ts | 1 + .../src/features/payment/FormPaymentPage.tsx | 79 +++++++++++++++++++ .../features/payment/FormPaymentService.ts | 31 ++++++++ frontend/src/features/payment/queries.ts | 25 ++++++ .../features/public-form/PublicFormService.ts | 21 ++--- frontend/src/features/public-form/queries.ts | 13 +-- shared/types/payment.ts | 5 ++ src/app/modules/payments/stripe.controller.ts | 52 ++++++++++++ .../encrypt-submission.controller.ts | 1 + .../routes/api/v3/payments/payments.routes.ts | 17 +++- 11 files changed, 219 insertions(+), 32 deletions(-) create mode 100644 frontend/src/features/payment/FormPaymentPage.tsx create mode 100644 frontend/src/features/payment/FormPaymentService.ts create mode 100644 frontend/src/features/payment/queries.ts diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index e4bb0971f6..4272828a83 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -12,6 +12,7 @@ import { DASHBOARD_ROUTE, LANDING_ROUTE, LOGIN_ROUTE, + PAYMENT_PAGE_SUBROUTE, PRIVACY_POLICY_ROUTE, PUBLICFORM_ROUTE, RESULTS_FEEDBACK_SUBROUTE, @@ -32,6 +33,7 @@ import { ResponsesPage, } from '~features/admin-form/responses' import { SettingsPage } from '~features/admin-form/settings/SettingsPage' +import { FormPaymentPage } from '~features/payment/FormPaymentPage' import { BillingPage } from '~features/user/billing' import { HashRouterElement } from './HashRouterElement' @@ -99,6 +101,10 @@ export const AppRouter = (): JSX.Element => { path={USE_TEMPLATE_REDIRECT_SUBROUTE} element={} />} /> + } />} + /> { + const { formId, paymentPageId } = useParams() + + if (!formId) throw new Error('No formId provided') + if (!paymentPageId) throw new Error('No paymentPageId provided') + + const { data: paymentInfoData, error: paymentInfoError } = + useGetPaymentInfo(paymentPageId) + const { data, isLoading, error } = useGetPaymentReceiptStatus( + formId, + paymentPageId, + ) + + const navigate = useNavigate() + + useEffect(() => { + if (!isLoading && error) { + const currentUrl = new URL(window.location.href) + currentUrl.searchParams.set('retryPayment', 'true') + const urlPathAndSearch = currentUrl.pathname + currentUrl.search + navigate(urlPathAndSearch) + } + }, [error, isLoading, navigate]) + + return ( + + + + + + + + + + + + + + {data?.isReady ? ( + + ) : null} + + + + + + + + + + + ) +} diff --git a/frontend/src/features/payment/FormPaymentService.ts b/frontend/src/features/payment/FormPaymentService.ts new file mode 100644 index 0000000000..939962cfc1 --- /dev/null +++ b/frontend/src/features/payment/FormPaymentService.ts @@ -0,0 +1,31 @@ +import { GetPaymentInfoDto, PaymentReceiptStatusDto } from '~shared/types' + +import { ApiService } from '~services/ApiService' + +/** + * Obtain payment receipt status for a given submission. + * @param formId the id of the form + * @param submissionId the id of the form submission + * @returns PaymentReceiptStatusDto on success + */ +export const getPaymentReceiptStatus = async ( + formId: string, + submissionId: string, +): Promise => { + return ApiService.get( + `payments/receipt/${formId}/${submissionId}/status`, + ).then(({ data }) => data) +} + +/** + * Obtain payment information neccessary to do a subsequent + * for a given paymentId. + * @param formId the id of the form + * @param submissionId the id of the form submission + * @returns PaymentReceiptStatusDto on success + */ +export const getPaymentInfo = async (paymentId: string) => { + return ApiService.get( + `payments/${paymentId}/getInfo`, + ).then(({ data }) => data) +} diff --git a/frontend/src/features/payment/queries.ts b/frontend/src/features/payment/queries.ts new file mode 100644 index 0000000000..e40ecd5fcc --- /dev/null +++ b/frontend/src/features/payment/queries.ts @@ -0,0 +1,25 @@ +import { useQuery, UseQueryResult } from 'react-query' + +import { GetPaymentInfoDto, PaymentReceiptStatusDto } from '~shared/types' + +import { ApiError } from '~typings/core' + +import { getPaymentInfo, getPaymentReceiptStatus } from './FormPaymentService' + +export const useGetPaymentReceiptStatus = ( + formId: string, + submissionId: string, +): UseQueryResult => { + return useQuery( + [formId, submissionId], + () => getPaymentReceiptStatus(formId, submissionId), + ) +} + +export const useGetPaymentInfo = ( + paymentId: string, +): UseQueryResult => { + return useQuery(paymentId, () => + getPaymentInfo(paymentId), + ) +} diff --git a/frontend/src/features/public-form/PublicFormService.ts b/frontend/src/features/public-form/PublicFormService.ts index 273fb8ba70..dcd260d9be 100644 --- a/frontend/src/features/public-form/PublicFormService.ts +++ b/frontend/src/features/public-form/PublicFormService.ts @@ -1,4 +1,8 @@ -import { PaymentReceiptStatusDto, SuccessMessageDto } from '~shared/types' +import { + GetPaymentInfoDto, + PaymentReceiptStatusDto, + SuccessMessageDto, +} from '~shared/types' import { FormFieldDto } from '~shared/types/field' import { PublicFormAuthLogoutDto, @@ -153,18 +157,3 @@ export const submitFormFeedback = async ( feedbackToPost, ).then(({ data }) => data) } - -/** - * Obtain payment receipt status for a given submission. - * @param formId the id of the form - * @param submissionId the id of the form submission - * @returns PaymentReceiptStatusDto on success - */ -export const getPaymentReceiptStatus = async ( - formId: string, - submissionId: string, -): Promise => { - return ApiService.get( - `payments/receipt/${formId}/${submissionId}/status`, - ).then(({ data }) => data) -} diff --git a/frontend/src/features/public-form/queries.ts b/frontend/src/features/public-form/queries.ts index 2af4678b7e..c246ce9f43 100644 --- a/frontend/src/features/public-form/queries.ts +++ b/frontend/src/features/public-form/queries.ts @@ -1,13 +1,12 @@ import { useQuery, UseQueryResult } from 'react-query' import { PublicFormViewDto } from '~shared/types/form/form' -import { PaymentReceiptStatusDto } from '~shared/types/payment' import { ApiError } from '~typings/core' import { FORMID_REGEX } from '~constants/routes' -import { getPaymentReceiptStatus, getPublicFormView } from './PublicFormService' +import { getPublicFormView } from './PublicFormService' export const publicFormKeys = { // All keys map to either an array or function returning an array for @@ -31,13 +30,3 @@ export const usePublicFormView = ( }, ) } - -export const useGetPaymentReceiptStatus = ( - formId: string, - submissionId: string, -): UseQueryResult => { - return useQuery( - [formId, submissionId], - () => getPaymentReceiptStatus(formId, submissionId), - ) -} diff --git a/shared/types/payment.ts b/shared/types/payment.ts index 4a6189eae5..76883915f3 100644 --- a/shared/types/payment.ts +++ b/shared/types/payment.ts @@ -53,3 +53,8 @@ export type Payment = { export type PaymentReceiptStatusDto = { isReady: boolean } + +export type GetPaymentInfoDto = { + client_secret: string | null + publishableKey: string +} diff --git a/src/app/modules/payments/stripe.controller.ts b/src/app/modules/payments/stripe.controller.ts index cd649c2736..d2dc3db1bf 100644 --- a/src/app/modules/payments/stripe.controller.ts +++ b/src/app/modules/payments/stripe.controller.ts @@ -8,6 +8,7 @@ import mongoose from 'mongoose' import { ok, Result } from 'neverthrow' import Stripe from 'stripe' +import { ErrorDto, GetPaymentInfoDto } from '../../../../shared/types' import config from '../../config/config' import { paymentConfig } from '../../config/features/payment.config' import { createLoggerWithLabel } from '../../config/logger' @@ -388,3 +389,54 @@ export const handleConnectOauthCallback = [ }), _handleConnectOauthCallback, ] as ControllerHandler[] + +export const getPaymentInfo: ControllerHandler< + { + paymentIntentId: string + }, + GetPaymentInfoDto | ErrorDto +> = async (req, res) => { + const { paymentIntentId } = req.params + logger.info({ + message: 'getPaymentInfo endpoint called', + meta: { + action: 'getPaymentInfo', + paymentIntentId, + }, + }) + + let stripeFullIntentObj + try { + stripeFullIntentObj = await stripe.paymentIntents.retrieve(paymentIntentId) + } catch (error) { + logger.error({ + message: 'getPaymentInfo endpoint called', + meta: { + action: 'getPaymentInfo', + paymentIntentId, + error, + }, + }) + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .json({ message: 'Stripe retreival error' }) + } + + if (!stripeFullIntentObj?.client_secret) { + logger.error({ + message: 'Error occurred when reading client_secret', + meta: { + action: 'getPaymentInfo', + paymentIntentId, + }, + }) + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .json({ message: 'Missing client secret' }) + } + + return res.status(StatusCodes.OK).json({ + client_secret: stripeFullIntentObj.client_secret, + publishableKey: paymentConfig.stripePublishableKey, + }) +} diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts index d24a74fa20..bfe5cc30e4 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts @@ -533,6 +533,7 @@ const submitEncryptModeForm: ControllerHandler< ? { paymentClientSecret, paymentPublishableKey: form.payments_channel.publishable_key, + paymentIntentId: paymentIntent?.id, } : {}), }) diff --git a/src/app/routes/api/v3/payments/payments.routes.ts b/src/app/routes/api/v3/payments/payments.routes.ts index b59692bc85..8a111db892 100644 --- a/src/app/routes/api/v3/payments/payments.routes.ts +++ b/src/app/routes/api/v3/payments/payments.routes.ts @@ -16,18 +16,19 @@ PaymentsRouter.get('/stripe') * @returns 404 if receipt URL does not exist */ PaymentsRouter.route( - '/receipt/:formId([a-fA-F0-9]{24})/:submissionId([a-fA-F0-9]{24})/status', + '/receipt/:formId([a-fA-F0-9]{24})/:paymentIntentId([a-fA-F0-9]{24}/status', ).get(StripeController.checkPaymentReceiptStatus) /** * Downloads the receipt pdf - * @route GET /receipt/:formId/:submissionId/download + * @route GET /payments/receipt/:formId/:submissionId/download * * @returns 200 with receipt URL exists */ -// TODO: consider rate limiting this endpoint #5924 PaymentsRouter.route( - '/receipt/:formId([a-fA-F0-9]{24})/:submissionId([a-fA-F0-9]{24})/download', + // '/receipt/:formId([a-fA-F0-9]{24})/:submissionId([a-fA-F0-9]{24})/download', + // TODO: change to paymentIntentId in child funcs + '/receipt/:formId([a-fA-F0-9]{24})/:paymentIntentId([a-fA-F0-9]{24})/download', ).get( limitRate({ max: rateLimitConfig.downloadPaymentReceipt }), StripeController.downloadPaymentReceipt, @@ -36,3 +37,11 @@ PaymentsRouter.route( PaymentsRouter.route('/stripe/callback').get( StripeController.handleConnectOauthCallback, ) + +/** + * returns clientSecret and publishableKey from paymentIntentId + * @route /payments/:paymentIntentId/getInfo + */ +PaymentsRouter.route('/:paymentIntentId([a-fA-F0-9]{24}/getInfo').get( + StripeController.getPaymentInfo, +) From 33c8e1e436ede3fa7fb729a67a4557d5d8ea811d Mon Sep 17 00:00:00 2001 From: Ken Date: Fri, 24 Mar 2023 01:24:07 +0800 Subject: [PATCH 02/37] feat: add fetch payment model from paymentintent id --- src/app/modules/payments/payments.service.ts | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/app/modules/payments/payments.service.ts b/src/app/modules/payments/payments.service.ts index f570cb5a4a..9140a1d994 100644 --- a/src/app/modules/payments/payments.service.ts +++ b/src/app/modules/payments/payments.service.ts @@ -233,3 +233,35 @@ export const confirmPaymentPendingSubmission = ( ) }) } + +/** + * Retrieves payment associated with paymentIntentId from Stripe + * @param paymentIntentId the paymentIntentId from Stripe + * @returns ok(payment) if associated entry is found + * @returns err(PaymentNotFoundError) if the payment does not exist + * @returns err(DatabaseError) if error occurs whilst querying the database + */ +export const findPaymentByPaymentIntentId = ( + paymentIntentId: string, +): ResultAsync => { + return ResultAsync.fromPromise( + // TODO: what happens if we have multiple matches? + PaymentModel.findOne({ paymentIntentId }).exec(), + (error) => { + logger.error({ + message: 'Database find payment by paymentIntentId error', + meta: { + action: 'findPaymentByPaymentIntentId', + paymentIntentId, + }, + error, + }) + return new DatabaseError(getMongoErrorMessage(error)) + }, + ).andThen((payment) => { + if (!payment) { + return errAsync(new PaymentNotFoundError()) + } + return okAsync(payment) + }) +} From 72401d70d1907099c5eb15c06da14cebfcdc0bfe Mon Sep 17 00:00:00 2001 From: Ken Date: Fri, 24 Mar 2023 15:50:24 +0800 Subject: [PATCH 03/37] refactor(fe): move payment related modules to payment folder --- .../src/features/payment/FormPaymentPage.tsx | 4 +- .../FormPaymentRedirectPage.tsx | 6 +- .../components/DownloadReceiptBlock.tsx | 5 +- .../components/PaymentSuccessSvgr.tsx | 0 .../features/public-form/PublicFormPage.tsx | 3 +- src/app/modules/payments/payments.service.ts | 26 +++++++ src/app/modules/payments/stripe.controller.ts | 71 ++++++++++--------- .../routes/api/v3/payments/payments.routes.ts | 4 +- 8 files changed, 76 insertions(+), 43 deletions(-) rename frontend/src/features/{public-form/components => payment}/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx (89%) rename frontend/src/features/{public-form/components => payment}/FormPaymentRedirectPage/components/DownloadReceiptBlock.tsx (91%) rename frontend/src/features/{public-form/components => payment}/FormPaymentRedirectPage/components/PaymentSuccessSvgr.tsx (100%) diff --git a/frontend/src/features/payment/FormPaymentPage.tsx b/frontend/src/features/payment/FormPaymentPage.tsx index 709b9aedd6..8a92c0cfed 100644 --- a/frontend/src/features/payment/FormPaymentPage.tsx +++ b/frontend/src/features/payment/FormPaymentPage.tsx @@ -8,12 +8,12 @@ import { FormBanner } from '~features/public-form/components/FormBanner' import { FormSectionsProvider } from '~features/public-form/components/FormFields/FormSectionsContext' import { FormFooter } from '~features/public-form/components/FormFooter' import { PublicFormLogo } from '~features/public-form/components/FormLogo' -import { DownloadReceiptBlock } from '~features/public-form/components/FormPaymentRedirectPage/components/DownloadReceiptBlock' -import { PaymentSuccessSvgr } from '~features/public-form/components/FormPaymentRedirectPage/components/PaymentSuccessSvgr' import FormStartPage from '~features/public-form/components/FormStartPage' import { PublicFormWrapper } from '~features/public-form/components/PublicFormWrapper' import { PublicFormProvider } from '~features/public-form/PublicFormProvider' +import { DownloadReceiptBlock } from './FormPaymentRedirectPage/components/DownloadReceiptBlock' +import { PaymentSuccessSvgr } from './FormPaymentRedirectPage/components/PaymentSuccessSvgr' import { useGetPaymentInfo, useGetPaymentReceiptStatus } from './queries' export const FormPaymentPage = () => { diff --git a/frontend/src/features/public-form/components/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx b/frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx similarity index 89% rename from frontend/src/features/public-form/components/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx rename to frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx index b714b362c2..dc0d990c22 100644 --- a/frontend/src/features/public-form/components/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx +++ b/frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx @@ -2,8 +2,9 @@ import { useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { Box, Container, Flex, Skeleton } from '@chakra-ui/react' -import { usePublicFormContext } from '../../PublicFormContext' -import { useGetPaymentReceiptStatus } from '../../queries' +import { usePublicFormContext } from '~features/public-form/PublicFormContext' + +import { useGetPaymentReceiptStatus } from '../queries' import { DownloadReceiptBlock } from './components/DownloadReceiptBlock' import { PaymentSuccessSvgr } from './components/PaymentSuccessSvgr' @@ -12,6 +13,7 @@ type FormPaymentRedirectPageProps = { stripeSubmissionId: string } +// TODO: this file should be replaced by FormPaymentPage export const FormPaymentRedirectPage = ({ stripeSubmissionId, }: FormPaymentRedirectPageProps) => { diff --git a/frontend/src/features/public-form/components/FormPaymentRedirectPage/components/DownloadReceiptBlock.tsx b/frontend/src/features/payment/FormPaymentRedirectPage/components/DownloadReceiptBlock.tsx similarity index 91% rename from frontend/src/features/public-form/components/FormPaymentRedirectPage/components/DownloadReceiptBlock.tsx rename to frontend/src/features/payment/FormPaymentRedirectPage/components/DownloadReceiptBlock.tsx index f37d038d99..ab97a92d1a 100644 --- a/frontend/src/features/public-form/components/FormPaymentRedirectPage/components/DownloadReceiptBlock.tsx +++ b/frontend/src/features/payment/FormPaymentRedirectPage/components/DownloadReceiptBlock.tsx @@ -2,9 +2,8 @@ import { BiDownload } from 'react-icons/bi' import { Box, Stack, Text } from '@chakra-ui/react' import { useToast } from '~hooks/useToast' - -import Button from '../../../../../components/Button' -import { API_BASE_URL } from '../../../../../services/ApiService' +import { API_BASE_URL } from '~services/ApiService' +import Button from '~components/Button' type DownloadReceiptBlockProps = { formId: string diff --git a/frontend/src/features/public-form/components/FormPaymentRedirectPage/components/PaymentSuccessSvgr.tsx b/frontend/src/features/payment/FormPaymentRedirectPage/components/PaymentSuccessSvgr.tsx similarity index 100% rename from frontend/src/features/public-form/components/FormPaymentRedirectPage/components/PaymentSuccessSvgr.tsx rename to frontend/src/features/payment/FormPaymentRedirectPage/components/PaymentSuccessSvgr.tsx diff --git a/frontend/src/features/public-form/PublicFormPage.tsx b/frontend/src/features/public-form/PublicFormPage.tsx index 636ead7a25..e96a9355b3 100644 --- a/frontend/src/features/public-form/PublicFormPage.tsx +++ b/frontend/src/features/public-form/PublicFormPage.tsx @@ -4,6 +4,8 @@ import { Flex } from '@chakra-ui/react' import { fillMinHeightCss } from '~utils/fillHeightCss' +import { FormPaymentRedirectPage } from '~features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage' + import { FormBanner } from './components/FormBanner' import FormEndPage from './components/FormEndPage' import { FormPaymentPage } from './components/FormEndPage/FormPaymentPage' @@ -12,7 +14,6 @@ import { FormSectionsProvider } from './components/FormFields/FormSectionsContex import { FormFooter } from './components/FormFooter' import FormInstructions from './components/FormInstructions' import { PublicFormLogo } from './components/FormLogo' -import { FormPaymentRedirectPage } from './components/FormPaymentRedirectPage/FormPaymentRedirectPage' import FormStartPage from './components/FormStartPage' import { PublicFormWrapper } from './components/PublicFormWrapper' import { diff --git a/src/app/modules/payments/payments.service.ts b/src/app/modules/payments/payments.service.ts index 9140a1d994..fe5da3b501 100644 --- a/src/app/modules/payments/payments.service.ts +++ b/src/app/modules/payments/payments.service.ts @@ -265,3 +265,29 @@ export const findPaymentByPaymentIntentId = ( return okAsync(payment) }) } + +// export const findSubmissionByPaymentIntentId = (paymentIntentId: string) => { +// return ResultAsync.fromPromise( +// findPaymentByPaymentIntentId(paymentIntentId) +// .andThen((payment) => { +// if (!payment) { +// return errAsync(new PaymentNotFoundError()) +// } +// return okAsync(payment.submissionId) +// }) +// .andThen((submissionId) => +// SubmissionService.findSubmissionById(submissionId), +// ), +// (error) => { +// logger.error({ +// message: 'Database find submission by paymentIntentId error', +// meta: { +// action: 'findSubmissionByPaymentIntentId', +// paymentIntentId, +// }, +// error, +// }) +// return new DatabaseError(getMongoErrorMessage(error)) +// }, +// ) +// } diff --git a/src/app/modules/payments/stripe.controller.ts b/src/app/modules/payments/stripe.controller.ts index d2dc3db1bf..abdda68331 100644 --- a/src/app/modules/payments/stripe.controller.ts +++ b/src/app/modules/payments/stripe.controller.ts @@ -5,7 +5,7 @@ import { celebrate, Joi, Segments } from 'celebrate' import { StatusCodes } from 'http-status-codes' import get from 'lodash/get' import mongoose from 'mongoose' -import { ok, Result } from 'neverthrow' +import { ok, Result, ResultAsync } from 'neverthrow' import Stripe from 'stripe' import { ErrorDto, GetPaymentInfoDto } from '../../../../shared/types' @@ -19,6 +19,7 @@ import { createReqMeta } from '../../utils/request' import { ControllerHandler } from '../core/core.types' import { retrieveFullFormById } from '../form/form.service' import { checkFormIsEncryptMode } from '../submission/encrypt-submission/encrypt-submission.service' +import * as SubmissionService from '../submission/submission.service' import * as PaymentService from './payments.service' import * as StripeService from './stripe.service' @@ -390,6 +391,7 @@ export const handleConnectOauthCallback = [ _handleConnectOauthCallback, ] as ControllerHandler[] +// #TODO: think about where to place this payment fetcher, should it be strongly tied to stripe? export const getPaymentInfo: ControllerHandler< { paymentIntentId: string @@ -405,38 +407,41 @@ export const getPaymentInfo: ControllerHandler< }, }) - let stripeFullIntentObj - try { - stripeFullIntentObj = await stripe.paymentIntents.retrieve(paymentIntentId) - } catch (error) { - logger.error({ - message: 'getPaymentInfo endpoint called', - meta: { - action: 'getPaymentInfo', - paymentIntentId, - error, - }, + return PaymentService.findPaymentByPaymentIntentId(paymentIntentId) + .andThen((payment) => + SubmissionService.findSubmissionById(payment.submissionId), + ) + .andThen((submission) => + ResultAsync.fromPromise( + stripe.paymentIntents.retrieve(paymentIntentId, { + stripeAccount: submission.form.payments_channel.target_account_id, + }), + (error) => { + logger.error({ + message: 'stripe.paymentIntents.retrieve called', + meta: { + action: 'getPaymentInfo', + paymentIntentId, + error, + }, + }) + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .json({ message: 'Stripe retreival error' }) + }, + ), + ) + .map((stripeFullIntentObj) => { + console.log({ stripeFullIntentObj }) + return res.status(StatusCodes.OK).json({ + client_secret: stripeFullIntentObj.client_secret, + publishableKey: paymentConfig.stripePublishableKey, + }) }) - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .json({ message: 'Stripe retreival error' }) - } - - if (!stripeFullIntentObj?.client_secret) { - logger.error({ - message: 'Error occurred when reading client_secret', - meta: { - action: 'getPaymentInfo', - paymentIntentId, - }, + .mapErr((e) => { + console.error(e) + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .json({ message: 'Missing client secret' }) }) - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .json({ message: 'Missing client secret' }) - } - - return res.status(StatusCodes.OK).json({ - client_secret: stripeFullIntentObj.client_secret, - publishableKey: paymentConfig.stripePublishableKey, - }) } diff --git a/src/app/routes/api/v3/payments/payments.routes.ts b/src/app/routes/api/v3/payments/payments.routes.ts index 8a111db892..c2efa44ff9 100644 --- a/src/app/routes/api/v3/payments/payments.routes.ts +++ b/src/app/routes/api/v3/payments/payments.routes.ts @@ -16,7 +16,7 @@ PaymentsRouter.get('/stripe') * @returns 404 if receipt URL does not exist */ PaymentsRouter.route( - '/receipt/:formId([a-fA-F0-9]{24})/:paymentIntentId([a-fA-F0-9]{24}/status', + '/receipt/:formId([a-fA-F0-9]{24})/:paymentIntentId([a-fA-F0-9]{24})/status', ).get(StripeController.checkPaymentReceiptStatus) /** @@ -42,6 +42,6 @@ PaymentsRouter.route('/stripe/callback').get( * returns clientSecret and publishableKey from paymentIntentId * @route /payments/:paymentIntentId/getInfo */ -PaymentsRouter.route('/:paymentIntentId([a-fA-F0-9]{24}/getInfo').get( +PaymentsRouter.route('/:paymentIntentId/getInfo').get( StripeController.getPaymentInfo, ) From 7b3a6381e95e6d5915afdaebffdc84ec04901482 Mon Sep 17 00:00:00 2001 From: Ken Date: Mon, 27 Mar 2023 11:04:03 +0800 Subject: [PATCH 04/37] refactor: update payments/:formid/getInfo url to lowercase --- .../src/features/payment/FormPaymentPage.tsx | 49 +---- frontend/src/features/payment/queries.ts | 18 +- .../payment/stripe/StripePaymentModal.tsx | 193 ++++++++++++++++++ .../payment/stripe/StripePaymentWrapper.tsx | 130 ++++++++++++ frontend/src/features/payment/stripe/utils.ts | 10 + src/app/modules/payments/stripe.controller.ts | 67 ++++-- .../routes/api/v3/payments/payments.routes.ts | 2 +- 7 files changed, 408 insertions(+), 61 deletions(-) create mode 100644 frontend/src/features/payment/stripe/StripePaymentModal.tsx create mode 100644 frontend/src/features/payment/stripe/StripePaymentWrapper.tsx create mode 100644 frontend/src/features/payment/stripe/utils.ts diff --git a/frontend/src/features/payment/FormPaymentPage.tsx b/frontend/src/features/payment/FormPaymentPage.tsx index 8a92c0cfed..7b36ee5d76 100644 --- a/frontend/src/features/payment/FormPaymentPage.tsx +++ b/frontend/src/features/payment/FormPaymentPage.tsx @@ -1,6 +1,6 @@ -import { useEffect } from 'react' -import { useNavigate, useParams } from 'react-router-dom' -import { Box, Container, Flex, Skeleton } from '@chakra-ui/react' +import { Suspense } from 'react' +import { useParams } from 'react-router-dom' +import { Box, Container, Flex } from '@chakra-ui/react' import { fillMinHeightCss } from '~utils/fillHeightCss' @@ -12,9 +12,7 @@ import FormStartPage from '~features/public-form/components/FormStartPage' import { PublicFormWrapper } from '~features/public-form/components/PublicFormWrapper' import { PublicFormProvider } from '~features/public-form/PublicFormProvider' -import { DownloadReceiptBlock } from './FormPaymentRedirectPage/components/DownloadReceiptBlock' -import { PaymentSuccessSvgr } from './FormPaymentRedirectPage/components/PaymentSuccessSvgr' -import { useGetPaymentInfo, useGetPaymentReceiptStatus } from './queries' +import StripePaymentWrapper from './stripe/StripePaymentWrapper' export const FormPaymentPage = () => { const { formId, paymentPageId } = useParams() @@ -22,24 +20,6 @@ export const FormPaymentPage = () => { if (!formId) throw new Error('No formId provided') if (!paymentPageId) throw new Error('No paymentPageId provided') - const { data: paymentInfoData, error: paymentInfoError } = - useGetPaymentInfo(paymentPageId) - const { data, isLoading, error } = useGetPaymentReceiptStatus( - formId, - paymentPageId, - ) - - const navigate = useNavigate() - - useEffect(() => { - if (!isLoading && error) { - const currentUrl = new URL(window.location.href) - currentUrl.searchParams.set('retryPayment', 'true') - const urlPathAndSearch = currentUrl.pathname + currentUrl.search - navigate(urlPathAndSearch) - } - }, [error, isLoading, navigate]) - return ( @@ -50,24 +30,9 @@ export const FormPaymentPage = () => { - - - - - {data?.isReady ? ( - - ) : null} - - - + still loading}> + + diff --git a/frontend/src/features/payment/queries.ts b/frontend/src/features/payment/queries.ts index e40ecd5fcc..5922b0d8f4 100644 --- a/frontend/src/features/payment/queries.ts +++ b/frontend/src/features/payment/queries.ts @@ -1,4 +1,5 @@ import { useQuery, UseQueryResult } from 'react-query' +import { PaymentIntentResult, Stripe } from '@stripe/stripe-js' import { GetPaymentInfoDto, PaymentReceiptStatusDto } from '~shared/types' @@ -16,10 +17,23 @@ export const useGetPaymentReceiptStatus = ( ) } +export const useGetPaymentReceiptStatusFromStripe = ( + clientSecret: string, + stripe: Stripe, +) => { + return useQuery( + clientSecret, + () => stripe.retrievePaymentIntent(clientSecret), + { suspense: true }, + ) +} + export const useGetPaymentInfo = ( paymentId: string, ): UseQueryResult => { - return useQuery(paymentId, () => - getPaymentInfo(paymentId), + return useQuery( + paymentId, + () => getPaymentInfo(paymentId), + { suspense: true }, ) } diff --git a/frontend/src/features/payment/stripe/StripePaymentModal.tsx b/frontend/src/features/payment/stripe/StripePaymentModal.tsx new file mode 100644 index 0000000000..f2ec670bfc --- /dev/null +++ b/frontend/src/features/payment/stripe/StripePaymentModal.tsx @@ -0,0 +1,193 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { + Box, + Flex, + FormControl, + FormErrorMessage, + Stack, + Text, + VisuallyHidden, +} from '@chakra-ui/react' +import { + Elements, + PaymentElement, + useElements, + useStripe, +} from '@stripe/react-stripe-js' +import { loadStripe } from '@stripe/stripe-js' + +import { FormColorTheme, FormResponseMode } from '~shared/types/form' + +import { centsToDollars } from '~utils/payments' +import Button from '~components/Button' + +import { FormPaymentPageProps } from '~features/public-form/components/FormEndPage/FormPaymentPage' +import { usePublicFormContext } from '~features/public-form/PublicFormContext' + +// Make sure to call `loadStripe` outside of a component’s render to avoid +// recreating the `Stripe` object on every render. + +export interface PaymentPageBlockProps extends FormPaymentPageProps { + focusOnMount?: boolean +} + +type StripeCheckoutFormProps = Pick< + PaymentPageBlockProps, + 'submissionId' | 'isRetry' +> & { + colorTheme: FormColorTheme +} + +const StripeCheckoutForm = ({ + colorTheme, + submissionId, + isRetry, +}: StripeCheckoutFormProps) => { + const stripe = useStripe() + const elements = useElements() + + const [stripeMessage, setStripeMessage] = useState('') + const [isStripeProcessing, setIsStripeProcessing] = useState(false) + + useEffect(() => { + if (isRetry) { + setStripeMessage('Your payment attempt failed.') + } + }, [isRetry]) + + // Upon complete payment, redirect to ?stripeSubmissionId= + const return_url = window.location.href + + const handleSubmit = async (event: React.FormEvent) => { + // We don't want to let default form submission happen here, + // which would refresh the page. + event.preventDefault() + setIsStripeProcessing(true) + + if (!stripe || !elements) { + return null + // Stripe.js has not yet loaded. + // Make sure to disable form submission until Stripe.js has loaded. + } + + const result = await stripe.confirmPayment({ + //`Elements` instance that was used to create the Payment Element + elements, + confirmParams: { + return_url, + }, + }) + + if (result.error && result.error.message) { + // Show error to your customer (for example, payment details incomplete) + setStripeMessage(result.error.message) + } else { + setStripeMessage('') + // Your customer will be redirected to your `return_url`. For some payment + // methods like iDEAL, your customer will be redirected to an intermediate + // site first to authorize the payment, then redirected to the `return_url`. + } + setIsStripeProcessing(false) + } + + return ( +
+ + + {stripeMessage !== '' ? ( + + {`${stripeMessage} No payment has been taken. Please try again.`} + + ) : null} + + + +
+ ) +} + +export const StripePaymentModal = ({ + submissionId, + paymentClientSecret, + publishableKey, + focusOnMount, + isRetry, +}: PaymentPageBlockProps): JSX.Element => { + const { form } = usePublicFormContext() + + const formTitle = form?.title + const colorTheme = form?.startPage.colorTheme || FormColorTheme.Blue + + const stripePromise = useMemo( + () => loadStripe(publishableKey), + [publishableKey], + ) + + const focusRef = useRef(null) + useEffect(() => { + if (focusOnMount) { + focusRef.current?.focus() + } + }, [focusOnMount]) + + const submittedAriaText = useMemo(() => { + if (formTitle) { + return `Please make payment for ${formTitle}.` + } + return 'Please make payment.' + }, [formTitle]) + + return form?.responseMode === FormResponseMode.Encrypt ? ( + + + + + {submittedAriaText} + + + Payment + + + This amount is inclusive of GST + + + + Your credit card will be charged:{' '} + + S${centsToDollars(form?.payments_field?.amount_cents || 0)} + + + + {paymentClientSecret && ( + + + + )} + + Response ID: {submissionId} + + + ) : ( + <> + ) +} diff --git a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx b/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx new file mode 100644 index 0000000000..00f78494d1 --- /dev/null +++ b/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx @@ -0,0 +1,130 @@ +import { Suspense, useMemo } from 'react' +import { useParams } from 'react-router-dom' +import { Box, Code, Flex, Skeleton } from '@chakra-ui/react' +import { Elements, useStripe } from '@stripe/react-stripe-js' +import { loadStripe, Stripe } from '@stripe/stripe-js' + +import { GetPaymentInfoDto } from '~shared/types' + +import { PaymentSuccessSvgr } from '../FormPaymentRedirectPage/components/PaymentSuccessSvgr' +import { + useGetPaymentInfo, + useGetPaymentReceiptStatusFromStripe, +} from '../queries' + +import { StripePaymentModal } from './StripePaymentModal' +import { getPaymentViewType } from './utils' + +const StripePaymentWrapper = ({ paymentPageId }: { paymentPageId: string }) => { + const { data: paymentInfoData, error: paymentInfoError } = + useGetPaymentInfo(paymentPageId) + + console.log({ paymentInfoData, paymentInfoError }) + + if (!paymentInfoData) { + throw new Error('useGetPaymentInfo not ready') + } + const stripePromise = useMemo( + () => loadStripe(paymentInfoData.publishableKey), + [paymentInfoData], + ) + return ( + + + {JSON.stringify(paymentInfoData, null, 2)} + Loading Stripe Payment}> + + + + + ) +} + +const StripeWrapper = ({ + paymentInfoData, +}: { + paymentInfoData: GetPaymentInfoDto +}) => { + const stripe = useStripe() + if (!stripe) { + return loading stripe + } + return ( + + ) +} + +/** + * Handles decision to render StripePaymentModal or StripeReceiptModal + * @returns + */ +const StripePaymentContainer = ({ + paymentInfoData, + stripe, +}: { + paymentInfoData: GetPaymentInfoDto + stripe: Stripe +}) => { + const { formId, paymentPageId } = useParams() + + console.log('asd') + // if (!stripe) throw new Error('Stripe is not ready') + if (!formId) throw new Error('No formId provided') + if (!paymentPageId) throw new Error('No paymentPageId provided') + + const { data, isLoading, error } = useGetPaymentReceiptStatusFromStripe( + paymentInfoData.client_secret, + stripe, + ) + console.log({ isLoading, error, data }) + + const viewType = getPaymentViewType(data?.paymentIntent?.status) + let paymentViewElementElement + switch (viewType) { + case 'invalid': + paymentViewElementElement = null + break + case 'canceled': + paymentViewElementElement = {viewType} + break + case 'payment': + paymentViewElementElement = ( + + ) + break + case 'receipt': + paymentViewElementElement = {viewType} + // + break + } + return ( + <> + + + + {paymentViewElementElement} + + + + ) +} + +export default StripePaymentWrapper diff --git a/frontend/src/features/payment/stripe/utils.ts b/frontend/src/features/payment/stripe/utils.ts new file mode 100644 index 0000000000..ec2caae957 --- /dev/null +++ b/frontend/src/features/payment/stripe/utils.ts @@ -0,0 +1,10 @@ +import { PaymentIntent } from '@stripe/stripe-js' + +export const getPaymentViewType = ( + status: PaymentIntent.Status | undefined, +) => { + if (!status) return 'invalid' + if (['succeeded'].includes(status)) return 'receipt' + if (['canceled'].includes(status)) return 'canceled' + return 'payment' +} diff --git a/src/app/modules/payments/stripe.controller.ts b/src/app/modules/payments/stripe.controller.ts index abdda68331..25e1fa15ee 100644 --- a/src/app/modules/payments/stripe.controller.ts +++ b/src/app/modules/payments/stripe.controller.ts @@ -5,7 +5,7 @@ import { celebrate, Joi, Segments } from 'celebrate' import { StatusCodes } from 'http-status-codes' import get from 'lodash/get' import mongoose from 'mongoose' -import { ok, Result, ResultAsync } from 'neverthrow' +import { errAsync, ok, Result, ResultAsync } from 'neverthrow' import Stripe from 'stripe' import { ErrorDto, GetPaymentInfoDto } from '../../../../shared/types' @@ -17,11 +17,13 @@ import getPaymentModel from '../../models/payment.server.model' import { generatePdfFromHtml } from '../../utils/convert-html-to-pdf' import { createReqMeta } from '../../utils/request' import { ControllerHandler } from '../core/core.types' -import { retrieveFullFormById } from '../form/form.service' +import * as FormService from '../form/form.service' +import { isFormEncryptMode } from '../form/form.utils' import { checkFormIsEncryptMode } from '../submission/encrypt-submission/encrypt-submission.service' import * as SubmissionService from '../submission/submission.service' import * as PaymentService from './payments.service' +import { StripeFetchError } from './stripe.errors' import * as StripeService from './stripe.service' import { getChargeIdFromNestedCharge, @@ -351,7 +353,7 @@ const _handleConnectOauthCallback: ControllerHandler< const redirectUrl = `${config.app.appUrl}/admin/form/${formId}/settings` // Step 2: Retrieve currently logged in user. return ( - retrieveFullFormById(formId) + FormService.retrieveFullFormById(formId) .andThen(checkFormIsEncryptMode) .andThen((form) => StripeService.exchangeCodeForAccessToken(code).andThen((token) => { @@ -409,12 +411,43 @@ export const getPaymentInfo: ControllerHandler< return PaymentService.findPaymentByPaymentIntentId(paymentIntentId) .andThen((payment) => - SubmissionService.findSubmissionById(payment.submissionId), + // TODO: swap to pending submission service + SubmissionService.findSubmissionById(payment.pendingSubmissionId), ) - .andThen((submission) => - ResultAsync.fromPromise( + .andThen((submission) => { + return FormService.retrieveFormById(submission.form) + }) + .andThen((form) => { + // Payment forms are encrypted + if (!isFormEncryptMode(form)) { + logger.warn({ + message: + 'Requested for payment information for possibly non-payment form', + meta: { + action: 'getPaymentInfo', + paymentIntentId, + }, + }) + // TODO: change to error object + return errAsync(new Error('Is not a payment form')) + } + const stripeAccount = form.payments_channel?.target_account_id + // Early termination to prevent consumption of QPS to stripe + if (!stripeAccount) { + logger.warn({ + message: 'Missing payments_channel on this form', + meta: { + action: 'getPaymentInfo', + paymentIntentId, + }, + }) + // TODO: change to error object + return errAsync(new Error('Is not a payment form')) + } + + return ResultAsync.fromPromise( stripe.paymentIntents.retrieve(paymentIntentId, { - stripeAccount: submission.form.payments_channel.target_account_id, + stripeAccount, }), (error) => { logger.error({ @@ -425,23 +458,25 @@ export const getPaymentInfo: ControllerHandler< error, }, }) - return res - .status(StatusCodes.INTERNAL_SERVER_ERROR) - .json({ message: 'Stripe retreival error' }) + return new StripeFetchError(String(error)) }, - ), - ) - .map((stripeFullIntentObj) => { + ).map((stripeFullIntentObj) => ({ + stripeFullIntentObj, + // TODO: check publiashable key seemed to be tied to payment intent? + publishableKey: form.payments_channel?.publishable_key || '', + })) + }) + .map(({ stripeFullIntentObj, publishableKey }) => { console.log({ stripeFullIntentObj }) return res.status(StatusCodes.OK).json({ - client_secret: stripeFullIntentObj.client_secret, - publishableKey: paymentConfig.stripePublishableKey, + client_secret: stripeFullIntentObj.client_secret || '', + publishableKey, }) }) .mapErr((e) => { console.error(e) return res .status(StatusCodes.INTERNAL_SERVER_ERROR) - .json({ message: 'Missing client secret' }) + .json({ message: e.message }) }) } diff --git a/src/app/routes/api/v3/payments/payments.routes.ts b/src/app/routes/api/v3/payments/payments.routes.ts index c2efa44ff9..d24e0f82fa 100644 --- a/src/app/routes/api/v3/payments/payments.routes.ts +++ b/src/app/routes/api/v3/payments/payments.routes.ts @@ -42,6 +42,6 @@ PaymentsRouter.route('/stripe/callback').get( * returns clientSecret and publishableKey from paymentIntentId * @route /payments/:paymentIntentId/getInfo */ -PaymentsRouter.route('/:paymentIntentId/getInfo').get( +PaymentsRouter.route('/:paymentIntentId/getinfo').get( StripeController.getPaymentInfo, ) From 8dfeddc93fe40c887f40315f7bed351434e30df1 Mon Sep 17 00:00:00 2001 From: Ken Date: Mon, 27 Mar 2023 23:59:17 +0800 Subject: [PATCH 05/37] refactor: refine GetPaymentInfoDto type --- shared/types/payment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/types/payment.ts b/shared/types/payment.ts index 76883915f3..1574d9b30a 100644 --- a/shared/types/payment.ts +++ b/shared/types/payment.ts @@ -55,6 +55,6 @@ export type PaymentReceiptStatusDto = { } export type GetPaymentInfoDto = { - client_secret: string | null + client_secret: string publishableKey: string } From f14c402034ea926484cb66d96b3c6a8fa7cdd2ba Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 28 Mar 2023 00:20:28 +0800 Subject: [PATCH 06/37] chore: minor url casing update --- .../features/payment/FormPaymentService.ts | 2 +- .../public-form/PublicFormContext.tsx | 5 ++-- .../public-form/PublicFormProvider.tsx | 19 +++++++++++---- shared/types/submission.ts | 12 ++++++++-- .../encrypt-submission.controller.ts | 23 +++++++++++-------- .../routes/api/v3/payments/payments.routes.ts | 2 +- 6 files changed, 42 insertions(+), 21 deletions(-) diff --git a/frontend/src/features/payment/FormPaymentService.ts b/frontend/src/features/payment/FormPaymentService.ts index 939962cfc1..2aecd34e44 100644 --- a/frontend/src/features/payment/FormPaymentService.ts +++ b/frontend/src/features/payment/FormPaymentService.ts @@ -26,6 +26,6 @@ export const getPaymentReceiptStatus = async ( */ export const getPaymentInfo = async (paymentId: string) => { return ApiService.get( - `payments/${paymentId}/getInfo`, + `payments/${paymentId}/getinfo`, ).then(({ data }) => data) } diff --git a/frontend/src/features/public-form/PublicFormContext.tsx b/frontend/src/features/public-form/PublicFormContext.tsx index fcc17802d1..9efc88c0bc 100644 --- a/frontend/src/features/public-form/PublicFormContext.tsx +++ b/frontend/src/features/public-form/PublicFormContext.tsx @@ -2,6 +2,7 @@ import { createContext, RefObject, useContext } from 'react' import { UseQueryResult } from 'react-query' +import { PaymentSubmissionData } from '~shared/types' import { PublicFormViewDto } from '~shared/types/form' export type SubmissionData = { @@ -9,8 +10,8 @@ export type SubmissionData = { id: string | undefined /** Submission time in ms from epoch */ timestamp: number - paymentClientSecret?: string - paymentPublishableKey?: string + // payment forms will return a paymentIntentId + paymentData?: PaymentSubmissionData } export interface PublicFormContextProps diff --git a/frontend/src/features/public-form/PublicFormProvider.tsx b/frontend/src/features/public-form/PublicFormProvider.tsx index 1c90163c53..aea4876232 100644 --- a/frontend/src/features/public-form/PublicFormProvider.tsx +++ b/frontend/src/features/public-form/PublicFormProvider.tsx @@ -8,6 +8,7 @@ import { } from 'react' import { Helmet } from 'react-helmet-async' import { SubmitHandler } from 'react-hook-form' +import { useNavigate } from 'react-router-dom' import { useDisclosure } from '@chakra-ui/react' import { datadogLogs } from '@datadog/browser-logs' import { differenceInMilliseconds, isPast } from 'date-fns' @@ -188,6 +189,7 @@ export const PublicFormProvider = ({ const { handleLogoutMutation } = usePublicAuthMutations(formId) + const navigate = useNavigate() const handleSubmitForm: SubmitHandler< FormFieldValues & { payment_receipt_email_field?: { value: string } } > = useCallback( @@ -273,16 +275,23 @@ export const PublicFormProvider = ({ onSuccess: ({ submissionId, timestamp, - paymentClientSecret, - paymentPublishableKey, + // payment forms will return a paymentIntentId + paymentData, }) => { + trackSubmitForm(form) + + if (paymentData) { + navigate( + `/${formId}/payment/${paymentData.paymentIntentId}`, + ) + return + } + // TODO: ensure that there's no side effect for on function setSubmissionData({ id: submissionId, timestamp, - paymentClientSecret, - paymentPublishableKey, + paymentData, }) - trackSubmitForm(form) }, }, ) diff --git a/shared/types/submission.ts b/shared/types/submission.ts index 420e8cd6ab..52ee9e865d 100644 --- a/shared/types/submission.ts +++ b/shared/types/submission.ts @@ -103,8 +103,9 @@ export type SubmissionResponseDto = { submissionId: string // Timestamp is given as ms from epoch timestamp: number - paymentClientSecret?: string - paymentPublishableKey?: string + + // payment form only fields + paymentData?: PaymentSubmissionData } export type SubmissionErrorDto = ErrorDto & { spcpSubmissionFailure?: true } @@ -159,3 +160,10 @@ export type StorageModeSubmissionContentDto = { } export type StorageModePaymentSubmissionDto = Payment + +export type PaymentSubmissionData = { + // TODO: can remove payment clientsecret and publishablekey + paymentIntentId: string + paymentClientSecret: string + paymentPublishableKey: string +} diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts index bfe5cc30e4..f555287893 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts @@ -522,20 +522,23 @@ const submitEncryptModeForm: ControllerHandler< }) } + // Attach required payment configs if client secret is present. Otherwise, + // client will display error message. We still return 200 OK because the + // state is recoverable. + const paymentData = paymentClientSecret + ? { + paymentClientSecret, + paymentPublishableKey: form.payments_channel?.publishable_key, + paymentIntentId: paymentIntent?.id, + } + : {} + return res.json({ message: 'Form submission successful', submissionId: pendingSubmissionId, timestamp: (pendingSubmission.created || new Date()).getTime(), - // Attach required payment configs if client secret is present. Otherwise, - // client will display error message. We still return 200 OK because the - // state is recoverable. - ...(paymentClientSecret - ? { - paymentClientSecret, - paymentPublishableKey: form.payments_channel.publishable_key, - paymentIntentId: paymentIntent?.id, - } - : {}), + // hide paymentData obj in response if it is a payment form + ...(paymentData ? { paymentData } : {}), }) } diff --git a/src/app/routes/api/v3/payments/payments.routes.ts b/src/app/routes/api/v3/payments/payments.routes.ts index d24e0f82fa..d8257db92f 100644 --- a/src/app/routes/api/v3/payments/payments.routes.ts +++ b/src/app/routes/api/v3/payments/payments.routes.ts @@ -40,7 +40,7 @@ PaymentsRouter.route('/stripe/callback').get( /** * returns clientSecret and publishableKey from paymentIntentId - * @route /payments/:paymentIntentId/getInfo + * @route /payments/:paymentIntentId/getinfo */ PaymentsRouter.route('/:paymentIntentId/getinfo').get( StripeController.getPaymentInfo, From 79371fc03732fbe229bc0c4580c9af32e59d6421 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 28 Mar 2023 01:07:59 +0800 Subject: [PATCH 07/37] feat: receipt block to show after payment --- .../payment/stripe/StripePaymentWrapper.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx b/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx index 00f78494d1..813e121284 100644 --- a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx +++ b/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx @@ -6,6 +6,7 @@ import { loadStripe, Stripe } from '@stripe/stripe-js' import { GetPaymentInfoDto } from '~shared/types' +import { DownloadReceiptBlock } from '../FormPaymentRedirectPage/components/DownloadReceiptBlock' import { PaymentSuccessSvgr } from '../FormPaymentRedirectPage/components/PaymentSuccessSvgr' import { useGetPaymentInfo, @@ -37,7 +38,11 @@ const StripePaymentWrapper = ({ paymentPageId }: { paymentPageId: string }) => { }} > - {JSON.stringify(paymentInfoData, null, 2)} + +
+            {JSON.stringify(paymentInfoData, null, 2)}
+          
+
Loading Stripe Payment}> @@ -103,11 +108,12 @@ const StripePaymentContainer = ({ ) break case 'receipt': - paymentViewElementElement = {viewType} - // + paymentViewElementElement = ( + + ) break } return ( From 4ee2282e40a8f86db1d395e81b4fdaa307bef979 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 28 Mar 2023 11:16:46 +0800 Subject: [PATCH 08/37] refactor: move payment elements to payment folder --- .../FormPaymentRedirectPage.tsx | 5 +-- .../components/DownloadReceiptBlock.tsx | 0 .../components/PaymentSuccessSvgr.tsx | 0 .../features/public-form/PublicFormPage.tsx | 43 +++---------------- 4 files changed, 7 insertions(+), 41 deletions(-) rename frontend/src/features/payment/{FormPaymentRedirectPage => }/components/DownloadReceiptBlock.tsx (100%) rename frontend/src/features/payment/{FormPaymentRedirectPage => }/components/PaymentSuccessSvgr.tsx (100%) diff --git a/frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx b/frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx index dc0d990c22..e892486964 100644 --- a/frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx +++ b/frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx @@ -4,11 +4,10 @@ import { Box, Container, Flex, Skeleton } from '@chakra-ui/react' import { usePublicFormContext } from '~features/public-form/PublicFormContext' +import { DownloadReceiptBlock } from '../components/DownloadReceiptBlock' +import { PaymentSuccessSvgr } from '../components/PaymentSuccessSvgr' import { useGetPaymentReceiptStatus } from '../queries' -import { DownloadReceiptBlock } from './components/DownloadReceiptBlock' -import { PaymentSuccessSvgr } from './components/PaymentSuccessSvgr' - type FormPaymentRedirectPageProps = { stripeSubmissionId: string } diff --git a/frontend/src/features/payment/FormPaymentRedirectPage/components/DownloadReceiptBlock.tsx b/frontend/src/features/payment/components/DownloadReceiptBlock.tsx similarity index 100% rename from frontend/src/features/payment/FormPaymentRedirectPage/components/DownloadReceiptBlock.tsx rename to frontend/src/features/payment/components/DownloadReceiptBlock.tsx diff --git a/frontend/src/features/payment/FormPaymentRedirectPage/components/PaymentSuccessSvgr.tsx b/frontend/src/features/payment/components/PaymentSuccessSvgr.tsx similarity index 100% rename from frontend/src/features/payment/FormPaymentRedirectPage/components/PaymentSuccessSvgr.tsx rename to frontend/src/features/payment/components/PaymentSuccessSvgr.tsx diff --git a/frontend/src/features/public-form/PublicFormPage.tsx b/frontend/src/features/public-form/PublicFormPage.tsx index e96a9355b3..6b3efffe8d 100644 --- a/frontend/src/features/public-form/PublicFormPage.tsx +++ b/frontend/src/features/public-form/PublicFormPage.tsx @@ -1,14 +1,10 @@ -import { useMemo } from 'react' -import { useParams, useSearchParams } from 'react-router-dom' +import { useParams } from 'react-router-dom' import { Flex } from '@chakra-ui/react' import { fillMinHeightCss } from '~utils/fillHeightCss' -import { FormPaymentRedirectPage } from '~features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage' - import { FormBanner } from './components/FormBanner' import FormEndPage from './components/FormEndPage' -import { FormPaymentPage } from './components/FormEndPage/FormPaymentPage' import FormFields from './components/FormFields' import { FormSectionsProvider } from './components/FormFields/FormSectionsContext' import { FormFooter } from './components/FormFooter' @@ -16,11 +12,6 @@ import FormInstructions from './components/FormInstructions' import { PublicFormLogo } from './components/FormLogo' import FormStartPage from './components/FormStartPage' import { PublicFormWrapper } from './components/PublicFormWrapper' -import { - RETRY_PAYMENT_KEY, - STRIPE_PAYMENT_SECRET_KEY, - STRIPE_SUBMISSION_ID_KEY, -} from './constants' import { PublicFormProvider } from './PublicFormProvider' export const PublicFormPage = (): JSX.Element => { @@ -28,15 +19,6 @@ export const PublicFormPage = (): JSX.Element => { if (!formId) throw new Error('No formId provided') - const [searchParams] = useSearchParams() - const { stripeSubmissionId, retryPayment, clientSecret } = useMemo(() => { - return { - stripeSubmissionId: searchParams.get(STRIPE_SUBMISSION_ID_KEY), - retryPayment: searchParams.get(RETRY_PAYMENT_KEY) === 'true', - clientSecret: searchParams.get(STRIPE_PAYMENT_SECRET_KEY), - } - }, [searchParams]) - return ( @@ -45,25 +27,10 @@ export const PublicFormPage = (): JSX.Element => { - {retryPayment && stripeSubmissionId && clientSecret ? ( - - ) : stripeSubmissionId ? ( - - ) : ( - <> - - - - - )} + + + +
From fdf600e0b125b429272bc52fa4a37c93eb0dd821 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 28 Mar 2023 12:59:08 +0800 Subject: [PATCH 09/37] refactor: cleanup old payment block --- .../src/features/payment/FormPaymentPage.tsx | 7 + .../CreatePaymentIntentFailureBlock.tsx | 2 +- .../components/LoadingReceiptBlock.tsx | 32 +++ .../payment/stripe/StripePaymentWrapper.tsx | 47 +++-- .../FormEndPage/FormEndPageContainer.tsx | 28 +-- .../FormEndPage/FormPaymentPage.tsx | 34 --- .../components/PaymentPageBlock.tsx | 196 ------------------ 7 files changed, 83 insertions(+), 263 deletions(-) rename frontend/src/features/{public-form/components/FormEndPage => payment}/components/CreatePaymentIntentFailureBlock.tsx (96%) create mode 100644 frontend/src/features/payment/components/LoadingReceiptBlock.tsx delete mode 100644 frontend/src/features/public-form/components/FormEndPage/FormPaymentPage.tsx delete mode 100644 frontend/src/features/public-form/components/FormEndPage/components/PaymentPageBlock.tsx diff --git a/frontend/src/features/payment/FormPaymentPage.tsx b/frontend/src/features/payment/FormPaymentPage.tsx index 7b36ee5d76..f23fe5a639 100644 --- a/frontend/src/features/payment/FormPaymentPage.tsx +++ b/frontend/src/features/payment/FormPaymentPage.tsx @@ -14,6 +14,13 @@ import { PublicFormProvider } from '~features/public-form/PublicFormProvider' import StripePaymentWrapper from './stripe/StripePaymentWrapper' +export interface FormPaymentPageProps { + submissionId: string + paymentClientSecret: string + publishableKey: string + isRetry?: boolean +} + export const FormPaymentPage = () => { const { formId, paymentPageId } = useParams() diff --git a/frontend/src/features/public-form/components/FormEndPage/components/CreatePaymentIntentFailureBlock.tsx b/frontend/src/features/payment/components/CreatePaymentIntentFailureBlock.tsx similarity index 96% rename from frontend/src/features/public-form/components/FormEndPage/components/CreatePaymentIntentFailureBlock.tsx rename to frontend/src/features/payment/components/CreatePaymentIntentFailureBlock.tsx index c7fe0e9b7f..b90c434d45 100644 --- a/frontend/src/features/public-form/components/FormEndPage/components/CreatePaymentIntentFailureBlock.tsx +++ b/frontend/src/features/payment/components/CreatePaymentIntentFailureBlock.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef } from 'react' import { Box, Flex, Stack, Text, VisuallyHidden } from '@chakra-ui/react' -import { usePublicFormContext } from '../../../PublicFormContext' +import { usePublicFormContext } from '../../public-form/PublicFormContext' import { FormPaymentPageProps } from '../FormPaymentPage' // Make sure to call `loadStripe` outside of a component’s render to avoid diff --git a/frontend/src/features/payment/components/LoadingReceiptBlock.tsx b/frontend/src/features/payment/components/LoadingReceiptBlock.tsx new file mode 100644 index 0000000000..715bb51641 --- /dev/null +++ b/frontend/src/features/payment/components/LoadingReceiptBlock.tsx @@ -0,0 +1,32 @@ +import { BiDownload } from 'react-icons/bi' +import { Box, Stack, Text } from '@chakra-ui/react' + +import { useToast } from '~hooks/useToast' +import { API_BASE_URL } from '~services/ApiService' +import Button from '~components/Button' + +type DownloadReceiptBlockProps = { + formId: string + stripeSubmissionId: string +} + +export const LoadingReceiptBlock = ({ + formId, + stripeSubmissionId, +}: DownloadReceiptBlockProps) => { + return ( + + + + Your payment has been made successfully. + + + We're confirming your payment with Stripe + + + + Response ID: {stripeSubmissionId} + + + ) +} diff --git a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx b/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx index 813e121284..8ce143d920 100644 --- a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx +++ b/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx @@ -1,4 +1,4 @@ -import { Suspense, useMemo } from 'react' +import { Suspense, useMemo, useState } from 'react' import { useParams } from 'react-router-dom' import { Box, Code, Flex, Skeleton } from '@chakra-ui/react' import { Elements, useStripe } from '@stripe/react-stripe-js' @@ -6,8 +6,10 @@ import { loadStripe, Stripe } from '@stripe/stripe-js' import { GetPaymentInfoDto } from '~shared/types' -import { DownloadReceiptBlock } from '../FormPaymentRedirectPage/components/DownloadReceiptBlock' -import { PaymentSuccessSvgr } from '../FormPaymentRedirectPage/components/PaymentSuccessSvgr' +import { CreatePaymentIntentFailureBlock } from '~features/payment/components/CreatePaymentIntentFailureBlock' + +import { DownloadReceiptBlock } from '../components/DownloadReceiptBlock' +import { PaymentSuccessSvgr } from '../components/PaymentSuccessSvgr' import { useGetPaymentInfo, useGetPaymentReceiptStatusFromStripe, @@ -20,6 +22,7 @@ const StripePaymentWrapper = ({ paymentPageId }: { paymentPageId: string }) => { const { data: paymentInfoData, error: paymentInfoError } = useGetPaymentInfo(paymentPageId) + const [debugText, setDebugText] = useState('') console.log({ paymentInfoData, paymentInfoError }) if (!paymentInfoData) { @@ -40,11 +43,18 @@ const StripePaymentWrapper = ({ paymentPageId }: { paymentPageId: string }) => {
-            {JSON.stringify(paymentInfoData, null, 2)}
+            
+              {JSON.stringify(paymentInfoData, null, 2)}
+              
+ {debugText} +
Loading Stripe Payment}> - +
@@ -53,15 +63,21 @@ const StripePaymentWrapper = ({ paymentPageId }: { paymentPageId: string }) => { const StripeWrapper = ({ paymentInfoData, + setDebugText, }: { paymentInfoData: GetPaymentInfoDto + setDebugText: (text: string) => void }) => { const stripe = useStripe() if (!stripe) { return loading stripe } return ( - + ) } @@ -72,13 +88,13 @@ const StripeWrapper = ({ const StripePaymentContainer = ({ paymentInfoData, stripe, + setDebugText, }: { paymentInfoData: GetPaymentInfoDto stripe: Stripe + setDebugText: (text: string) => void }) => { const { formId, paymentPageId } = useParams() - - console.log('asd') // if (!stripe) throw new Error('Stripe is not ready') if (!formId) throw new Error('No formId provided') if (!paymentPageId) throw new Error('No paymentPageId provided') @@ -90,10 +106,19 @@ const StripePaymentContainer = ({ console.log({ isLoading, error, data }) const viewType = getPaymentViewType(data?.paymentIntent?.status) + setDebugText( + JSON.stringify({ viewType, status: data?.paymentIntent?.status }, null, 2), + ) let paymentViewElementElement switch (viewType) { case 'invalid': - paymentViewElementElement = null + paymentViewElementElement = ( + + ) break case 'canceled': paymentViewElementElement = {viewType} @@ -125,9 +150,7 @@ const StripePaymentContainer = ({ bg="white" w="100%" > - - {paymentViewElementElement} - + {paymentViewElementElement} ) diff --git a/frontend/src/features/public-form/components/FormEndPage/FormEndPageContainer.tsx b/frontend/src/features/public-form/components/FormEndPage/FormEndPageContainer.tsx index db968d1000..8d00660139 100644 --- a/frontend/src/features/public-form/components/FormEndPage/FormEndPageContainer.tsx +++ b/frontend/src/features/public-form/components/FormEndPage/FormEndPageContainer.tsx @@ -1,8 +1,6 @@ import { useCallback, useState } from 'react' import { Box } from '@chakra-ui/react' -import { FormResponseMode } from '~shared/types' - import { useToast } from '~hooks/useToast' import { usePublicFormMutations } from '~features/public-form/mutations' @@ -10,7 +8,6 @@ import { usePublicFormContext } from '~features/public-form/PublicFormContext' import { FeedbackFormInput } from './components/FeedbackBlock' import { FormEndPage } from './FormEndPage' -import { FormPaymentPage } from './FormPaymentPage' interface FormEndPageContainerProps { isPreview?: boolean @@ -65,23 +62,14 @@ export const FormEndPageContainer = ({ return ( - {form?.responseMode === FormResponseMode.Encrypt && - form?.payments_field?.enabled ? ( - - ) : ( - - )} + ) } diff --git a/frontend/src/features/public-form/components/FormEndPage/FormPaymentPage.tsx b/frontend/src/features/public-form/components/FormEndPage/FormPaymentPage.tsx deleted file mode 100644 index f20bbde96f..0000000000 --- a/frontend/src/features/public-form/components/FormEndPage/FormPaymentPage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Container, Flex, Stack, StackDivider } from '@chakra-ui/react' - -import { CreatePaymentIntentFailureBlock } from './components/CreatePaymentIntentFailureBlock' -import { PaymentPageBlock } from './components/PaymentPageBlock' - -export interface FormPaymentPageProps { - submissionId: string - paymentClientSecret: string - publishableKey: string - isRetry?: boolean -} - -export const FormPaymentPage = (props: FormPaymentPageProps): JSX.Element => { - return ( - - - } - > - {props.paymentClientSecret ? ( - - ) : ( - - )} - - - - ) -} diff --git a/frontend/src/features/public-form/components/FormEndPage/components/PaymentPageBlock.tsx b/frontend/src/features/public-form/components/FormEndPage/components/PaymentPageBlock.tsx deleted file mode 100644 index f3c20d72a6..0000000000 --- a/frontend/src/features/public-form/components/FormEndPage/components/PaymentPageBlock.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from 'react' -import { - Box, - Flex, - FormControl, - Stack, - Text, - VisuallyHidden, -} from '@chakra-ui/react' -import { - Elements, - PaymentElement, - useElements, - useStripe, -} from '@stripe/react-stripe-js' -import { loadStripe } from '@stripe/stripe-js' - -import { FormColorTheme, FormResponseMode } from '~shared/types/form' - -import { centsToDollars } from '~utils/payments' -import Button from '~components/Button' - -import FormErrorMessage from '../../../../../components/FormControl/FormErrorMessage' -import { STRIPE_SUBMISSION_ID_KEY } from '../../../constants' -import { usePublicFormContext } from '../../../PublicFormContext' -import { FormPaymentPageProps } from '../FormPaymentPage' - -// Make sure to call `loadStripe` outside of a component’s render to avoid -// recreating the `Stripe` object on every render. - -export interface PaymentPageBlockProps extends FormPaymentPageProps { - focusOnMount?: boolean -} - -type StripeCheckoutFormProps = Pick< - PaymentPageBlockProps, - 'submissionId' | 'isRetry' -> & { - colorTheme: FormColorTheme -} - -const StripeCheckoutForm = ({ - colorTheme, - submissionId, - isRetry, -}: StripeCheckoutFormProps) => { - const stripe = useStripe() - const elements = useElements() - - const [stripeMessage, setStripeMessage] = useState('') - const [isStripeProcessing, setIsStripeProcessing] = useState(false) - - useEffect(() => { - if (isRetry) { - setStripeMessage('Your payment attempt failed.') - } - }, [isRetry]) - - // Upon complete payment, redirect to ?stripeSubmissionId= - const return_url = `${ - window.location.href.split('?')[0] - }?${STRIPE_SUBMISSION_ID_KEY}=${submissionId}` - - const handleSubmit = async (event: React.FormEvent) => { - // We don't want to let default form submission happen here, - // which would refresh the page. - event.preventDefault() - setIsStripeProcessing(true) - - if (!stripe || !elements) { - return null - // Stripe.js has not yet loaded. - // Make sure to disable form submission until Stripe.js has loaded. - } - - const result = await stripe.confirmPayment({ - //`Elements` instance that was used to create the Payment Element - elements, - confirmParams: { - return_url, - }, - }) - - if (result.error && result.error.message) { - // Show error to your customer (for example, payment details incomplete) - setStripeMessage(result.error.message) - } else { - setStripeMessage('') - // Your customer will be redirected to your `return_url`. For some payment - // methods like iDEAL, your customer will be redirected to an intermediate - // site first to authorize the payment, then redirected to the `return_url`. - } - setIsStripeProcessing(false) - } - - return ( -
- - - {stripeMessage !== '' ? ( - - {`${stripeMessage} No payment has been taken. Please try again.`} - - ) : null} - - - -
- ) -} - -export const PaymentPageBlock = ({ - submissionId, - paymentClientSecret, - publishableKey, - focusOnMount, - isRetry, -}: PaymentPageBlockProps): JSX.Element => { - const { form } = usePublicFormContext() - - const formTitle = form?.title - const colorTheme = form?.startPage.colorTheme || FormColorTheme.Blue - - const stripePromise = useMemo( - () => loadStripe(publishableKey), - [publishableKey], - ) - - const focusRef = useRef(null) - useEffect(() => { - if (focusOnMount) { - focusRef.current?.focus() - } - }, [focusOnMount]) - - const submittedAriaText = useMemo(() => { - if (formTitle) { - return `Please make payment for ${formTitle}.` - } - return 'Please make payment.' - }, [formTitle]) - - return form?.responseMode === FormResponseMode.Encrypt ? ( - - - - - {submittedAriaText} - - - Payment - - - This amount is inclusive of GST - - - - Your credit card will be charged:{' '} - - S${centsToDollars(form?.payments_field?.amount_cents || 0)} - - - - {paymentClientSecret && ( - - - - )} - - Response ID: {submissionId} - - - ) : ( - <> - ) -} From 873b1cc6e74ec6ce2230926c421b3ba30073d0f5 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 28 Mar 2023 13:00:15 +0800 Subject: [PATCH 10/37] add refreshkey to refetch stripe PI after payment --- frontend/src/features/payment/queries.ts | 15 ++++++++++----- .../payment/stripe/StripePaymentModal.tsx | 14 ++++++++++++-- .../payment/stripe/StripePaymentWrapper.tsx | 9 ++++++--- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/frontend/src/features/payment/queries.ts b/frontend/src/features/payment/queries.ts index 5922b0d8f4..e69d652edb 100644 --- a/frontend/src/features/payment/queries.ts +++ b/frontend/src/features/payment/queries.ts @@ -17,12 +17,17 @@ export const useGetPaymentReceiptStatus = ( ) } -export const useGetPaymentReceiptStatusFromStripe = ( - clientSecret: string, - stripe: Stripe, -) => { +export const useGetPaymentReceiptStatusFromStripe = ({ + clientSecret, + stripe, + refetchKey, // a nonce which triggers refetch if changed +}: { + clientSecret: string + stripe: Stripe + refetchKey: number +}) => { return useQuery( - clientSecret, + [clientSecret, refetchKey], () => stripe.retrievePaymentIntent(clientSecret), { suspense: true }, ) diff --git a/frontend/src/features/payment/stripe/StripePaymentModal.tsx b/frontend/src/features/payment/stripe/StripePaymentModal.tsx index f2ec670bfc..fd15f802fe 100644 --- a/frontend/src/features/payment/stripe/StripePaymentModal.tsx +++ b/frontend/src/features/payment/stripe/StripePaymentModal.tsx @@ -21,14 +21,17 @@ import { FormColorTheme, FormResponseMode } from '~shared/types/form' import { centsToDollars } from '~utils/payments' import Button from '~components/Button' -import { FormPaymentPageProps } from '~features/public-form/components/FormEndPage/FormPaymentPage' import { usePublicFormContext } from '~features/public-form/PublicFormContext' +import { FormPaymentPageProps } from '../FormPaymentPage' + // Make sure to call `loadStripe` outside of a component’s render to avoid // recreating the `Stripe` object on every render. export interface PaymentPageBlockProps extends FormPaymentPageProps { focusOnMount?: boolean + // TODO: want to refactor FormPaymentPageProps into payment + triggerPaymentStatusRefetch: () => void } type StripeCheckoutFormProps = Pick< @@ -36,12 +39,13 @@ type StripeCheckoutFormProps = Pick< 'submissionId' | 'isRetry' > & { colorTheme: FormColorTheme + triggerPaymentStatusRefetch: () => void } const StripeCheckoutForm = ({ colorTheme, - submissionId, isRetry, + triggerPaymentStatusRefetch, }: StripeCheckoutFormProps) => { const stripe = useStripe() const elements = useElements() @@ -76,6 +80,7 @@ const StripeCheckoutForm = ({ confirmParams: { return_url, }, + redirect: 'if_required', }) if (result.error && result.error.message) { @@ -86,6 +91,9 @@ const StripeCheckoutForm = ({ // Your customer will be redirected to your `return_url`. For some payment // methods like iDEAL, your customer will be redirected to an intermediate // site first to authorize the payment, then redirected to the `return_url`. + + // in the event that customer is not redirected, we will trigger a payment status refetch + triggerPaymentStatusRefetch() } setIsStripeProcessing(false) } @@ -122,6 +130,7 @@ export const StripePaymentModal = ({ publishableKey, focusOnMount, isRetry, + triggerPaymentStatusRefetch, }: PaymentPageBlockProps): JSX.Element => { const { form } = usePublicFormContext() @@ -180,6 +189,7 @@ export const StripePaymentModal = ({ colorTheme={colorTheme} submissionId={submissionId} isRetry={isRetry} + triggerPaymentStatusRefetch={triggerPaymentStatusRefetch} /> )} diff --git a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx b/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx index 8ce143d920..0b34c8bbbf 100644 --- a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx +++ b/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx @@ -99,10 +99,12 @@ const StripePaymentContainer = ({ if (!formId) throw new Error('No formId provided') if (!paymentPageId) throw new Error('No paymentPageId provided') - const { data, isLoading, error } = useGetPaymentReceiptStatusFromStripe( - paymentInfoData.client_secret, + const [refetchKey, setRefetchKey] = useState(0) + const { data, isLoading, error } = useGetPaymentReceiptStatusFromStripe({ + clientSecret: paymentInfoData.client_secret, stripe, - ) + refetchKey, + }) console.log({ isLoading, error, data }) const viewType = getPaymentViewType(data?.paymentIntent?.status) @@ -129,6 +131,7 @@ const StripePaymentContainer = ({ submissionId={paymentPageId} paymentClientSecret={paymentInfoData.client_secret} publishableKey={paymentInfoData.publishableKey} + triggerPaymentStatusRefetch={() => setRefetchKey(Date.now())} /> ) break From 9117cd07e9e26bb87b3ed5da2e04f173144e5701 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 28 Mar 2023 13:02:24 +0800 Subject: [PATCH 11/37] add missing default case in switch --- frontend/src/features/payment/stripe/StripePaymentWrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx b/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx index 0b34c8bbbf..c73061669d 100644 --- a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx +++ b/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx @@ -36,7 +36,6 @@ const StripePaymentWrapper = ({ paymentPageId }: { paymentPageId: string }) => { @@ -95,7 +94,6 @@ const StripePaymentContainer = ({ setDebugText: (text: string) => void }) => { const { formId, paymentPageId } = useParams() - // if (!stripe) throw new Error('Stripe is not ready') if (!formId) throw new Error('No formId provided') if (!paymentPageId) throw new Error('No paymentPageId provided') @@ -143,6 +141,8 @@ const StripePaymentContainer = ({ /> ) break + default: + throw new Error(`Undefined view type: ${viewType}`) } return ( <> From 50470f3b7c3774525b0f189346fb9309809bfc80 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 28 Mar 2023 14:13:36 +0800 Subject: [PATCH 12/37] fix: cleanup suspense flashes --- .../src/features/payment/FormPaymentPage.tsx | 16 ++++-- ...ntWrapper.tsx => StripeElementWrapper.tsx} | 49 +++++++++++-------- .../encrypt-submission.controller.ts | 2 +- 3 files changed, 41 insertions(+), 26 deletions(-) rename frontend/src/features/payment/stripe/{StripePaymentWrapper.tsx => StripeElementWrapper.tsx} (82%) diff --git a/frontend/src/features/payment/FormPaymentPage.tsx b/frontend/src/features/payment/FormPaymentPage.tsx index f23fe5a639..8bb336992f 100644 --- a/frontend/src/features/payment/FormPaymentPage.tsx +++ b/frontend/src/features/payment/FormPaymentPage.tsx @@ -1,6 +1,6 @@ import { Suspense } from 'react' import { useParams } from 'react-router-dom' -import { Box, Container, Flex } from '@chakra-ui/react' +import { Box, Container, Flex, Skeleton, Text } from '@chakra-ui/react' import { fillMinHeightCss } from '~utils/fillHeightCss' @@ -12,7 +12,7 @@ import FormStartPage from '~features/public-form/components/FormStartPage' import { PublicFormWrapper } from '~features/public-form/components/PublicFormWrapper' import { PublicFormProvider } from '~features/public-form/PublicFormProvider' -import StripePaymentWrapper from './stripe/StripePaymentWrapper' +import StripeElementWrapper from './stripe/StripeElementWrapper' export interface FormPaymentPageProps { submissionId: string @@ -37,8 +37,16 @@ export const FormPaymentPage = () => { - still loading}> - + + + Loading Payment Information + + + } + > + diff --git a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx b/frontend/src/features/payment/stripe/StripeElementWrapper.tsx similarity index 82% rename from frontend/src/features/payment/stripe/StripePaymentWrapper.tsx rename to frontend/src/features/payment/stripe/StripeElementWrapper.tsx index c73061669d..0b9569d5e7 100644 --- a/frontend/src/features/payment/stripe/StripePaymentWrapper.tsx +++ b/frontend/src/features/payment/stripe/StripeElementWrapper.tsx @@ -1,6 +1,6 @@ -import { Suspense, useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useParams } from 'react-router-dom' -import { Box, Code, Flex, Skeleton } from '@chakra-ui/react' +import { Box, Code, Flex } from '@chakra-ui/react' import { Elements, useStripe } from '@stripe/react-stripe-js' import { loadStripe, Stripe } from '@stripe/stripe-js' @@ -18,7 +18,7 @@ import { import { StripePaymentModal } from './StripePaymentModal' import { getPaymentViewType } from './utils' -const StripePaymentWrapper = ({ paymentPageId }: { paymentPageId: string }) => { +const StripeElementWrapper = ({ paymentPageId }: { paymentPageId: string }) => { const { data: paymentInfoData, error: paymentInfoError } = useGetPaymentInfo(paymentPageId) @@ -49,18 +49,16 @@ const StripePaymentWrapper = ({ paymentPageId }: { paymentPageId: string }) => { - Loading Stripe Payment}> - - + ) } -const StripeWrapper = ({ +const StripeHookWrapper = ({ paymentInfoData, setDebugText, }: { @@ -69,7 +67,7 @@ const StripeWrapper = ({ }) => { const stripe = useStripe() if (!stripe) { - return loading stripe + throw Promise.reject('Stripe is not ready') } return ( { + setDebugText( + JSON.stringify( + { viewType, status: data?.paymentIntent?.status }, + null, + 2, + ), + ) + }, [setDebugText, viewType, data]) + let paymentViewElementElement switch (viewType) { case 'invalid': @@ -135,10 +140,12 @@ const StripePaymentContainer = ({ break case 'receipt': paymentViewElementElement = ( - + <> + + ) break default: @@ -146,17 +153,17 @@ const StripePaymentContainer = ({ } return ( <> - + {viewType === 'receipt' ? : null} - {paymentViewElementElement} + {paymentViewElementElement} ) } -export default StripePaymentWrapper +export default StripeElementWrapper diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts index f555287893..537657d4bf 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts @@ -531,7 +531,7 @@ const submitEncryptModeForm: ControllerHandler< paymentPublishableKey: form.payments_channel?.publishable_key, paymentIntentId: paymentIntent?.id, } - : {} + : null return res.json({ message: 'Form submission successful', From c3b0080f61ad05b10e85eb715e34027afbd2eeb8 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 28 Mar 2023 17:06:47 +0800 Subject: [PATCH 13/37] refactor: receipt loading, download block --- .../FormPaymentRedirectPage.tsx | 61 ------------ .../features/payment/FormPaymentService.ts | 8 +- frontend/src/features/payment/queries.ts | 24 +---- .../payment/stripe/StripeElementWrapper.tsx | 92 ++++++++++--------- .../payment/stripe/StripeReceiptContainer.tsx | 23 +++++ .../StripeDownloadReceiptBlock.tsx} | 8 +- .../components/StripeLoadingReceiptBlock.tsx} | 15 +-- .../StripePaymentBlock.tsx} | 6 +- .../features/payment/stripe/stripeQueries.ts | 20 ++++ .../routes/api/v3/payments/payments.routes.ts | 18 ++-- 10 files changed, 115 insertions(+), 160 deletions(-) delete mode 100644 frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx create mode 100644 frontend/src/features/payment/stripe/StripeReceiptContainer.tsx rename frontend/src/features/payment/{components/DownloadReceiptBlock.tsx => stripe/components/StripeDownloadReceiptBlock.tsx} (90%) rename frontend/src/features/payment/{components/LoadingReceiptBlock.tsx => stripe/components/StripeLoadingReceiptBlock.tsx} (61%) rename frontend/src/features/payment/stripe/{StripePaymentModal.tsx => components/StripePaymentBlock.tsx} (97%) create mode 100644 frontend/src/features/payment/stripe/stripeQueries.ts diff --git a/frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx b/frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx deleted file mode 100644 index e892486964..0000000000 --- a/frontend/src/features/payment/FormPaymentRedirectPage/FormPaymentRedirectPage.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect } from 'react' -import { useNavigate } from 'react-router-dom' -import { Box, Container, Flex, Skeleton } from '@chakra-ui/react' - -import { usePublicFormContext } from '~features/public-form/PublicFormContext' - -import { DownloadReceiptBlock } from '../components/DownloadReceiptBlock' -import { PaymentSuccessSvgr } from '../components/PaymentSuccessSvgr' -import { useGetPaymentReceiptStatus } from '../queries' - -type FormPaymentRedirectPageProps = { - stripeSubmissionId: string -} - -// TODO: this file should be replaced by FormPaymentPage -export const FormPaymentRedirectPage = ({ - stripeSubmissionId, -}: FormPaymentRedirectPageProps) => { - const { formId } = usePublicFormContext() - - const { data, isLoading, error } = useGetPaymentReceiptStatus( - formId, - stripeSubmissionId, - ) - - const navigate = useNavigate() - - useEffect(() => { - if (!isLoading && error) { - const currentUrl = new URL(window.location.href) - currentUrl.searchParams.set('retryPayment', 'true') - const urlPathAndSearch = currentUrl.pathname + currentUrl.search - navigate(urlPathAndSearch) - } - }, [error, isLoading, navigate]) - - return ( - - - - - - - {data?.isReady ? ( - - ) : null} - - - - - - ) -} diff --git a/frontend/src/features/payment/FormPaymentService.ts b/frontend/src/features/payment/FormPaymentService.ts index 2aecd34e44..821abc45d3 100644 --- a/frontend/src/features/payment/FormPaymentService.ts +++ b/frontend/src/features/payment/FormPaymentService.ts @@ -5,15 +5,15 @@ import { ApiService } from '~services/ApiService' /** * Obtain payment receipt status for a given submission. * @param formId the id of the form - * @param submissionId the id of the form submission + * @param paymentId the id of the payment submission * @returns PaymentReceiptStatusDto on success */ export const getPaymentReceiptStatus = async ( formId: string, - submissionId: string, + paymentId: string, ): Promise => { return ApiService.get( - `payments/receipt/${formId}/${submissionId}/status`, + `payments/receipt/${formId}/${paymentId}/status`, ).then(({ data }) => data) } @@ -21,7 +21,7 @@ export const getPaymentReceiptStatus = async ( * Obtain payment information neccessary to do a subsequent * for a given paymentId. * @param formId the id of the form - * @param submissionId the id of the form submission + * @param submissionId the id of the payment submission * @returns PaymentReceiptStatusDto on success */ export const getPaymentInfo = async (paymentId: string) => { diff --git a/frontend/src/features/payment/queries.ts b/frontend/src/features/payment/queries.ts index e69d652edb..b9c1ac79f3 100644 --- a/frontend/src/features/payment/queries.ts +++ b/frontend/src/features/payment/queries.ts @@ -1,5 +1,4 @@ import { useQuery, UseQueryResult } from 'react-query' -import { PaymentIntentResult, Stripe } from '@stripe/stripe-js' import { GetPaymentInfoDto, PaymentReceiptStatusDto } from '~shared/types' @@ -9,27 +8,10 @@ import { getPaymentInfo, getPaymentReceiptStatus } from './FormPaymentService' export const useGetPaymentReceiptStatus = ( formId: string, - submissionId: string, + paymentId: string, ): UseQueryResult => { - return useQuery( - [formId, submissionId], - () => getPaymentReceiptStatus(formId, submissionId), - ) -} - -export const useGetPaymentReceiptStatusFromStripe = ({ - clientSecret, - stripe, - refetchKey, // a nonce which triggers refetch if changed -}: { - clientSecret: string - stripe: Stripe - refetchKey: number -}) => { - return useQuery( - [clientSecret, refetchKey], - () => stripe.retrievePaymentIntent(clientSecret), - { suspense: true }, + return useQuery([formId, paymentId], () => + getPaymentReceiptStatus(formId, paymentId), ) } diff --git a/frontend/src/features/payment/stripe/StripeElementWrapper.tsx b/frontend/src/features/payment/stripe/StripeElementWrapper.tsx index 0b9569d5e7..1e366610d4 100644 --- a/frontend/src/features/payment/stripe/StripeElementWrapper.tsx +++ b/frontend/src/features/payment/stripe/StripeElementWrapper.tsx @@ -1,4 +1,5 @@ -import { useEffect, useMemo, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' +import { ReactElement } from 'react-markdown/lib/react-markdown' import { useParams } from 'react-router-dom' import { Box, Code, Flex } from '@chakra-ui/react' import { Elements, useStripe } from '@stripe/react-stripe-js' @@ -8,14 +9,12 @@ import { GetPaymentInfoDto } from '~shared/types' import { CreatePaymentIntentFailureBlock } from '~features/payment/components/CreatePaymentIntentFailureBlock' -import { DownloadReceiptBlock } from '../components/DownloadReceiptBlock' import { PaymentSuccessSvgr } from '../components/PaymentSuccessSvgr' -import { - useGetPaymentInfo, - useGetPaymentReceiptStatusFromStripe, -} from '../queries' +import { useGetPaymentInfo } from '../queries' -import { StripePaymentModal } from './StripePaymentModal' +import { StripePaymentBlock } from './components/StripePaymentBlock' +import { useGetPaymentStatusFromStripe } from './stripeQueries' +import { StripeReceiptContainer } from './StripeReceiptContainer' import { getPaymentViewType } from './utils' const StripeElementWrapper = ({ paymentPageId }: { paymentPageId: string }) => { @@ -78,6 +77,16 @@ const StripeHookWrapper = ({ ) } +const PaymentBox = ({ children }: { children: React.ReactNode }) => ( + + {children} + +) /** * Handles decision to render StripePaymentModal or StripeReceiptModal * @returns @@ -96,12 +105,11 @@ const StripePaymentContainer = ({ if (!paymentPageId) throw new Error('No paymentPageId provided') const [refetchKey, setRefetchKey] = useState(0) - const { data, isLoading, error } = useGetPaymentReceiptStatusFromStripe({ + const { data, isLoading, error } = useGetPaymentStatusFromStripe({ clientSecret: paymentInfoData.client_secret, stripe, refetchKey, }) - console.log({ isLoading, error, data }) const viewType = getPaymentViewType(data?.paymentIntent?.status) useEffect(() => { @@ -114,56 +122,50 @@ const StripePaymentContainer = ({ ) }, [setDebugText, viewType, data]) - let paymentViewElementElement switch (viewType) { case 'invalid': - paymentViewElementElement = ( - + return ( + + + ) - break case 'canceled': - paymentViewElementElement = {viewType} + return ( + + {viewType} + + ) break case 'payment': - paymentViewElementElement = ( - setRefetchKey(Date.now())} - /> + return ( + + setRefetchKey(Date.now())} + /> + ) - break case 'receipt': - paymentViewElementElement = ( + return ( <> - + + + + ) - break default: throw new Error(`Undefined view type: ${viewType}`) } - return ( - <> - {viewType === 'receipt' ? : null} - - {paymentViewElementElement} - - - ) } export default StripeElementWrapper diff --git a/frontend/src/features/payment/stripe/StripeReceiptContainer.tsx b/frontend/src/features/payment/stripe/StripeReceiptContainer.tsx new file mode 100644 index 0000000000..2e5e62e86b --- /dev/null +++ b/frontend/src/features/payment/stripe/StripeReceiptContainer.tsx @@ -0,0 +1,23 @@ +import { useGetPaymentReceiptStatus } from '../queries' + +import { DownloadReceiptBlock } from './components/StripeDownloadReceiptBlock' +import { StripeLoadingReceiptBlock } from './components/StripeLoadingReceiptBlock' + +export const StripeReceiptContainer = ({ + formId, + paymentPageId, +}: { + formId: string + paymentPageId: string +}) => { + const { data, isLoading, error } = useGetPaymentReceiptStatus( + formId, + paymentPageId, + ) + console.log({ isLoading, error, data }) + + if (isLoading || error) { + return + } + return +} diff --git a/frontend/src/features/payment/components/DownloadReceiptBlock.tsx b/frontend/src/features/payment/stripe/components/StripeDownloadReceiptBlock.tsx similarity index 90% rename from frontend/src/features/payment/components/DownloadReceiptBlock.tsx rename to frontend/src/features/payment/stripe/components/StripeDownloadReceiptBlock.tsx index ab97a92d1a..7a0947a53c 100644 --- a/frontend/src/features/payment/components/DownloadReceiptBlock.tsx +++ b/frontend/src/features/payment/stripe/components/StripeDownloadReceiptBlock.tsx @@ -7,12 +7,12 @@ import Button from '~components/Button' type DownloadReceiptBlockProps = { formId: string - stripeSubmissionId: string + paymentPageId: string } export const DownloadReceiptBlock = ({ formId, - stripeSubmissionId, + paymentPageId, }: DownloadReceiptBlockProps) => { const toast = useToast({ status: 'success', isClosable: true }) @@ -20,7 +20,7 @@ export const DownloadReceiptBlock = ({ toast({ description: 'Receipt download started', }) - window.location.href = `${API_BASE_URL}/payments/receipt/${formId}/${stripeSubmissionId}/download` + window.location.href = `${API_BASE_URL}/payments/receipt/${formId}/${paymentPageId}/download` } return ( @@ -33,7 +33,7 @@ export const DownloadReceiptBlock = ({ - Response ID: {stripeSubmissionId} + Response ID: {paymentPageId} + + + + + + + ) +} From a2a3223975c9855bbc0211f1f8f53c33b7b8d912 Mon Sep 17 00:00:00 2001 From: Ken Date: Mon, 3 Apr 2023 15:38:44 +0800 Subject: [PATCH 26/37] feat: add payment local storage hook --- frontend/src/hooks/payments/index.ts | 1 + .../src/hooks/payments/useBrowserStm/index.ts | 57 +++++++++++++++++++ .../src/hooks/payments/useBrowserStm/utils.ts | 46 +++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 frontend/src/hooks/payments/index.ts create mode 100644 frontend/src/hooks/payments/useBrowserStm/index.ts create mode 100644 frontend/src/hooks/payments/useBrowserStm/utils.ts diff --git a/frontend/src/hooks/payments/index.ts b/frontend/src/hooks/payments/index.ts new file mode 100644 index 0000000000..9a39985181 --- /dev/null +++ b/frontend/src/hooks/payments/index.ts @@ -0,0 +1 @@ +export { useBrowserStm } from './useBrowserStm' diff --git a/frontend/src/hooks/payments/useBrowserStm/index.ts b/frontend/src/hooks/payments/useBrowserStm/index.ts new file mode 100644 index 0000000000..23bdb54c39 --- /dev/null +++ b/frontend/src/hooks/payments/useBrowserStm/index.ts @@ -0,0 +1,57 @@ +import { useLocalStorage } from '../../useLocalStorage' + +import { + addEntry, + deleteEntry, + deserialize, + getEntry, + processEviction, + serialize, + StmEntryDto, +} from './utils' + +const PAYMENT_STM_KEY = 'PAYMENT_STM_KEY' +/** + * In local storage, add a marker that a form submission and payment + * has been submitted and is ongoing. The marker must be cleared when + * the payment flow is complete. The marker also contains a eviction policy + * of maxmium 1 day, that will be assess whenever this hook is used. + * + * Returns an array of three variables: + * + * - `lastPaymentMemory` returns the previous value. It will be an empty string if + * there's no previous value in memory + * + * - `storePaymentMemory` expects the paymentId that will be stored. Replacing + * the previous value, if any. + * + * - `clearPaymentMemory` sets the memory to be an empty string + * + * @returns lastPaymentMemory + */ +export const useBrowserStm = ( + formId: string, +): [ + lastPaymentMemory: StmEntryDto | undefined, + storePaymentMemory: (paymentId: string) => void, + clearPaymentMemory: () => void, +] => { + const [paymentMemory, setPaymentMemory] = useLocalStorage( + PAYMENT_STM_KEY, + JSON.stringify({}), + ) + + const entryObj = deserialize(paymentMemory || '') + processEviction(entryObj) + + const lastPaymentMemory = getEntry(entryObj, formId) + const storePaymentMemory = (paymentId: string) => { + const updatedMemory = addEntry(entryObj, { formId, paymentId }) + setPaymentMemory(serialize(updatedMemory)) + } + const clearPaymentMemory = () => { + const updatedMemory = deleteEntry(entryObj, { formId }) + setPaymentMemory(serialize(updatedMemory)) + } + return [lastPaymentMemory, storePaymentMemory, clearPaymentMemory] +} diff --git a/frontend/src/hooks/payments/useBrowserStm/utils.ts b/frontend/src/hooks/payments/useBrowserStm/utils.ts new file mode 100644 index 0000000000..0aa8cba07d --- /dev/null +++ b/frontend/src/hooks/payments/useBrowserStm/utils.ts @@ -0,0 +1,46 @@ +export type StmEntryDto = { + paymentId: string + dateCreated: number +} +export type BrowserStmDto = { + [formId: string]: StmEntryDto +} +export const deserialize = (jsonString: string): BrowserStmDto => { + try { + return JSON.parse(jsonString) + } catch { + return {} + } +} +export const serialize = (entryObj: BrowserStmDto) => { + return JSON.stringify(entryObj) +} +export const processEviction = (data: BrowserStmDto) => { + // asd +} + +export const addEntry = ( + entryObj: BrowserStmDto, + { formId, paymentId }: { formId: string; paymentId: string }, +): BrowserStmDto => { + entryObj[formId] = { + paymentId, + dateCreated: Number(new Date()), + } + return entryObj +} + +export const deleteEntry = ( + entryObj: BrowserStmDto, + { formId }: { formId: string }, +): BrowserStmDto => { + delete entryObj[formId] + return entryObj +} + +export const getEntry = ( + entryObj: BrowserStmDto, + formId: string, +): StmEntryDto => { + return entryObj[formId] +} From c3acee3a6f19d754435dd31b2af324487cd86a0e Mon Sep 17 00:00:00 2001 From: Ken Date: Mon, 3 Apr 2023 15:56:15 +0800 Subject: [PATCH 27/37] feat: add modal popup flow --- frontend/src/features/public-form/PublicFormProvider.tsx | 4 ++++ .../features/public-form/components/FormFields/FormFields.tsx | 2 ++ .../FormPaymentPage/stripe/components/StripePaymentBlock.tsx | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/frontend/src/features/public-form/PublicFormProvider.tsx b/frontend/src/features/public-form/PublicFormProvider.tsx index ffe40639db..7dcb795c2d 100644 --- a/frontend/src/features/public-form/PublicFormProvider.tsx +++ b/frontend/src/features/public-form/PublicFormProvider.tsx @@ -22,6 +22,7 @@ import { } from '~shared/types/form' import { FORMID_REGEX } from '~constants/routes' +import { useBrowserStm } from '~hooks/payments' import { useTimeout } from '~hooks/useTimeout' import { useToast } from '~hooks/useToast' import { getPaymentPageUrl } from '~utils/urls' @@ -191,6 +192,7 @@ export const PublicFormProvider = ({ const { handleLogoutMutation } = usePublicAuthMutations(formId) const navigate = useNavigate() + const [, storePaymentMemory] = useBrowserStm(formId) const handleSubmitForm: SubmitHandler< FormFieldValues & { payment_receipt_email_field?: { value: string } } > = useCallback( @@ -283,6 +285,7 @@ export const PublicFormProvider = ({ if (paymentData) { navigate(getPaymentPageUrl(formId, paymentData.paymentId)) + storePaymentMemory(paymentData.paymentId) return } setSubmissionData({ @@ -330,6 +333,7 @@ export const PublicFormProvider = ({ submitStorageModeFormMutation, formId, navigate, + storePaymentMemory, ], ) diff --git a/frontend/src/features/public-form/components/FormFields/FormFields.tsx b/frontend/src/features/public-form/components/FormFields/FormFields.tsx index 906df61dd7..f7371d93e3 100644 --- a/frontend/src/features/public-form/components/FormFields/FormFields.tsx +++ b/frontend/src/features/public-form/components/FormFields/FormFields.tsx @@ -19,6 +19,7 @@ import { import { useFetchPrefillQuery } from '~features/public-form/hooks/useFetchPrefillQuery' import { usePublicFormContext } from '~features/public-form/PublicFormContext' +import { PublicFormPaymentResumeModal } from '../FormPaymentPage/FormPaymentResumeModal' import { FormPaymentPreview } from '../FormPaymentPreview/FormPaymentPreview' import { PublicFormSubmitButton } from './PublicFormSubmitButton' @@ -136,6 +137,7 @@ export const FormFields = ({ paymentDetails={form.payments_field} /> )} + { + const { formId } = usePublicFormContext() const stripe = useStripe() const elements = useElements() const [stripeMessage, setStripeMessage] = useState('') const [isStripeProcessing, setIsStripeProcessing] = useState(false) + const [, , clearPaymentMemory] = useBrowserStm(formId) useEffect(() => { if (isRetry) { @@ -95,6 +98,7 @@ const StripeCheckoutForm = ({ // In the event that customer is not on a payment that has a redirected flow, // we will trigger a payment status refetch triggerPaymentStatusRefetch() + clearPaymentMemory() } setIsStripeProcessing(false) } From d48e0737922a29ddcf42127a68bb2e985f9867ba Mon Sep 17 00:00:00 2001 From: Ken Date: Mon, 3 Apr 2023 18:00:53 +0800 Subject: [PATCH 28/37] fix: incorrect jest types definition version --- frontend/package-lock.json | 722 ++++++------------------------------- frontend/package.json | 2 +- 2 files changed, 119 insertions(+), 605 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 40da5c43ea..cb2fd6daf4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -109,7 +109,7 @@ "@types/dedent": "^0.7.0", "@types/file-saver": "^2.0.3", "@types/gtag.js": "0.0.10", - "@types/jest": "^29.2.4", + "@types/jest": "^26.0.14", "@types/node": "^16.11.12", "@types/react": "^17.0.37", "@types/react-beautiful-dnd": "^13.1.2", @@ -4303,18 +4303,6 @@ "node": ">=8" } }, - "node_modules/@jest/expect-utils": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.3.1.tgz", - "integrity": "sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==", - "dev": true, - "dependencies": { - "jest-get-type": "^29.2.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/fake-timers": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", @@ -4688,18 +4676,6 @@ "node": ">=8" } }, - "node_modules/@jest/schemas": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", - "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.24.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/source-map": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", @@ -5614,12 +5590,6 @@ "rollup": "^1.20.0||^2.0.0" } }, - "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", - "dev": true - }, "node_modules/@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -9018,16 +8988,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/addon-storyshots/node_modules/@types/jest": { - "version": "26.0.24", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", - "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", - "dev": true, - "dependencies": { - "jest-diff": "^26.0.0", - "pretty-format": "^26.0.0" - } - }, "node_modules/@storybook/addon-storyshots/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9121,15 +9081,6 @@ "node": ">=10" } }, - "node_modules/@storybook/addon-storyshots/node_modules/diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/@storybook/addon-storyshots/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -9164,30 +9115,6 @@ "node": ">=0.10.0" } }, - "node_modules/@storybook/addon-storyshots/node_modules/jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/@storybook/addon-storyshots/node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/@storybook/addon-storyshots/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -15560,13 +15487,13 @@ } }, "node_modules/@types/jest": { - "version": "29.2.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.4.tgz", - "integrity": "sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A==", + "version": "26.0.24", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", + "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", "dev": true, "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" } }, "node_modules/@types/jest-image-snapshot": { @@ -15590,29 +15517,19 @@ } }, "node_modules/@types/jest/node_modules/@jest/types": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.3.1.tgz", - "integrity": "sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.0.0", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", - "@types/yargs": "^17.0.8", + "@types/yargs": "^15.0.0", "chalk": "^4.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/@types/yargs": { - "version": "17.0.17", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.17.tgz", - "integrity": "sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" + "node": ">= 10.14.2" } }, "node_modules/@types/jest/node_modules/ansi-styles": { @@ -15646,15 +15563,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@types/jest/node_modules/ci-info": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", - "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@types/jest/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -15673,22 +15581,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/@types/jest/node_modules/expect": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.3.1.tgz", - "integrity": "sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==", - "dev": true, - "dependencies": { - "@jest/expect-utils": "^29.3.1", - "jest-get-type": "^29.2.0", - "jest-matcher-utils": "^29.3.1", - "jest-message-util": "^29.3.1", - "jest-util": "^29.3.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@types/jest/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -15698,88 +15590,25 @@ "node": ">=8" } }, - "node_modules/@types/jest/node_modules/jest-matcher-utils": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.3.1.tgz", - "integrity": "sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.3.1", - "jest-get-type": "^29.2.0", - "pretty-format": "^29.3.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-message-util": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.3.1.tgz", - "integrity": "sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.3.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.3.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/jest-util": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.3.1.tgz", - "integrity": "sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==", - "dev": true, - "dependencies": { - "@jest/types": "^29.3.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@types/jest/node_modules/pretty-format": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", - "integrity": "sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", "dev": true, "dependencies": { - "@jest/schemas": "^29.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 10" } }, "node_modules/@types/jest/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, "node_modules/@types/jest/node_modules/supports-color": { @@ -24307,12 +24136,12 @@ } }, "node_modules/diff-sequences": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", - "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", "dev": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 10.14.2" } }, "node_modules/diffie-hellman": { @@ -26896,15 +26725,6 @@ "node": ">=8" } }, - "node_modules/expect/node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/expect/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -31440,15 +31260,6 @@ "node": ">=8" } }, - "node_modules/jest-config/node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/jest-config/node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -31483,18 +31294,34 @@ } }, "node_modules/jest-diff": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.3.1.tgz", - "integrity": "sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.3.1", - "jest-get-type": "^29.2.0", - "pretty-format": "^29.3.1" + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 10.14.2" + } + }, + "node_modules/jest-diff/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" } }, "node_modules/jest-diff/node_modules/ansi-styles": { @@ -31556,35 +31383,24 @@ } }, "node_modules/jest-diff/node_modules/pretty-format": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", - "integrity": "sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", "dev": true, "dependencies": { - "@jest/schemas": "^29.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 10" } }, "node_modules/jest-diff/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, "node_modules/jest-diff/node_modules/supports-color": { @@ -31701,15 +31517,6 @@ "node": ">=8" } }, - "node_modules/jest-each/node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/jest-each/node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -31977,12 +31784,12 @@ } }, "node_modules/jest-get-type": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", - "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 10.14.2" } }, "node_modules/jest-haste-map": { @@ -32429,15 +32236,6 @@ "node": ">=8" } }, - "node_modules/jest-leak-detector/node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/jest-leak-detector/node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -32551,15 +32349,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/jest-matcher-utils/node_modules/diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/jest-matcher-utils/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -32569,30 +32358,6 @@ "node": ">=8" } }, - "node_modules/jest-matcher-utils/node_modules/jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-matcher-utils/node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -33509,15 +33274,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/jest-snapshot/node_modules/diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/jest-snapshot/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -33527,30 +33283,6 @@ "node": ">=8" } }, - "node_modules/jest-snapshot/node_modules/jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/jest-snapshot/node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/jest-snapshot/node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -33802,15 +33534,6 @@ "node": ">=8" } }, - "node_modules/jest-validate/node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, "node_modules/jest-validate/node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -54190,15 +53913,6 @@ } } }, - "@jest/expect-utils": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.3.1.tgz", - "integrity": "sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==", - "dev": true, - "requires": { - "jest-get-type": "^29.2.0" - } - }, "@jest/fake-timers": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", @@ -54483,15 +54197,6 @@ } } }, - "@jest/schemas": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", - "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.24.1" - } - }, "@jest/source-map": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", @@ -55176,12 +54881,6 @@ "picomatch": "^2.2.2" } }, - "@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", - "dev": true - }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -57487,16 +57186,6 @@ "pretty-hrtime": "^1.0.3" } }, - "@types/jest": { - "version": "26.0.24", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", - "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", - "dev": true, - "requires": { - "jest-diff": "^26.0.0", - "pretty-format": "^26.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -57565,12 +57254,6 @@ "yaml": "^1.10.0" } }, - "diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true - }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -57593,24 +57276,6 @@ "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", "dev": true }, - "jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - } - }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -62472,38 +62137,28 @@ } }, "@types/jest": { - "version": "29.2.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.4.tgz", - "integrity": "sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A==", + "version": "26.0.24", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", + "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", "dev": true, "requires": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" }, "dependencies": { "@jest/types": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.3.1.tgz", - "integrity": "sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", "dev": true, "requires": { - "@jest/schemas": "^29.0.0", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", - "@types/yargs": "^17.0.8", + "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, - "@types/yargs": { - "version": "17.0.17", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.17.tgz", - "integrity": "sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -62523,12 +62178,6 @@ "supports-color": "^7.1.0" } }, - "ci-info": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", - "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", - "dev": true - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -62544,91 +62193,28 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "expect": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.3.1.tgz", - "integrity": "sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==", - "dev": true, - "requires": { - "@jest/expect-utils": "^29.3.1", - "jest-get-type": "^29.2.0", - "jest-matcher-utils": "^29.3.1", - "jest-message-util": "^29.3.1", - "jest-util": "^29.3.1" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-matcher-utils": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.3.1.tgz", - "integrity": "sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^29.3.1", - "jest-get-type": "^29.2.0", - "pretty-format": "^29.3.1" - } - }, - "jest-message-util": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.3.1.tgz", - "integrity": "sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.3.1", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.3.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - } - }, - "jest-util": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.3.1.tgz", - "integrity": "sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==", - "dev": true, - "requires": { - "@jest/types": "^29.3.1", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - } - }, "pretty-format": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", - "integrity": "sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", "dev": true, "requires": { - "@jest/schemas": "^29.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" } }, "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, "supports-color": { @@ -69210,9 +68796,9 @@ "dev": true }, "diff-sequences": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", - "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", "dev": true }, "diffie-hellman": { @@ -71085,12 +70671,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -74715,12 +74295,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, "pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -74751,17 +74325,30 @@ } }, "jest-diff": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.3.1.tgz", - "integrity": "sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", "dev": true, "requires": { "chalk": "^4.0.0", - "diff-sequences": "^29.3.1", - "jest-get-type": "^29.2.0", - "pretty-format": "^29.3.1" + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" }, "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -74803,28 +74390,21 @@ "dev": true }, "pretty-format": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", - "integrity": "sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", "dev": true, "requires": { - "@jest/schemas": "^29.0.0", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" } }, "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, "supports-color": { @@ -74913,12 +74493,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, "pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -75126,9 +74700,9 @@ } }, "jest-get-type": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", - "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true }, "jest-haste-map": { @@ -75468,12 +75042,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, "pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -75562,36 +75130,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - } - }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, "pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -76298,36 +75842,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - } - }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, "pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -76517,12 +76037,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, "pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index ea5723c86f..fc1f68faa4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -149,7 +149,7 @@ "@types/dedent": "^0.7.0", "@types/file-saver": "^2.0.3", "@types/gtag.js": "0.0.10", - "@types/jest": "^29.2.4", + "@types/jest": "^26.0.14", "@types/node": "^16.11.12", "@types/react": "^17.0.37", "@types/react-beautiful-dnd": "^13.1.2", From 9bbcbd0b644c7fecaf3fa70ff1e2d58e008ad41e Mon Sep 17 00:00:00 2001 From: Ken Date: Mon, 3 Apr 2023 18:48:43 +0800 Subject: [PATCH 29/37] add test cases for browser stm utils --- .../useBrowserStm/__tests__/utils.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 frontend/src/hooks/payments/useBrowserStm/__tests__/utils.test.ts diff --git a/frontend/src/hooks/payments/useBrowserStm/__tests__/utils.test.ts b/frontend/src/hooks/payments/useBrowserStm/__tests__/utils.test.ts new file mode 100644 index 0000000000..950ad05b06 --- /dev/null +++ b/frontend/src/hooks/payments/useBrowserStm/__tests__/utils.test.ts @@ -0,0 +1,39 @@ +import { addEntry, getEntry, processEviction } from '../utils' + +const EMPTY_OBJ = {} +describe('useBrowserStm', () => { + describe('processEviction', () => { + afterEach(() => { + jest.useRealTimers() + }) + it('should only be called once if called in quick succession', () => { + // Arrange + + // Act + const result1 = processEviction(EMPTY_OBJ) + const result2 = processEviction(EMPTY_OBJ) + + // Assert + expect(result1).toEqual(result2) + }) + + it('should evict entries that are old', () => { + // Arrange + const tempId = 'form1' + const mockDate = new Date('2020-12-21') + + jest.useFakeTimers('modern').setSystemTime(mockDate) + const entryObj = addEntry(EMPTY_OBJ, { + formId: tempId, + paymentId: 'payment1', + }) + jest.useRealTimers() + + // Act + processEviction(entryObj) + + // Assert + expect(getEntry(entryObj, tempId)).toBeFalsy() + }) + }) +}) From c59572b64c06d51d6cf3d12c851a2bb4faf35bfa Mon Sep 17 00:00:00 2001 From: Ken Date: Mon, 3 Apr 2023 18:49:30 +0800 Subject: [PATCH 30/37] refactor: remove side effects, switch to throttle --- .../src/hooks/payments/useBrowserStm/utils.ts | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/frontend/src/hooks/payments/useBrowserStm/utils.ts b/frontend/src/hooks/payments/useBrowserStm/utils.ts index 0aa8cba07d..53c1717392 100644 --- a/frontend/src/hooks/payments/useBrowserStm/utils.ts +++ b/frontend/src/hooks/payments/useBrowserStm/utils.ts @@ -1,3 +1,6 @@ +import { differenceInHours } from 'date-fns' +import throttle from 'lodash/throttle' + export type StmEntryDto = { paymentId: string dateCreated: number @@ -5,37 +8,63 @@ export type StmEntryDto = { export type BrowserStmDto = { [formId: string]: StmEntryDto } + +const EXPIRY_TIME_IN_HRS = 24 // 1 day +const EVICTION_DEBOUNCE_TIME = 1000 * 20 // 20 seconds + +const _processEviction = (entryObj: BrowserStmDto) => { + const curTime = Date.now() + const returnObj = { ...entryObj } + Object.keys(returnObj).forEach((formId) => { + const { dateCreated } = returnObj[formId] + const deltaHrs = differenceInHours(curTime, new Date(dateCreated)) + if (deltaHrs > EXPIRY_TIME_IN_HRS) { + delete returnObj[formId] + } + }) + return returnObj +} + +export const processEviction = throttle( + _processEviction, + EVICTION_DEBOUNCE_TIME, + { leading: true, trailing: false }, +) + export const deserialize = (jsonString: string): BrowserStmDto => { + let parsedObj try { - return JSON.parse(jsonString) + parsedObj = JSON.parse(jsonString) } catch { - return {} + parsedObj = {} } + return parsedObj } + export const serialize = (entryObj: BrowserStmDto) => { return JSON.stringify(entryObj) } -export const processEviction = (data: BrowserStmDto) => { - // asd -} export const addEntry = ( entryObj: BrowserStmDto, { formId, paymentId }: { formId: string; paymentId: string }, ): BrowserStmDto => { - entryObj[formId] = { - paymentId, - dateCreated: Number(new Date()), + return { + ...entryObj, + formId: { + paymentId, + dateCreated: Date.now(), + }, } - return entryObj } export const deleteEntry = ( entryObj: BrowserStmDto, { formId }: { formId: string }, ): BrowserStmDto => { - delete entryObj[formId] - return entryObj + const returnObj = { ...entryObj } + delete returnObj[formId] + return returnObj } export const getEntry = ( From 339debe68b85ee5884e44f03f1f85e13d0bff5b1 Mon Sep 17 00:00:00 2001 From: Ken Date: Mon, 3 Apr 2023 18:56:15 +0800 Subject: [PATCH 31/37] refactor: update manual construction of url to utils func --- .../components/FormPaymentPage/FormPaymentResumeModal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentResumeModal.tsx b/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentResumeModal.tsx index dfcc826f72..bb6a2285a8 100644 --- a/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentResumeModal.tsx +++ b/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentResumeModal.tsx @@ -15,6 +15,7 @@ import { FormResponseMode } from '~shared/types' import { useBrowserStm } from '~hooks/payments' import { useIsMobile } from '~hooks/useIsMobile' +import { getPaymentPageUrl } from '~utils/urls' import Button from '~components/Button' import { usePublicFormContext } from '../../PublicFormContext' @@ -46,7 +47,7 @@ export const PublicFormPaymentResumeModal = (): JSX.Element => { onClose() return } - navigate(`payment/${lastPaymentMemory.paymentId}`) + navigate(getPaymentPageUrl(formId, lastPaymentMemory.paymentId)) } const handleStartOver = () => { From 187bd794cef9a1eebb22a1190a44263c03686286 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 4 Apr 2023 10:58:32 +0800 Subject: [PATCH 32/37] fix: formid not set after refactor --- frontend/src/hooks/payments/useBrowserStm/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/hooks/payments/useBrowserStm/utils.ts b/frontend/src/hooks/payments/useBrowserStm/utils.ts index 53c1717392..112ecb4f7d 100644 --- a/frontend/src/hooks/payments/useBrowserStm/utils.ts +++ b/frontend/src/hooks/payments/useBrowserStm/utils.ts @@ -51,7 +51,7 @@ export const addEntry = ( ): BrowserStmDto => { return { ...entryObj, - formId: { + [formId]: { paymentId, dateCreated: Date.now(), }, From 34a45fe2a0125472374967e631eaa34fadccd4ed Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 4 Apr 2023 10:59:22 +0800 Subject: [PATCH 33/37] fix: restore button not ordered first in mobile mode --- .../components/FormPaymentPage/FormPaymentResumeModal.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentResumeModal.tsx b/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentResumeModal.tsx index bb6a2285a8..959897aba3 100644 --- a/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentResumeModal.tsx +++ b/frontend/src/features/public-form/components/FormPaymentPage/FormPaymentResumeModal.tsx @@ -70,7 +70,10 @@ export const PublicFormPaymentResumeModal = (): JSX.Element => { previous session and complete payment. - +