From c26d6d2dec451dcd6a8f2b624f32c653da107fac Mon Sep 17 00:00:00 2001 From: Aleksandar Petkov Date: Wed, 11 Dec 2024 18:36:49 +0200 Subject: [PATCH] Donation flow more fixes (#1991) * 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 --- public/locales/bg/donation-flow.json | 19 +++++++++-------- public/locales/en/donation-flow.json | 14 ++++++++----- .../client/donation-flow/DonationFlowForm.tsx | 15 ++++++++----- .../client/donation-flow/DonationFlowPage.tsx | 7 +------ .../contexts/DonationFlowProvider.tsx | 4 ---- .../helpers/confirmStripeDonation.ts | 8 +------ .../steps/payment-method/PaymentMethod.tsx | 4 ++-- .../steps/payment-method/TaxesCheckbox.tsx | 21 +++++++++++-------- src/components/common/ExternalLink.tsx | 5 +++-- src/components/common/Link.tsx | 2 +- src/components/common/form/RadioButton.tsx | 15 ++++++++++--- src/gql/donations.d.ts | 1 - src/pages/campaigns/donation/[slug]/index.tsx | 4 +--- src/service/apiEndpoints.ts | 12 +++++------ src/service/donation.ts | 13 ++++-------- 15 files changed, 72 insertions(+), 72 deletions(-) diff --git a/public/locales/bg/donation-flow.json b/public/locales/bg/donation-flow.json index e039106a0..79bff765d 100644 --- a/public/locales/bg/donation-flow.json +++ b/public/locales/bg/donation-flow.json @@ -85,9 +85,9 @@ } }, "alert": { - "card-fee": "Таксата на Stripe се изчислява според района на картодържателя: 1.2% + 0.5лв. за Европейската икономическа зона", - "bank-fee": "Таксата за транзакция при банков превод зависи от индивидуалните условия на Вашата банка (от 0 до 4 лв.)", - "calculated-fees": "За вашия превод от {{totalAmount}}, таксата на Stripe ще е {{fees}}, а кампанията ще получи {{amount}}" + "card-fee": "Таксата на Stripe се изчислява според района на картодържателя: 1.2% + 0.5лв. за Европейската икономическа зона.", + "bank-fee": "Таксата за транзакция при банков превод зависи от индивидуалните условия на Вашата банка (от 0 до 4 лв.).", + "calculated-fees": "За вашия превод от {{totalAmount}}, таксата на Stripe ще е {{fees}}, а кампанията ще получи {{amount}}." } }, "authentication": { @@ -107,16 +107,16 @@ "field": { "password": "Парола", "email": { - "error": "Трябва да въведете валиден имейл" + "error": "Трябва да въведете валиден имейл." } }, "alert": { "authenticate": { "title": "Избирайки да се впишете, ще можете да", - "create-account": "създадете акаунт като физическо или юридическо лице", - "certificate": "получите сертификат за дарение", - "monthly-donation": "правите месечни дарения по избрана кампания", - "notification": "можете да получавате и известия за статуса на подкрепени вече кампании" + "create-account": "създадете акаунт като физическо или юридическо лице.", + "certificate": "получите сертификат за дарение.", + "monthly-donation": "правите месечни дарения по избрана кампания.", + "notification": "можете да получавате и известия за статуса на подкрепени вече кампании." } } }, @@ -129,13 +129,14 @@ "field": { "anonymous": { "label": "Искам да съм анонимен", - "description": "Ако останете анонимен името ви няма да бъде показано на кампанията" + "description": "Ако останете анонимен името ви няма да бъде показано на кампанията." }, "privacy": { "error": "Трябва да приемете политиката за поверителност" } }, "alerts": { + "subscription-unauthorized": "Ако желаете да правите ежемесечни дарения, моля влезте с вашия профил в платформата или се регистрирайте, ако все още не сте го направили", "error": "Нещо се обърка, моля опитайте пак или презаредете страницата" }, "total": "Общо" diff --git a/public/locales/en/donation-flow.json b/public/locales/en/donation-flow.json index 2f6c0ca80..7cda46ca4 100644 --- a/public/locales/en/donation-flow.json +++ b/public/locales/en/donation-flow.json @@ -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." } } }, @@ -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" diff --git a/src/components/client/donation-flow/DonationFlowForm.tsx b/src/components/client/donation-flow/DonationFlowForm.tsx index 7ee93d06a..1b0a5b3f3 100644 --- a/src/components/client/donation-flow/DonationFlowForm.tsx +++ b/src/components/client/donation-flow/DonationFlowForm.tsx @@ -101,7 +101,7 @@ export const validationSchema: yup.SchemaOf = yup export function DonationFlowForm() { const formikRef = useRef | 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() @@ -109,7 +109,6 @@ export function DonationFlowForm() { const cancelSetupIntentMutation = useCancelSetupIntent() const paymentMethodSectionRef = React.useRef(null) const authenticationSectionRef = React.useRef(null) - const stripeChargeRef = React.useRef(idempotencyKey) const [showCancelDialog, setShowCancelDialog] = React.useState(false) const [submitPaymentLoading, setSubmitPaymentLoading] = React.useState(false) const { data: { user: person } = { user: null } } = useCurrentPerson() @@ -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, @@ -199,7 +206,6 @@ export function DonationFlowForm() { campaign, values, session, - stripeChargeRef.current, ) router.push( `${window.location.origin}${routes.campaigns.donationStatus(campaign.slug)}?p_status=${ @@ -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 } diff --git a/src/components/client/donation-flow/DonationFlowPage.tsx b/src/components/client/donation-flow/DonationFlowPage.tsx index 4145bba02..611638475 100644 --- a/src/components/client/donation-flow/DonationFlowPage.tsx +++ b/src/components/client/donation-flow/DonationFlowPage.tsx @@ -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 ( - + diff --git a/src/components/client/donation-flow/contexts/DonationFlowProvider.tsx b/src/components/client/donation-flow/contexts/DonationFlowProvider.tsx index c1a8167be..1709031cc 100644 --- a/src/components/client/donation-flow/contexts/DonationFlowProvider.tsx +++ b/src/components/client/donation-flow/contexts/DonationFlowProvider.tsx @@ -11,7 +11,6 @@ type DonationContext = { setPaymentError: React.Dispatch> campaign: CampaignResponse stripe: Promise - idempotencyKey: string } const DonationFlowContext = React.createContext({} as DonationContext) @@ -19,17 +18,14 @@ 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(null) const value = { - idempotencyKey, setupIntent, paymentError, setPaymentError, diff --git a/src/components/client/donation-flow/helpers/confirmStripeDonation.ts b/src/components/client/donation-flow/helpers/confirmStripeDonation.ts index f47accd95..ab1ba8547 100644 --- a/src/components/client/donation-flow/helpers/confirmStripeDonation.ts +++ b/src/components/client/donation-flow/helpers/confirmStripeDonation.ts @@ -13,7 +13,6 @@ export async function confirmStripePayment( campaign: CampaignResponse, values: DonationFormData, session: Session | null, - idempotencyKey: string, ): Promise { if (setupIntent.status !== DonationFormPaymentStatus.SUCCEEDED) { const { error: intentError } = await stripe.confirmSetup({ @@ -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( diff --git a/src/components/client/donation-flow/steps/payment-method/PaymentMethod.tsx b/src/components/client/donation-flow/steps/payment-method/PaymentMethod.tsx index a021bf558..383941a72 100644 --- a/src/components/client/donation-flow/steps/payment-method/PaymentMethod.tsx +++ b/src/components/client/donation-flow/steps/payment-method/PaymentMethod.tsx @@ -35,13 +35,13 @@ export default function PaymentMethod({ { value: 'card', label: t('step.payment-method.field.method.card'), - icon: , + icon: , disabled: false, }, { value: 'bank', label: t('step.payment-method.field.method.bank'), - icon: , + icon: , disabled: mode.value === 'subscription', }, ] diff --git a/src/components/client/donation-flow/steps/payment-method/TaxesCheckbox.tsx b/src/components/client/donation-flow/steps/payment-method/TaxesCheckbox.tsx index 2d0af6358..26f7964bd 100644 --- a/src/components/client/donation-flow/steps/payment-method/TaxesCheckbox.tsx +++ b/src/components/client/donation-flow/steps/payment-method/TaxesCheckbox.tsx @@ -12,6 +12,7 @@ export const TaxesCheckbox = () => { const { t } = useTranslation('donation-flow') const [amountWithFees] = useField('finalAmount') const [amountWithoutFees] = useField('amountWithoutFees') + const showCalculatedFees = Boolean(amountWithFees.value) return ( <> @@ -50,15 +51,17 @@ export const TaxesCheckbox = () => { /> - + {showCalculatedFees && ( + + )} ) } diff --git a/src/components/common/ExternalLink.tsx b/src/components/common/ExternalLink.tsx index e109b74c9..6adace6ea 100644 --- a/src/components/common/ExternalLink.tsx +++ b/src/components/common/ExternalLink.tsx @@ -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 export default function ExternalLink({ children, ...props }: ExternalLinkParams) { return ( - + {children} ) diff --git a/src/components/common/Link.tsx b/src/components/common/Link.tsx index bba88fbfd..2ebc13be6 100644 --- a/src/components/common/Link.tsx +++ b/src/components/common/Link.tsx @@ -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 + return } diff --git a/src/components/common/form/RadioButton.tsx b/src/components/common/form/RadioButton.tsx index f630754dc..c843c4a99 100644 --- a/src/components/common/form/RadioButton.tsx +++ b/src/components/common/form/RadioButton.tsx @@ -70,13 +70,21 @@ type RadioButtonProps = { error?: boolean } -function RadioButton({ checked, label, muiRadioButtonProps, value, error }: RadioButtonProps) { +function RadioButton({ + checked, + label, + muiRadioButtonProps, + value, + disabled, + error, +}: RadioButtonProps) { return ( {label} @@ -84,6 +92,7 @@ function RadioButton({ checked, label, muiRadioButtonProps, value, error }: Radi } control={ } checkedIcon={ diff --git a/src/gql/donations.d.ts b/src/gql/donations.d.ts index 4f5b9236d..de7b43730 100644 --- a/src/gql/donations.d.ts +++ b/src/gql/donations.d.ts @@ -42,7 +42,6 @@ export type CancelSetupIntentInput = { export type UpdateSetupIntentInput = { id: string - idempotencyKey: string payload: Stripe.SetupIntentUpdateParams } diff --git a/src/pages/campaigns/donation/[slug]/index.tsx b/src/pages/campaigns/donation/[slug]/index.tsx index e76de7f95..a5eefa024 100644 --- a/src/pages/campaigns/donation/[slug]/index.tsx +++ b/src/pages/campaigns/donation/[slug]/index.tsx @@ -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 - >(endpoints.donation.createSetupIntent.url, idempotencyKey) + >(endpoints.donation.createSetupIntent.url) return { props: { slug, setupIntent, - idempotencyKey, dehydratedState: dehydrate(client), ...(await serverSideTranslations(ctx.locale ?? 'bg', [ 'common', diff --git a/src/service/apiEndpoints.ts b/src/service/apiEndpoints.ts index 163479a02..e054c029f 100644 --- a/src/service/apiEndpoints.ts +++ b/src/service/apiEndpoints.ts @@ -154,19 +154,19 @@ export const endpoints = { createSubscriptionPayment: { url: '/stripe/create-subscription', method: 'POST' }, createPaymentIntent: { url: '/stripe/payment-intent', method: 'POST' }, createSetupIntent: { url: '/stripe/setup-intent', method: 'POST' }, - createPaymentIntentFromSetup: (id: string, idempotencyKey: string) => + createPaymentIntentFromSetup: (id: string) => { - 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) => { - 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) => { - url: `/stripe/setup-intent/${id}?idempotency-key=${idempotencyKey}`, + url: `/stripe/setup-intent/${id}`, method: 'POST', }, cancelSetupIntent: (id: string) => diff --git a/src/service/donation.ts b/src/service/donation.ts index dcb2c2431..41c6f9ac0 100644 --- a/src/service/donation.ts +++ b/src/service/donation.ts @@ -48,15 +48,11 @@ export function useUpdateSetupIntent() { //Create payment intent useing the react-query mutation const { data: session } = useSession() return useMutation({ - mutationFn: async ({ id, idempotencyKey, payload }: UpdateSetupIntentInput) => { + mutationFn: async ({ id, payload }: UpdateSetupIntentInput) => { return await apiClient.post< Stripe.SetupIntentUpdateParams, AxiosResponse - >( - endpoints.donation.updateSetupIntent(id, idempotencyKey).url, - payload, - authConfig(session?.accessToken), - ) + >(endpoints.donation.updateSetupIntent(id).url, payload, authConfig(session?.accessToken)) }, }) } @@ -84,14 +80,13 @@ export function useCreateSubscriptionPayment() { export async function createIntentFromSetup( setupIntentId: string, - idempotencyKey: string, mode: PaymentMode, session: Session | null, ): Promise> { return await apiClient.post, AxiosError>( mode === 'one-time' - ? endpoints.donation.createPaymentIntentFromSetup(setupIntentId, idempotencyKey).url - : endpoints.donation.createSubscriptionFromSetup(setupIntentId, idempotencyKey).url, + ? endpoints.donation.createPaymentIntentFromSetup(setupIntentId).url + : endpoints.donation.createSubscriptionFromSetup(setupIntentId).url, undefined, authConfig(session?.accessToken), )