Skip to content

Commit 437f372

Browse files
OWA-107: Fix broken SCA flow payment finalization (#677)
1 parent bba7337 commit 437f372

File tree

14 files changed

+173
-75
lines changed

14 files changed

+173
-75
lines changed

packages/common/src/controllers/CheckoutController.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ export default class CheckoutController {
224224
return response.responseData;
225225
};
226226

227+
finalizeStripePpvPayment = (paymentIntent: string) => this.checkoutService?.finalizeStripePpvPayment?.({ paymentIntent });
228+
227229
paypalPayment = async ({
228230
successUrl,
229231
waitingUrl,

packages/common/src/services/integrations/CheckoutService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
GetDirectPostCardPayment,
88
GetEntitlements,
99
GetFinalizeAdyenPayment,
10+
GetFinalizeStripePpvPayment,
1011
GetInitialAdyenPayment,
1112
GetOffer,
1213
GetOffers,
@@ -54,6 +55,8 @@ export default abstract class CheckoutService {
5455

5556
abstract finalizeAdyenPayment?: GetFinalizeAdyenPayment;
5657

58+
abstract finalizeStripePpvPayment?: GetFinalizeStripePpvPayment;
59+
5760
abstract updatePaymentMethodWithPayPal?: UpdatePaymentWithPayPal;
5861

5962
abstract deletePaymentMethod?: DeletePaymentMethod;

packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ export default class CleengCheckoutService extends CheckoutService {
162162
finalizeAdyenPayment: GetFinalizeAdyenPayment = async (payload) =>
163163
this.cleengService.post('/connectors/adyen/initial-payment/finalize', JSON.stringify(payload), { authenticate: true });
164164

165+
finalizeStripePpvPayment: undefined;
166+
165167
updatePaymentMethodWithPayPal: UpdatePaymentWithPayPal = async (payload) => {
166168
return this.cleengService.post('/connectors/paypal/v1/payment_details/tokens', JSON.stringify(payload), { authenticate: true });
167169
};

packages/common/src/services/integrations/jwp/JWPCheckoutService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,14 @@ export default class JWPCheckoutService extends CheckoutService {
306306
}
307307
};
308308

309+
finalizeStripePpvPayment = async ({ paymentIntent }: { paymentIntent: string }) => {
310+
try {
311+
await this.apiService.post<CommonResponse>('/payments', { pi_id: paymentIntent }, { withAuthentication: true });
312+
} catch {
313+
throw new Error('Failed to confirm payment');
314+
}
315+
};
316+
309317
getSubscriptionSwitches = undefined;
310318

311319
getOrder = undefined;

packages/common/types/checkout.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,12 @@ export type Order = {
9494
requiredPaymentDetails: boolean;
9595
};
9696

97+
export type PaymentProvider = 'stripe' | 'adyen';
98+
9799
export type PaymentMethod = {
98100
id: number;
99101
methodName: 'card' | 'paypal';
100-
provider?: 'stripe' | 'adyen';
102+
provider?: PaymentProvider;
101103
paymentGateway?: 'adyen' | 'paypal'; // @todo: merge with provider
102104
logoUrl: string;
103105
};
@@ -383,6 +385,7 @@ export type GetEntitlements = EnvironmentServiceRequest<GetEntitlementsPayload,
383385
export type GetAdyenPaymentSession = EnvironmentServiceRequest<AdyenPaymentMethodPayload, AdyenPaymentSession>;
384386
export type GetInitialAdyenPayment = EnvironmentServiceRequest<InitialAdyenPaymentPayload, InitialAdyenPayment>;
385387
export type GetFinalizeAdyenPayment = EnvironmentServiceRequest<FinalizeAdyenPaymentPayload, FinalizeAdyenPayment>;
388+
export type GetFinalizeStripePpvPayment = PromiseRequest<{ paymentIntent: string }, void>;
386389
export type UpdatePaymentWithPayPal = EnvironmentServiceRequest<UpdatePaymentWithPayPalPayload, PaymentWithPayPalResponse>;
387390
export type DeletePaymentMethod = EnvironmentServiceRequest<DeletePaymentMethodPayload, DeletePaymentMethodResponse>;
388391
export type AddAdyenPaymentDetails = EnvironmentServiceRequest<AddAdyenPaymentDetailsPayload, AddAdyenPaymentDetailsResponse>;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, { useEffect, useMemo, useState } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { useLocation, useNavigate } from 'react-router';
4+
import { useSearchParams } from 'react-router-dom';
5+
import { getModule } from '@jwp/ott-common/src/modules/container';
6+
import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore';
7+
import AccountController from '@jwp/ott-common/src/controllers/AccountController';
8+
import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController';
9+
import { ACCESS_MODEL } from '@jwp/ott-common/src/constants';
10+
import useEventCallback from '@jwp/ott-hooks-react/src/useEventCallback';
11+
12+
import Button from '../Button/Button';
13+
import { modalURLFromLocation } from '../../utils/location';
14+
import { useAriaAnnouncer } from '../../containers/AnnouncementProvider/AnnoucementProvider';
15+
16+
import styles from './FinalizePayment.module.scss';
17+
18+
type FinalizeAdyenPaymentProps = {
19+
onError: () => void;
20+
};
21+
22+
const FinalizeAdyenPayment = ({ onError }: FinalizeAdyenPaymentProps) => {
23+
const accountController = getModule(AccountController);
24+
const checkoutController = getModule(CheckoutController);
25+
26+
const { t } = useTranslation('account');
27+
const announce = useAriaAnnouncer();
28+
const navigate = useNavigate();
29+
const location = useLocation();
30+
31+
const { accessModel } = useConfigStore(({ accessModel }) => ({ accessModel }));
32+
const [searchParams] = useSearchParams();
33+
const redirectResult = searchParams.get('redirectResult');
34+
const orderIdQueryParam = searchParams.get('orderId');
35+
36+
const [errorMessage, setErrorMessage] = useState<string>();
37+
38+
const paymentSuccessUrl = useMemo(() => {
39+
return modalURLFromLocation(location, accessModel === ACCESS_MODEL.SVOD ? 'welcome' : null);
40+
}, [accessModel, location]);
41+
42+
const checkPaymentResult = useEventCallback(async (redirectResult: string) => {
43+
const orderId = orderIdQueryParam ? parseInt(orderIdQueryParam, 10) : undefined;
44+
45+
try {
46+
await checkoutController.finalizeAdyenPayment({ redirectResult: decodeURI(redirectResult) }, orderId);
47+
await accountController.reloadSubscriptions({ retry: 10 });
48+
49+
announce(t('checkout.payment_success'), 'success');
50+
navigate(paymentSuccessUrl);
51+
} catch (error: unknown) {
52+
if (error instanceof Error) {
53+
setErrorMessage(error.message);
54+
onError();
55+
}
56+
}
57+
});
58+
59+
useEffect(() => {
60+
if (!redirectResult) return;
61+
62+
checkPaymentResult(redirectResult);
63+
}, [checkPaymentResult, redirectResult]);
64+
65+
return (
66+
<>
67+
{errorMessage && (
68+
<>
69+
<h2 className={styles.title}>{errorMessage}</h2>
70+
<Button
71+
label={t('checkout.go_back_to_checkout')}
72+
variant="contained"
73+
color="primary"
74+
size="large"
75+
onClick={() => navigate(modalURLFromLocation(location, 'checkout'))}
76+
fullWidth
77+
/>
78+
</>
79+
)}
80+
</>
81+
);
82+
};
83+
84+
export default FinalizeAdyenPayment;

packages/ui-react/src/components/FinalizePayment/FinalizePayment.module.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,3 @@
1010
font-weight: var(--body-font-weight-bold);
1111
font-size: 24px;
1212
}
13-

packages/ui-react/src/components/FinalizePayment/FinalizePayment.tsx

Lines changed: 18 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,30 @@
1-
import React, { useEffect, useMemo, useState } from 'react';
2-
import { useTranslation } from 'react-i18next';
3-
import { useLocation, useNavigate } from 'react-router';
4-
import { useSearchParams } from 'react-router-dom';
5-
import { getModule } from '@jwp/ott-common/src/modules/container';
6-
import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore';
7-
import AccountController from '@jwp/ott-common/src/controllers/AccountController';
8-
import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController';
9-
import { ACCESS_MODEL } from '@jwp/ott-common/src/constants';
10-
import useEventCallback from '@jwp/ott-hooks-react/src/useEventCallback';
1+
import React, { useMemo, useState } from 'react';
2+
import type { PaymentProvider } from '@jwp/ott-common/types/checkout';
113

12-
import Button from '../Button/Button';
134
import Spinner from '../Spinner/Spinner';
14-
import { modalURLFromLocation } from '../../utils/location';
15-
import { useAriaAnnouncer } from '../../containers/AnnouncementProvider/AnnoucementProvider';
165

6+
import FinalizeAdyenPayment from './FinalizeAdyenPayment';
7+
import FinalizeStripePpvPayment from './FinalizeStripePpvPayment';
178
import styles from './FinalizePayment.module.scss';
189

19-
const FinalizePayment = () => {
20-
const accountController = getModule(AccountController);
21-
const checkoutController = getModule(CheckoutController);
10+
type FinalizePaymentProps = { type: PaymentProvider };
2211

23-
const { t } = useTranslation('account');
24-
const announce = useAriaAnnouncer();
25-
const navigate = useNavigate();
26-
const location = useLocation();
27-
28-
const { accessModel } = useConfigStore(({ accessModel }) => ({ accessModel }));
29-
const [searchParams] = useSearchParams();
30-
const redirectResult = searchParams.get('redirectResult');
31-
const orderIdQueryParam = searchParams.get('orderId');
32-
33-
const [errorMessage, setErrorMessage] = useState<string>();
34-
35-
const paymentSuccessUrl = useMemo(() => {
36-
return modalURLFromLocation(location, accessModel === ACCESS_MODEL.SVOD ? 'welcome' : null);
37-
}, [accessModel, location]);
38-
39-
const checkPaymentResult = useEventCallback(async (redirectResult: string) => {
40-
const orderId = orderIdQueryParam ? parseInt(orderIdQueryParam, 10) : undefined;
41-
42-
try {
43-
await checkoutController.finalizeAdyenPayment({ redirectResult: decodeURI(redirectResult) }, orderId);
44-
await accountController.reloadSubscriptions({ retry: 10 });
45-
46-
announce(t('checkout.payment_success'), 'success');
47-
navigate(paymentSuccessUrl);
48-
} catch (error: unknown) {
49-
if (error instanceof Error) {
50-
setErrorMessage(error.message);
51-
}
52-
}
53-
});
54-
55-
useEffect(() => {
56-
if (!redirectResult) return;
57-
58-
checkPaymentResult(redirectResult);
59-
}, [checkPaymentResult, redirectResult]);
12+
const FinalizePayment = ({ type }: FinalizePaymentProps) => {
13+
const [isInProgress, setIsInProgress] = useState(true);
6014

6115
return (
6216
<div className={styles.container}>
63-
{errorMessage ? (
64-
<>
65-
<h2 className={styles.title}>{errorMessage}</h2>
66-
<Button
67-
label={t('checkout.go_back_to_checkout')}
68-
variant="contained"
69-
color="primary"
70-
size="large"
71-
onClick={() => navigate(modalURLFromLocation(location, 'checkout'))}
72-
fullWidth
73-
/>
74-
</>
75-
) : (
17+
{useMemo(() => {
18+
switch (type) {
19+
case 'adyen':
20+
return <FinalizeAdyenPayment onError={() => setIsInProgress(false)} />;
21+
case 'stripe':
22+
return <FinalizeStripePpvPayment />;
23+
default:
24+
return null;
25+
}
26+
}, [type])}
27+
{isInProgress && (
7628
<div>
7729
<Spinner />
7830
</div>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useCallback, useEffect } from 'react';
2+
import { useSearchParams } from 'react-router-dom';
3+
import { getModule } from '@jwp/ott-common/src/modules/container';
4+
import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController';
5+
6+
const FinalizeStripePpvPayment = () => {
7+
const checkoutController = getModule(CheckoutController);
8+
9+
const [searchParams, setSearchParams] = useSearchParams();
10+
11+
const finalize = useCallback(async (paymentIntent: string) => {
12+
try {
13+
await checkoutController.finalizeStripePpvPayment(paymentIntent);
14+
15+
setSearchParams({ u: 'waiting-for-payment' });
16+
} finally {
17+
// we don't need to handle any outcome here, it is handled by notifications
18+
// NotificationsTypes.CARD_SUCCESS and NotificationsTypes.CARD_FAILED
19+
}
20+
21+
// eslint-disable-next-line react-hooks/exhaustive-deps
22+
}, []);
23+
24+
useEffect(() => {
25+
const paymentIntent = searchParams.get('payment_intent');
26+
27+
if (paymentIntent) {
28+
finalize(paymentIntent);
29+
}
30+
31+
// eslint-disable-next-line react-hooks/exhaustive-deps
32+
}, []);
33+
34+
return null;
35+
};
36+
37+
export default FinalizeStripePpvPayment;

packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ const WaitingForPayment = () => {
3737
}
3838
},
3939
});
40-
//eslint-disable-next-line
40+
41+
// eslint-disable-next-line react-hooks/exhaustive-deps
4142
}, []);
4243
return (
4344
<div className={styles.center}>

packages/ui-react/src/containers/AccountModal/AccountModal.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export type AccountModals = {
6060
'payment-method': 'payment-method';
6161
'payment-method-success': 'payment-method-success';
6262
'waiting-for-payment': 'waiting-for-payment';
63-
'finalize-payment': 'finalize-payment';
63+
'finalize-payment-adyen': 'finalize-payment-adyen';
64+
'finalize-payment-stripe-ppv': 'finalize-payment-stripe-ppv';
6465
};
6566

6667
const AccountModal = () => {
@@ -157,8 +158,10 @@ const AccountModal = () => {
157158
return <UpdatePaymentMethod onCloseButtonClick={closeHandler} />;
158159
case 'waiting-for-payment':
159160
return <WaitingForPayment />;
160-
case 'finalize-payment':
161-
return <FinalizePayment />;
161+
case 'finalize-payment-adyen':
162+
return <FinalizePayment type="adyen" />;
163+
case 'finalize-payment-stripe-ppv':
164+
return <FinalizePayment type="stripe" />;
162165
}
163166
};
164167

packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ const Checkout = () => {
153153
{isStripePayment && (
154154
<PaymentForm
155155
onPaymentFormSubmit={async (cardPaymentPayload: PaymentFormData) =>
156-
await submitPaymentStripe.mutateAsync({ cardPaymentPayload, referrer, returnUrl: waitingUrl })
156+
await submitPaymentStripe.mutateAsync({
157+
cardPaymentPayload,
158+
referrer,
159+
returnUrl: modalURLFromWindowLocation('finalize-payment-stripe-ppv'),
160+
})
157161
}
158162
/>
159163
)}

packages/ui-react/src/containers/AdyenInitialPayment/AdyenInitialPayment.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default function AdyenInitialPayment({ setUpdatingOrder, type, paymentSuc
6969
const captchaValue = await getCaptchaValue();
7070

7171
const returnUrl = createURL(window.location.href, {
72-
u: 'finalize-payment',
72+
u: 'finalize-payment-adyen',
7373
orderId: orderId,
7474
});
7575
const result = await checkoutController.initialAdyenPayment(state.data.paymentMethod, returnUrl, captchaValue);

platforms/web/src/hooks/useNotifications.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export default function useNotifications() {
4949
navigateToModal('payment-error', notification.resource?.message);
5050
break;
5151
case NotificationsTypes.CARD_SUCCESS:
52-
await queryClient.invalidateQueries(['entitlements']);
52+
await Promise.allSettled([queryClient.invalidateQueries(['entitlements']), accountController.reloadSubscriptions()]);
5353
navigateToModal(null); // close modal
5454
break;
5555
case NotificationsTypes.SUBSCRIBE_SUCCESS:

0 commit comments

Comments
 (0)