Skip to content

Commit

Permalink
Donation flow more fixes (#1991)
Browse files Browse the repository at this point in the history
* fix: Increase minWidth of payment method icons

* fix: Hide Stripe fee sentence if amount is not selected

* chore: Remove redundant field

* chore: Disable text decoration for MUI links

* chore: Change color of radio buttons if disabled

* chore: Change color of radio buttons if disabled

* fix: Move stripe idempotency key in serverside

* chore: Reuse our customized Link component

* chore: Show human readable error if not logged in for rec donation

* chore: Update translations

* refactor: Abstract result into a variable

* chore: Add fullstop to some messages
  • Loading branch information
sashko9807 authored Dec 11, 2024
1 parent 11ea027 commit c26d6d2
Show file tree
Hide file tree
Showing 15 changed files with 72 additions and 72 deletions.
19 changes: 10 additions & 9 deletions public/locales/bg/donation-flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@
}
},
"alert": {
"card-fee": "Таксата на Stripe се изчислява според района на картодържателя: 1.2% + 0.5лв. за Европейската икономическа зона",
"bank-fee": "Таксата за транзакция при банков превод зависи от индивидуалните условия на Вашата банка (от 0 до 4 лв.)",
"calculated-fees": "За вашия превод от <strong>{{totalAmount}}</strong>, таксата на Stripe ще е <strong>{{fees}}</strong>, а кампанията ще получи <strong>{{amount}}</strong>"
"card-fee": "Таксата на Stripe се изчислява според района на картодържателя: 1.2% + 0.5лв. за Европейската икономическа зона.",
"bank-fee": "Таксата за транзакция при банков превод зависи от индивидуалните условия на Вашата банка (от 0 до 4 лв.).",
"calculated-fees": "За вашия превод от <strong>{{totalAmount}}</strong>, таксата на Stripe ще е <strong>{{fees}}</strong>, а кампанията ще получи <strong>{{amount}}</strong>."
}
},
"authentication": {
Expand All @@ -107,16 +107,16 @@
"field": {
"password": "Парола",
"email": {
"error": "Трябва да въведете валиден имейл"
"error": "Трябва да въведете валиден имейл."
}
},
"alert": {
"authenticate": {
"title": "Избирайки да се впишете, ще можете да",
"create-account": "създадете акаунт като физическо или юридическо лице",
"certificate": "получите сертификат за дарение",
"monthly-donation": "правите месечни дарения по избрана кампания",
"notification": "можете да получавате и известия за статуса на подкрепени вече кампании"
"create-account": "създадете акаунт като физическо или юридическо лице.",
"certificate": "получите сертификат за дарение.",
"monthly-donation": "правите месечни дарения по избрана кампания.",
"notification": "можете да получавате и известия за статуса на подкрепени вече кампании."
}
}
},
Expand All @@ -129,13 +129,14 @@
"field": {
"anonymous": {
"label": "Искам да съм анонимен",
"description": "Ако останете анонимен името ви няма да бъде показано на кампанията"
"description": "Ако останете анонимен името ви няма да бъде показано на кампанията."
},
"privacy": {
"error": "Трябва да приемете политиката за поверителност"
}
},
"alerts": {
"subscription-unauthorized": "Ако желаете да правите ежемесечни дарения, моля влезте с вашия профил в платформата или се регистрирайте, ако все още не сте го направили",
"error": "Нещо се обърка, моля опитайте пак или презаредете страницата"
},
"total": "Общо"
Expand Down
14 changes: 9 additions & 5 deletions public/locales/en/donation-flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,10 @@
"alert": {
"authenticate": {
"title": "Choosing to login you will be able to",
"create-account": "Create an account",
"certificate": "Get a donation certificate",
"monthly-donation": "Make a monthly donation",
"notification": "Get notifications about campaigns you donated to"
"create-account": "Create an account.",
"certificate": "Get a donation certificate.",
"monthly-donation": "Make a monthly donation.",
"notification": "Get notifications about campaigns you donated to."
}
}
},
Expand All @@ -130,11 +130,15 @@
"title": "Transaction",
"description": "The transaction is only to compensate the transfer and is calculated based on your method of payment. \"Podkrepi.bg\" works with 0% commission."
},
"alerts": {
"subscription-unauthorized": "If you wish to make monthly donations, please sign in with your account or register on the platform if you still have not.",
"error": "Something went wrong, please try again or refresh the page."
},
"total": "Total",
"field": {
"anonymous": {
"label": "I want to be anonymous",
"description": "If you choose to be anonymous, your name will not be visible in the campaign"
"description": "If you choose to be anonymous, your name will not be visible in the campaign."
},
"privacy": {
"error": "You have to accept the privacy policy"
Expand Down
15 changes: 10 additions & 5 deletions src/components/client/donation-flow/DonationFlowForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,14 @@ export const validationSchema: yup.SchemaOf<DonationFormData> = yup
export function DonationFlowForm() {
const formikRef = useRef<FormikProps<DonationFormData> | null>(null)
const { t } = useTranslation('donation-flow')
const { campaign, setupIntent, paymentError, setPaymentError, idempotencyKey } = useDonationFlow()
const { campaign, setupIntent, paymentError, setPaymentError } = useDonationFlow()
const stripe = useStripe()
const elements = useElements()
const router = useRouter()
const updateSetupIntentMutation = useUpdateSetupIntent()
const cancelSetupIntentMutation = useCancelSetupIntent()
const paymentMethodSectionRef = React.useRef<HTMLDivElement>(null)
const authenticationSectionRef = React.useRef<HTMLDivElement>(null)
const stripeChargeRef = React.useRef<string>(idempotencyKey)
const [showCancelDialog, setShowCancelDialog] = React.useState(false)
const [submitPaymentLoading, setSubmitPaymentLoading] = React.useState(false)
const { data: { user: person } = { user: null } } = useCurrentPerson()
Expand Down Expand Up @@ -172,11 +171,19 @@ export function DonationFlowForm() {
return
}

if (values.mode === 'subscription' && !session?.user?.sub) {
setSubmitPaymentLoading(false)
formikRef.current?.setFieldError(
'authentication',
t('step.summary.alerts.subscription-unauthorized'),
)
return
}

// Update the setup intent with the latest calculated amount
try {
const updatedIntent = await updateSetupIntentMutation.mutateAsync({
id: setupIntent.id,
idempotencyKey,
payload: {
metadata: {
type: person?.company ? DonationType.corporate : DonationType.donation,
Expand All @@ -199,7 +206,6 @@ export function DonationFlowForm() {
campaign,
values,
session,
stripeChargeRef.current,
)
router.push(
`${window.location.origin}${routes.campaigns.donationStatus(campaign.slug)}?p_status=${
Expand All @@ -212,7 +218,6 @@ export function DonationFlowForm() {
type: 'invalid_request_error',
message: (error as StripeError).message ?? t('step.summary.alerts.error'),
})
stripeChargeRef.current = crypto.randomUUID()
return
}

Expand Down
7 changes: 1 addition & 6 deletions src/components/client/donation-flow/DonationFlowPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,16 @@ import { CampaignResponse } from 'gql/campaigns'
export default function DonationFlowPage({
slug,
setupIntent,
idempotencyKey,
}: {
slug: string
setupIntent: Stripe.SetupIntent
idempotencyKey: string
}) {
const { data } = useViewCampaign(slug)
//This query needs to be prefetched in the pages folder
//otherwise on the first render the data will be undefined
const campaign = data?.campaign as CampaignResponse
return (
<DonationFlowProvider
campaign={campaign}
setupIntent={setupIntent}
idempotencyKey={idempotencyKey}>
<DonationFlowProvider campaign={campaign} setupIntent={setupIntent}>
<StripeElementsProvider>
<DonationFlowLayout campaign={campaign}>
<DonationFlowForm />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,21 @@ type DonationContext = {
setPaymentError: React.Dispatch<React.SetStateAction<StripeError | null>>
campaign: CampaignResponse
stripe: Promise<StripeType | null>
idempotencyKey: string
}

const DonationFlowContext = React.createContext({} as DonationContext)

export const DonationFlowProvider = ({
campaign,
setupIntent,
idempotencyKey,
children,
}: PropsWithChildren<{
campaign: CampaignResponse
setupIntent: Stripe.SetupIntent
idempotencyKey: string
}>) => {
const [paymentError, setPaymentError] = React.useState<StripeError | null>(null)

const value = {
idempotencyKey,
setupIntent,
paymentError,
setPaymentError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export async function confirmStripePayment(
campaign: CampaignResponse,
values: DonationFormData,
session: Session | null,
idempotencyKey: string,
): Promise<StripeJS.PaymentIntent> {
if (setupIntent.status !== DonationFormPaymentStatus.SUCCEEDED) {
const { error: intentError } = await stripe.confirmSetup({
Expand All @@ -30,12 +29,7 @@ export async function confirmStripePayment(
throw intentError
}
}
const payment = await createIntentFromSetup(
setupIntent.id,
idempotencyKey,
values.mode as PaymentMode,
session,
)
const payment = await createIntentFromSetup(setupIntent.id, values.mode as PaymentMode, session)

if (payment.data.status === DonationFormPaymentStatus.REQUIRES_ACTION) {
const { error: confirmPaymentError } = await stripe.confirmCardPayment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ export default function PaymentMethod({
{
value: 'card',
label: t('step.payment-method.field.method.card'),
icon: <CardIcon sx={{ width: 80, height: 80, minWidth: 421 }} />,
icon: <CardIcon sx={{ width: 80, height: 80, minWidth: 480 }} />,
disabled: false,
},
{
value: 'bank',
label: t('step.payment-method.field.method.bank'),
icon: <BankIcon sx={{ width: 80, height: 80, minWidth: 421 }} />,
icon: <BankIcon sx={{ width: 80, height: 80, minWidth: 480 }} />,
disabled: mode.value === 'subscription',
},
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const TaxesCheckbox = () => {
const { t } = useTranslation('donation-flow')
const [amountWithFees] = useField('finalAmount')
const [amountWithoutFees] = useField<number>('amountWithoutFees')
const showCalculatedFees = Boolean(amountWithFees.value)
return (
<>
<Grid2 container>
Expand Down Expand Up @@ -50,15 +51,17 @@ export const TaxesCheckbox = () => {
/>
</Grid2>
</Grid2>
<Trans
t={t}
i18nKey="step.payment-method.alert.calculated-fees"
values={{
amount: moneyPublicDecimals2(amountWithoutFees.value),
fees: moneyPublicDecimals2(amountWithFees.value - amountWithoutFees.value),
totalAmount: moneyPublicDecimals2(amountWithFees.value),
}}
/>
{showCalculatedFees && (
<Trans
t={t}
i18nKey="step.payment-method.alert.calculated-fees"
values={{
amount: moneyPublicDecimals2(amountWithoutFees.value),
fees: moneyPublicDecimals2(amountWithFees.value - amountWithoutFees.value),
totalAmount: moneyPublicDecimals2(amountWithFees.value),
}}
/>
)}
</>
)
}
5 changes: 3 additions & 2 deletions src/components/common/ExternalLink.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Link, LinkProps } from '@mui/material'
import { LinkProps } from '@mui/material'
import { PropsWithChildren } from 'react'
import Link from './Link'

type ExternalLinkParams = PropsWithChildren<LinkProps>

export default function ExternalLink({ children, ...props }: ExternalLinkParams) {
return (
<Link target="_blank" rel="noreferrer noopener" {...props}>
<Link target="_blank" rel="noreferrer noopener" {...props} sx={{ textDecoration: 'none' }}>
{children}
</Link>
)
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { LinkProps, Link as MuiLink } from '@mui/material'
import NextLink from 'next/link'

export default function Link(props: LinkProps<'a'>) {
return <MuiLink component={NextLink} {...props} />
return <MuiLink component={NextLink} {...props} sx={{ textDecoration: 'none' }} />
}
15 changes: 12 additions & 3 deletions src/components/common/form/RadioButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,29 @@ type RadioButtonProps = {
error?: boolean
}

function RadioButton({ checked, label, muiRadioButtonProps, value, error }: RadioButtonProps) {
function RadioButton({
checked,
label,
muiRadioButtonProps,
value,
disabled,
error,
}: RadioButtonProps) {
return (
<StyledRadioButton error={error}>
<FormControlLabel
value={value}
className={`${classes.radioWrapper} ${checked ? classes.checked : null}`}
sx={checked ? {} : undefined}
disabled={disabled}
sx={disabled ? { backgroundColor: '#e0e0e0', opacity: 0.7 } : {}}
className={`${classes.radioWrapper} ${checked && !disabled ? classes.checked : null}`}
label={
<Typography className={classes.label} sx={{ wordBreak: 'break-word' }}>
{label}
</Typography>
}
control={
<Radio
disabled={disabled}
icon={<div className={`${classes.circle}`} />}
checkedIcon={
<Check color="primary" className={checked ? classes.checkIcon : undefined} />
Expand Down
1 change: 0 additions & 1 deletion src/gql/donations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export type CancelSetupIntentInput = {

export type UpdateSetupIntentInput = {
id: string
idempotencyKey: string
payload: Stripe.SetupIntentUpdateParams
}

Expand Down
4 changes: 1 addition & 3 deletions src/pages/campaigns/donation/[slug]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,17 @@ export const getServerSideProps: GetServerSideProps = async (ctx: GetServerSideP
)

//Generate idempotencyKey to prevent duplicate creation of resources in stripe
const idempotencyKey = crypto.randomUUID()

//create and prefetch the payment intent
const { data: setupIntent } = await apiClient.post<
Stripe.SetupIntentCreateParams,
AxiosResponse<Stripe.SetupIntentCreateParams>
>(endpoints.donation.createSetupIntent.url, idempotencyKey)
>(endpoints.donation.createSetupIntent.url)

return {
props: {
slug,
setupIntent,
idempotencyKey,
dehydratedState: dehydrate(client),
...(await serverSideTranslations(ctx.locale ?? 'bg', [
'common',
Expand Down
12 changes: 6 additions & 6 deletions src/service/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,19 +154,19 @@ export const endpoints = {
createSubscriptionPayment: <Endpoint>{ url: '/stripe/create-subscription', method: 'POST' },
createPaymentIntent: <Endpoint>{ url: '/stripe/payment-intent', method: 'POST' },
createSetupIntent: <Endpoint>{ url: '/stripe/setup-intent', method: 'POST' },
createPaymentIntentFromSetup: (id: string, idempotencyKey: string) =>
createPaymentIntentFromSetup: (id: string) =>
<Endpoint>{
url: `/stripe/setup-intent/${id}/payment-intent?idempotency-key=${idempotencyKey}`,
url: `/stripe/setup-intent/${id}/payment-intent`,
method: 'POST',
},
createSubscriptionFromSetup: (id: string, idempotencyKey: string) =>
createSubscriptionFromSetup: (id: string) =>
<Endpoint>{
url: `/stripe/setup-intent/${id}/subscription?idempotency-key=${idempotencyKey}`,
url: `/stripe/setup-intent/${id}/subscription`,
method: 'POST',
},
updateSetupIntent: (id: string, idempotencyKey: string) =>
updateSetupIntent: (id: string) =>
<Endpoint>{
url: `/stripe/setup-intent/${id}?idempotency-key=${idempotencyKey}`,
url: `/stripe/setup-intent/${id}`,
method: 'POST',
},
cancelSetupIntent: (id: string) =>
Expand Down
Loading

0 comments on commit c26d6d2

Please sign in to comment.