Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: payment browser stm #6030

Merged
merged 38 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
04210ab
feat: add new route for payment page
KenLSM Mar 23, 2023
33c8e1e
feat: add fetch payment model from paymentintent id
KenLSM Mar 23, 2023
72401d7
refactor(fe): move payment related modules to payment folder
KenLSM Mar 24, 2023
7b3a638
refactor: update payments/:formid/getInfo url to lowercase
KenLSM Mar 27, 2023
8dfeddc
refactor: refine GetPaymentInfoDto type
KenLSM Mar 27, 2023
f14c402
chore: minor url casing update
KenLSM Mar 27, 2023
79371fc
feat: receipt block to show after payment
KenLSM Mar 27, 2023
4ee2282
refactor: move payment elements to payment folder
KenLSM Mar 28, 2023
fdf600e
refactor: cleanup old payment block
KenLSM Mar 28, 2023
873b1cc
add refreshkey to refetch stripe PI after payment
KenLSM Mar 28, 2023
9117cd0
add missing default case in switch
KenLSM Mar 28, 2023
50470f3
fix: cleanup suspense flashes
KenLSM Mar 28, 2023
c3b0080
refactor: receipt loading, download block
KenLSM Mar 28, 2023
be5fb26
add workaround for transaction; readpreference on operation
KenLSM Mar 28, 2023
601747e
chore: remove debug code
KenLSM Mar 29, 2023
1b92aa0
fix: confirmpayment flow not triggered during receipt query
KenLSM Mar 29, 2023
6bc7219
fix: test case for readpreference additions
KenLSM Mar 29, 2023
8884749
refactor: remove unused PaymentSubmissionData
KenLSM Mar 29, 2023
cca3721
remove unused file
KenLSM Mar 29, 2023
ff6bab8
chore: remove unused imports
KenLSM Mar 30, 2023
c7cd457
refactor: move payment related features into public-form/payment
KenLSM Apr 3, 2023
9bba127
refactor: update payment view types to use enums and more consistent …
KenLSM Apr 3, 2023
a818cba
fix: incorrect file import on AppRouter after refactor
KenLSM Apr 3, 2023
a7ebb90
refactor: extract payment page url generation to separate utils file
KenLSM Apr 3, 2023
1b064da
feat: add payment resume modal
KenLSM Apr 3, 2023
a2a3223
feat: add payment local storage hook
KenLSM Apr 3, 2023
c3acee3
feat: add modal popup flow
KenLSM Apr 3, 2023
d48e073
fix: incorrect jest types definition version
KenLSM Apr 3, 2023
9bbcbd0
add test cases for browser stm utils
KenLSM Apr 3, 2023
c59572b
refactor: remove side effects, switch to throttle
KenLSM Apr 3, 2023
339debe
refactor: update manual construction of url to utils func
KenLSM Apr 3, 2023
187bd79
fix: formid not set after refactor
KenLSM Apr 4, 2023
34a45fe
fix: restore button not ordered first in mobile mode
KenLSM Apr 4, 2023
74a33b8
chore: update package.json
KenLSM Apr 4, 2023
9814f78
refactor: move utils/url into public-form folder
KenLSM Apr 4, 2023
c3a260d
fix: Payment Modals styling differences
KenLSM Apr 4, 2023
258b740
Merge remote-tracking branch 'origin/feat/payment-mvp' into feat/paym…
KenLSM Apr 4, 2023
4ea4c00
fix: processEviction not triggering
KenLSM Apr 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
722 changes: 118 additions & 604 deletions frontend/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
KenLSM marked this conversation as resolved.
Show resolved Hide resolved
"@types/jest": "^26.0.24",
"@types/node": "^16.11.12",
"@types/react": "^17.0.37",
"@types/react-beautiful-dnd": "^13.1.2",
Expand Down
3 changes: 0 additions & 3 deletions frontend/src/features/public-form/PublicFormContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@
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 = {
/** Submission id */
id: string | undefined
/** Submission time in ms from epoch */
timestamp: number
// payment forms will return a paymentIntentId
paymentData?: PaymentSubmissionData
}

export interface PublicFormContextProps
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/features/public-form/PublicFormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { HttpError } from '~services/ApiService'
Expand All @@ -35,6 +36,7 @@ import {
trackVisitPublicForm,
} from '~features/analytics/AnalyticsService'
import { useEnv } from '~features/env/queries'
import { getPaymentPageUrl } from '~features/public-form/utils/urls'
import {
RecaptchaClosedError,
useRecaptcha,
Expand Down Expand Up @@ -190,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(
Expand Down Expand Up @@ -275,13 +278,14 @@ export const PublicFormProvider = ({
onSuccess: ({
submissionId,
timestamp,
// payment forms will return a paymentIntentId
// payment forms will have non-empty paymentData field
paymentData,
}) => {
trackSubmitForm(form)

if (paymentData) {
navigate(`/${formId}/payment/${paymentData.paymentId}`)
navigate(getPaymentPageUrl(formId, paymentData.paymentId))
storePaymentMemory(paymentData.paymentId)
return
}
setSubmissionData({
Expand Down Expand Up @@ -328,6 +332,7 @@ export const PublicFormProvider = ({
submitStorageModeFormMutation,
formId,
navigate,
storePaymentMemory,
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -136,6 +137,7 @@ export const FormFields = ({
paymentDetails={form.payments_field}
/>
)}
<PublicFormPaymentResumeModal />
<PublicFormSubmitButton
onSubmit={onSubmit ? formMethods.handleSubmit(onSubmit) : undefined}
formFields={augmentedFormFields}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ export const FormPaymentModal = ({
<ModalContent>
<ModalCloseButton />
<ModalHeader>You are about to make payment</ModalHeader>
<ModalBody whiteSpace="pre-line">
{
'Please ensure that your form information is accurate. You will not be able to edit your form after you proceed.'
}
<ModalBody>
Please ensure that your form information is accurate. You will not
be able to edit your form after you proceed.
</ModalBody>
<ModalFooter>
<ButtonGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useNavigate } from 'react-router-dom'
import {
ButtonGroup,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
useDisclosure,
} from '@chakra-ui/react'

import { FormResponseMode } from '~shared/types'

import { useBrowserStm } from '~hooks/payments'
import { useIsMobile } from '~hooks/useIsMobile'
import Button from '~components/Button'

import { getPaymentPageUrl } from '~features/public-form/utils/urls'

import { usePublicFormContext } from '../../PublicFormContext'

/**
* This component is split up so that input changes will not rerender the
* entire FormFields component leading to terrible performance.
*/
export const PublicFormPaymentResumeModal = (): JSX.Element => {
const isMobile = useIsMobile()
const { form, formId } = usePublicFormContext()

const [lastPaymentMemory, , clearPaymentMemory] = useBrowserStm(formId)

const isPaymentEnabled =
form?.responseMode === FormResponseMode.Encrypt &&
form?.payments_field?.enabled

const { isOpen, onClose } = useDisclosure({
defaultIsOpen: Boolean(lastPaymentMemory && isPaymentEnabled),
})

const navigate = useNavigate()
if (!isOpen) {
return <></>
}
const onSubmit = () => {
if (!lastPaymentMemory) {
onClose()
return
}
navigate(getPaymentPageUrl(formId, lastPaymentMemory.paymentId))
}

const handleStartOver = () => {
clearPaymentMemory()
onClose()
}
return (
<Stack px={{ base: '1rem', md: 0 }} pt="2.5rem" pb="4rem">
<Modal
isOpen
onClose={() => {
// do nothing, prevent dismissal through backdrop touch
}}
>
<ModalOverlay />
foochifa marked this conversation as resolved.
Show resolved Hide resolved
<ModalContent>
<ModalHeader pb={'2rem'}>Restore previous session?</ModalHeader>
<ModalBody>
We noticed an incomplete session on this form. You can restore your
previous session and complete payment.
</ModalBody>
<ModalFooter pt={'2.5rem'} pb={'2.5rem'}>
<ButtonGroup
flexWrap={isMobile ? 'wrap-reverse' : 'wrap'}
justifyContent="end"
>
<Button
variant="clear"
onClick={handleStartOver}
isFullWidth={isMobile}
>
Start over again
</Button>
<Button onClick={onSubmit} isFullWidth={isMobile}>
Restore previous session
</Button>
</ButtonGroup>
</ModalFooter>
</ModalContent>
</Modal>
</Stack>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { loadStripe } from '@stripe/stripe-js'

import { FormColorTheme, FormResponseMode } from '~shared/types/form'

import { useBrowserStm } from '~hooks/payments'
import { centsToDollars } from '~utils/payments'
import Button from '~components/Button'

Expand Down Expand Up @@ -46,11 +47,13 @@ const StripeCheckoutForm = ({
isRetry,
triggerPaymentStatusRefetch,
}: StripeCheckoutFormProps) => {
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) {
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/features/public-form/utils/urls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const getPaymentPageUrl = (formId: string, paymentId: string) => {
return `/${formId}/payment/${paymentId}`
}
1 change: 1 addition & 0 deletions frontend/src/hooks/payments/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useBrowserStm } from './useBrowserStm'
44 changes: 44 additions & 0 deletions frontend/src/hooks/payments/useBrowserStm/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { addEntry, BrowserStmDto, getEntry, processEviction } from '../utils'

const EMPTY_OBJ = {}
describe('useBrowserStm', () => {
describe('processEviction', () => {
afterEach(() => {
jest.useRealTimers()
})
beforeEach(() => {
processEviction.cancel()
})
it('should only be called once if called in quick succession', () => {
// Arrange
const mockFn = jest.fn()

// Act, Assert
processEviction(EMPTY_OBJ, mockFn)
expect(mockFn).toBeCalledTimes(1)
processEviction(EMPTY_OBJ, mockFn)
expect(mockFn).toBeCalledTimes(1)
})

it('should evict entries that are old', (done) => {
// 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()
const mockFn = (retObject: BrowserStmDto) => {
// Assert
expect(getEntry(retObject, tempId)).toBeFalsy()
done()
}

// Act
processEviction(entryObj, mockFn)
})
})
})
57 changes: 57 additions & 0 deletions frontend/src/hooks/payments/useBrowserStm/index.ts
Original file line number Diff line number Diff line change
@@ -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, (obj) => setPaymentMemory(serialize(obj)))

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]
}
Loading