From dea0f212cf1ae57150b7aef3026b7e784f337981 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski <vladimir.dimitrovski@jwplayer.com> Date: Wed, 12 Mar 2025 15:59:18 +0100 Subject: [PATCH 1/5] feat: implement broken sca flow payment finalization --- .../src/controllers/CheckoutController.ts | 3 ++ .../services/integrations/CheckoutService.ts | 3 ++ .../cleeng/CleengCheckoutService.ts | 2 + .../integrations/jwp/JWPCheckoutService.ts | 9 ++++ packages/common/types/checkout.ts | 1 + .../FinalizePPVPayment/FinalizePPVPayment.tsx | 48 +++++++++++++++++++ .../WaitingForPayment/WaitingForPayment.tsx | 12 ++++- .../containers/AccountModal/AccountModal.tsx | 4 ++ platforms/web/src/hooks/useNotifications.ts | 2 +- 9 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 packages/ui-react/src/components/FinalizePPVPayment/FinalizePPVPayment.tsx diff --git a/packages/common/src/controllers/CheckoutController.ts b/packages/common/src/controllers/CheckoutController.ts index c1a5b3d7d..c239fea9e 100644 --- a/packages/common/src/controllers/CheckoutController.ts +++ b/packages/common/src/controllers/CheckoutController.ts @@ -8,6 +8,7 @@ import type { CreateOrderArgs, FinalizeAdyenPayment, FinalizeAdyenPaymentDetailsResponse, + FinalizePpvPayment, GetEntitlements, GetOffers, InitialAdyenPayment, @@ -383,4 +384,6 @@ export default class CheckoutController { return this.checkoutService.getEntitlements(payload); }; + + finalizePpvPayment: FinalizePpvPayment = async (paymentIntent: string) => this.checkoutService?.finalizePpvPayment?.(paymentIntent); } diff --git a/packages/common/src/services/integrations/CheckoutService.ts b/packages/common/src/services/integrations/CheckoutService.ts index 483422073..353cf000d 100644 --- a/packages/common/src/services/integrations/CheckoutService.ts +++ b/packages/common/src/services/integrations/CheckoutService.ts @@ -3,6 +3,7 @@ import type { CreateOrder, DeletePaymentMethod, FinalizeAdyenPaymentDetails, + FinalizePpvPayment, GetAdyenPaymentSession, GetDirectPostCardPayment, GetEntitlements, @@ -61,4 +62,6 @@ export default abstract class CheckoutService { abstract addAdyenPaymentDetails?: AddAdyenPaymentDetails; abstract finalizeAdyenPaymentDetails?: FinalizeAdyenPaymentDetails; + + abstract finalizePpvPayment?: FinalizePpvPayment; } diff --git a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts index ae4338076..505fbd39e 100644 --- a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts +++ b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts @@ -184,4 +184,6 @@ export default class CleengCheckoutService extends CheckoutService { this.cleengService.post('/connectors/adyen/payment-details/finalize', JSON.stringify(payload), { authenticate: true }); directPostCardPayment = async () => false; + + finalizePpvPayment = undefined; } diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 18a5b10a3..74987d8f5 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -5,6 +5,7 @@ import type { CardPaymentData, CreateOrder, CreateOrderArgs, + FinalizePpvPayment, GetEntitlements, GetEntitlementsResponse, GetOffers, @@ -303,6 +304,14 @@ export default class JWPCheckoutService extends CheckoutService { } }; + finalizePpvPayment: FinalizePpvPayment = async (pi_id: string) => { + try { + await this.apiService.post<CommonResponse>('/payments', { pi_id }, { withAuthentication: true }); + } catch { + throw new Error('Failed to confirm payment'); + } + }; + getSubscriptionSwitches = undefined; getOrder = undefined; diff --git a/packages/common/types/checkout.ts b/packages/common/types/checkout.ts index c286f4ec6..3f4cc0dde 100644 --- a/packages/common/types/checkout.ts +++ b/packages/common/types/checkout.ts @@ -389,3 +389,4 @@ export type AddAdyenPaymentDetails = EnvironmentServiceRequest<AddAdyenPaymentDe export type FinalizeAdyenPaymentDetails = EnvironmentServiceRequest<FinalizeAdyenPaymentDetailsPayload, FinalizeAdyenPaymentDetailsResponse>; export type GetDirectPostCardPayment = (cardPaymentPayload: CardPaymentData, order: Order, referrer: string, returnUrl: string) => Promise<boolean>; export type GetEntitledPlans = PromiseRequest<{ siteId: string }, PlansResponse>; +export type FinalizePpvPayment = (paymentIntent: string) => Promise<any>; diff --git a/packages/ui-react/src/components/FinalizePPVPayment/FinalizePPVPayment.tsx b/packages/ui-react/src/components/FinalizePPVPayment/FinalizePPVPayment.tsx new file mode 100644 index 000000000..28bfa7d36 --- /dev/null +++ b/packages/ui-react/src/components/FinalizePPVPayment/FinalizePPVPayment.tsx @@ -0,0 +1,48 @@ +import { useCallback, useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { getModule } from '@jwp/ott-common/src/modules/container'; +import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; +import AccountController from '@jwp/ott-common/src/controllers/AccountController'; + +import Spinner from '../Spinner/Spinner'; + +// Stripe returns many search parameters we don't use, so once we're redirected here we remove any search parameter that is not included in this list +const KNOWN_PARAMS = ['app-config']; + +const FinalizePPVPayment = () => { + const checkoutController = getModule(CheckoutController); + const accountController = getModule(AccountController); + + const [searchParams, setSearchParams] = useSearchParams(); + + const finalize = useCallback(async (paymentIntent: string) => { + try { + await checkoutController.finalizePpvPayment(paymentIntent); + await accountController.reloadSubscriptions(); + } finally { + // we don't need to handle any outcome, it is handled by notifications + // NotificationsTypes.CARD_SUCCESS and NotificationsTypes.CARD_FAILED + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const paymentIntent = searchParams.get('payment_intent'); + + if (paymentIntent) { + setSearchParams(Object.fromEntries(Array.from(searchParams).filter(([name]) => KNOWN_PARAMS.includes(name)))); + finalize(paymentIntent); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + <div style={{ display: 'flex', justifyContent: 'center', minHeight: '90px', marginTop: '24px' }}> + <Spinner /> + </div> + ); +}; + +export default FinalizePPVPayment; diff --git a/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx b/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx index a11b4820a..6229e1666 100644 --- a/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx +++ b/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import useCheckAccess from '@jwp/ott-hooks-react/src/useCheckAccess'; import { modalURLFromLocation } from '@jwp/ott-ui-react/src/utils/location'; @@ -17,6 +17,13 @@ const WaitingForPayment = () => { const navigate = useNavigate(); const announce = useAriaAnnouncer(); const { intervalCheckAccess, errorMessage } = useCheckAccess(); + const [searchParams] = useSearchParams(); + + useEffect(() => { + if (searchParams.get('payment_intent')) { + navigate(modalURLFromLocation(location, 'finalize-ppv-payment'), { replace: true }); + } + }, [navigate, location, searchParams]); useEffect(() => { intervalCheckAccess({ @@ -37,7 +44,8 @@ const WaitingForPayment = () => { } }, }); - //eslint-disable-next-line + + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <div className={styles.center}> diff --git a/packages/ui-react/src/containers/AccountModal/AccountModal.tsx b/packages/ui-react/src/containers/AccountModal/AccountModal.tsx index 68e4ccfd4..721f1253b 100644 --- a/packages/ui-react/src/containers/AccountModal/AccountModal.tsx +++ b/packages/ui-react/src/containers/AccountModal/AccountModal.tsx @@ -13,6 +13,7 @@ import PaymentFailed from '../../components/PaymentFailed/PaymentFailed'; import Dialog from '../../components/Dialog/Dialog'; import DeleteAccountModal from '../../components/DeleteAccountModal/DeleteAccountModal'; import FinalizePayment from '../../components/FinalizePayment/FinalizePayment'; +import FinalizePPVPayment from '../../components/FinalizePPVPayment/FinalizePPVPayment'; import WaitingForPayment from '../../components/WaitingForPayment/WaitingForPayment'; import UpgradeSubscription from '../../components/UpgradeSubscription/UpgradeSubscription'; import DeleteAccountPasswordWarning from '../../components/DeleteAccountPasswordWarning/DeleteAccountPasswordWarning'; @@ -61,6 +62,7 @@ export type AccountModals = { 'payment-method-success': 'payment-method-success'; 'waiting-for-payment': 'waiting-for-payment'; 'finalize-payment': 'finalize-payment'; + 'finalize-ppv-payment': 'finalize-ppv-payment'; }; const AccountModal = () => { @@ -159,6 +161,8 @@ const AccountModal = () => { return <WaitingForPayment />; case 'finalize-payment': return <FinalizePayment />; + case 'finalize-ppv-payment': + return <FinalizePPVPayment />; } }; diff --git a/platforms/web/src/hooks/useNotifications.ts b/platforms/web/src/hooks/useNotifications.ts index 6de5aa374..ad21a24e6 100644 --- a/platforms/web/src/hooks/useNotifications.ts +++ b/platforms/web/src/hooks/useNotifications.ts @@ -49,7 +49,7 @@ export default function useNotifications() { navigateToModal('payment-error', notification.resource?.message); break; case NotificationsTypes.CARD_SUCCESS: - await queryClient.invalidateQueries(['entitlements']); + await Promise.allSettled([queryClient.invalidateQueries(['entitlements']), accountController.reloadSubscriptions()]); navigateToModal(null); // close modal break; case NotificationsTypes.SUBSCRIBE_SUCCESS: From 4d74fd917140957cb4fbfb68116761a8354cf22d Mon Sep 17 00:00:00 2001 From: mirovladimitrovski <vladimir.dimitrovski@jwplayer.com> Date: Thu, 13 Mar 2025 11:59:11 +0100 Subject: [PATCH 2/5] fix: simplify sca logic --- .../src/controllers/CheckoutController.ts | 3 -- .../services/integrations/CheckoutService.ts | 3 -- .../cleeng/CleengCheckoutService.ts | 2 - .../integrations/jwp/JWPAccountService.ts | 27 ++++++++--- .../integrations/jwp/JWPCheckoutService.ts | 9 ---- packages/common/types/checkout.ts | 1 - .../FinalizePPVPayment/FinalizePPVPayment.tsx | 48 ------------------- .../WaitingForPayment/WaitingForPayment.tsx | 9 +--- .../containers/AccountModal/AccountModal.tsx | 4 -- 9 files changed, 21 insertions(+), 85 deletions(-) delete mode 100644 packages/ui-react/src/components/FinalizePPVPayment/FinalizePPVPayment.tsx diff --git a/packages/common/src/controllers/CheckoutController.ts b/packages/common/src/controllers/CheckoutController.ts index c239fea9e..c1a5b3d7d 100644 --- a/packages/common/src/controllers/CheckoutController.ts +++ b/packages/common/src/controllers/CheckoutController.ts @@ -8,7 +8,6 @@ import type { CreateOrderArgs, FinalizeAdyenPayment, FinalizeAdyenPaymentDetailsResponse, - FinalizePpvPayment, GetEntitlements, GetOffers, InitialAdyenPayment, @@ -384,6 +383,4 @@ export default class CheckoutController { return this.checkoutService.getEntitlements(payload); }; - - finalizePpvPayment: FinalizePpvPayment = async (paymentIntent: string) => this.checkoutService?.finalizePpvPayment?.(paymentIntent); } diff --git a/packages/common/src/services/integrations/CheckoutService.ts b/packages/common/src/services/integrations/CheckoutService.ts index 353cf000d..483422073 100644 --- a/packages/common/src/services/integrations/CheckoutService.ts +++ b/packages/common/src/services/integrations/CheckoutService.ts @@ -3,7 +3,6 @@ import type { CreateOrder, DeletePaymentMethod, FinalizeAdyenPaymentDetails, - FinalizePpvPayment, GetAdyenPaymentSession, GetDirectPostCardPayment, GetEntitlements, @@ -62,6 +61,4 @@ export default abstract class CheckoutService { abstract addAdyenPaymentDetails?: AddAdyenPaymentDetails; abstract finalizeAdyenPaymentDetails?: FinalizeAdyenPaymentDetails; - - abstract finalizePpvPayment?: FinalizePpvPayment; } diff --git a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts index 505fbd39e..ae4338076 100644 --- a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts +++ b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts @@ -184,6 +184,4 @@ export default class CleengCheckoutService extends CheckoutService { this.cleengService.post('/connectors/adyen/payment-details/finalize', JSON.stringify(payload), { authenticate: true }); directPostCardPayment = async () => false; - - finalizePpvPayment = undefined; } diff --git a/packages/common/src/services/integrations/jwp/JWPAccountService.ts b/packages/common/src/services/integrations/jwp/JWPAccountService.ts index 37a343f31..31315a6e5 100644 --- a/packages/common/src/services/integrations/jwp/JWPAccountService.ts +++ b/packages/common/src/services/integrations/jwp/JWPAccountService.ts @@ -140,6 +140,14 @@ export default class JWPAccountService extends AccountService { }; } + private async handlePpvPaymentIntent(pi_id: string) { + try { + await this.apiService.post<CommonResponse>('/payments', { pi_id }, { withAuthentication: true }); + } catch { + throw new Error('Failed to confirm payment'); + } + } + initialize = async (config: Config, url: string, _logoutFn: () => Promise<void>) => { const jwpConfig = config.integrations?.jwp; @@ -167,16 +175,21 @@ export default class JWPAccountService extends AccountService { } // restore session from URL params - const queryParams = new URLSearchParams(url.split('#')[1]); - const token = queryParams.get('token'); - const refreshToken = queryParams.get('refresh_token'); - const expires = queryParams.get('expires'); + const hashParams = new URLSearchParams(url.split('#')[1]); + const token = hashParams.get('token'); + const refreshToken = hashParams.get('refresh_token'); + const expires = hashParams.get('expires'); + + const searchParams = new URLSearchParams(url.split('?')[1].split('#')[0]); + const paymentIntent = searchParams.get('payment_intent'); - if (!token || !refreshToken || !expires) { - return; + if (token && refreshToken && expires) { + await this.apiService.setToken(token, refreshToken, parseInt(expires)); } - this.apiService.setToken(token, refreshToken, parseInt(expires)); + if (paymentIntent) { + await this.handlePpvPaymentIntent(paymentIntent); + } }; getAuthData = async () => { diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 74987d8f5..18a5b10a3 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -5,7 +5,6 @@ import type { CardPaymentData, CreateOrder, CreateOrderArgs, - FinalizePpvPayment, GetEntitlements, GetEntitlementsResponse, GetOffers, @@ -304,14 +303,6 @@ export default class JWPCheckoutService extends CheckoutService { } }; - finalizePpvPayment: FinalizePpvPayment = async (pi_id: string) => { - try { - await this.apiService.post<CommonResponse>('/payments', { pi_id }, { withAuthentication: true }); - } catch { - throw new Error('Failed to confirm payment'); - } - }; - getSubscriptionSwitches = undefined; getOrder = undefined; diff --git a/packages/common/types/checkout.ts b/packages/common/types/checkout.ts index 3f4cc0dde..c286f4ec6 100644 --- a/packages/common/types/checkout.ts +++ b/packages/common/types/checkout.ts @@ -389,4 +389,3 @@ export type AddAdyenPaymentDetails = EnvironmentServiceRequest<AddAdyenPaymentDe export type FinalizeAdyenPaymentDetails = EnvironmentServiceRequest<FinalizeAdyenPaymentDetailsPayload, FinalizeAdyenPaymentDetailsResponse>; export type GetDirectPostCardPayment = (cardPaymentPayload: CardPaymentData, order: Order, referrer: string, returnUrl: string) => Promise<boolean>; export type GetEntitledPlans = PromiseRequest<{ siteId: string }, PlansResponse>; -export type FinalizePpvPayment = (paymentIntent: string) => Promise<any>; diff --git a/packages/ui-react/src/components/FinalizePPVPayment/FinalizePPVPayment.tsx b/packages/ui-react/src/components/FinalizePPVPayment/FinalizePPVPayment.tsx deleted file mode 100644 index 28bfa7d36..000000000 --- a/packages/ui-react/src/components/FinalizePPVPayment/FinalizePPVPayment.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useCallback, useEffect } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { getModule } from '@jwp/ott-common/src/modules/container'; -import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; -import AccountController from '@jwp/ott-common/src/controllers/AccountController'; - -import Spinner from '../Spinner/Spinner'; - -// Stripe returns many search parameters we don't use, so once we're redirected here we remove any search parameter that is not included in this list -const KNOWN_PARAMS = ['app-config']; - -const FinalizePPVPayment = () => { - const checkoutController = getModule(CheckoutController); - const accountController = getModule(AccountController); - - const [searchParams, setSearchParams] = useSearchParams(); - - const finalize = useCallback(async (paymentIntent: string) => { - try { - await checkoutController.finalizePpvPayment(paymentIntent); - await accountController.reloadSubscriptions(); - } finally { - // we don't need to handle any outcome, it is handled by notifications - // NotificationsTypes.CARD_SUCCESS and NotificationsTypes.CARD_FAILED - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - const paymentIntent = searchParams.get('payment_intent'); - - if (paymentIntent) { - setSearchParams(Object.fromEntries(Array.from(searchParams).filter(([name]) => KNOWN_PARAMS.includes(name)))); - finalize(paymentIntent); - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - <div style={{ display: 'flex', justifyContent: 'center', minHeight: '90px', marginTop: '24px' }}> - <Spinner /> - </div> - ); -}; - -export default FinalizePPVPayment; diff --git a/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx b/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx index 6229e1666..ac8db4bd4 100644 --- a/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx +++ b/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import useCheckAccess from '@jwp/ott-hooks-react/src/useCheckAccess'; import { modalURLFromLocation } from '@jwp/ott-ui-react/src/utils/location'; @@ -17,13 +17,6 @@ const WaitingForPayment = () => { const navigate = useNavigate(); const announce = useAriaAnnouncer(); const { intervalCheckAccess, errorMessage } = useCheckAccess(); - const [searchParams] = useSearchParams(); - - useEffect(() => { - if (searchParams.get('payment_intent')) { - navigate(modalURLFromLocation(location, 'finalize-ppv-payment'), { replace: true }); - } - }, [navigate, location, searchParams]); useEffect(() => { intervalCheckAccess({ diff --git a/packages/ui-react/src/containers/AccountModal/AccountModal.tsx b/packages/ui-react/src/containers/AccountModal/AccountModal.tsx index 721f1253b..68e4ccfd4 100644 --- a/packages/ui-react/src/containers/AccountModal/AccountModal.tsx +++ b/packages/ui-react/src/containers/AccountModal/AccountModal.tsx @@ -13,7 +13,6 @@ import PaymentFailed from '../../components/PaymentFailed/PaymentFailed'; import Dialog from '../../components/Dialog/Dialog'; import DeleteAccountModal from '../../components/DeleteAccountModal/DeleteAccountModal'; import FinalizePayment from '../../components/FinalizePayment/FinalizePayment'; -import FinalizePPVPayment from '../../components/FinalizePPVPayment/FinalizePPVPayment'; import WaitingForPayment from '../../components/WaitingForPayment/WaitingForPayment'; import UpgradeSubscription from '../../components/UpgradeSubscription/UpgradeSubscription'; import DeleteAccountPasswordWarning from '../../components/DeleteAccountPasswordWarning/DeleteAccountPasswordWarning'; @@ -62,7 +61,6 @@ export type AccountModals = { 'payment-method-success': 'payment-method-success'; 'waiting-for-payment': 'waiting-for-payment'; 'finalize-payment': 'finalize-payment'; - 'finalize-ppv-payment': 'finalize-ppv-payment'; }; const AccountModal = () => { @@ -161,8 +159,6 @@ const AccountModal = () => { return <WaitingForPayment />; case 'finalize-payment': return <FinalizePayment />; - case 'finalize-ppv-payment': - return <FinalizePPVPayment />; } }; From e310049f70014e73aaea73b2c69ce79ae0873712 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski <vladimir.dimitrovski@jwplayer.com> Date: Fri, 14 Mar 2025 12:34:56 +0100 Subject: [PATCH 3/5] fix: refactor finalizing stripe ppv payment --- .../src/controllers/CheckoutController.ts | 2 + .../services/integrations/CheckoutService.ts | 3 + .../cleeng/CleengCheckoutService.ts | 2 + .../integrations/jwp/JWPAccountService.ts | 27 ++---- .../integrations/jwp/JWPCheckoutService.ts | 8 ++ packages/common/types/checkout.ts | 5 +- .../FinalizePayment/FinalizeAdyenPayment.tsx | 84 +++++++++++++++++++ .../FinalizePayment.module.scss | 1 - .../FinalizePayment/FinalizePayment.tsx | 84 ++++--------------- .../FinalizeStripePpvPayment.tsx | 37 ++++++++ .../containers/AccountModal/AccountModal.tsx | 9 +- .../AccountModal/forms/Checkout.tsx | 6 +- .../AdyenInitialPayment.tsx | 2 +- 13 files changed, 177 insertions(+), 93 deletions(-) create mode 100644 packages/ui-react/src/components/FinalizePayment/FinalizeAdyenPayment.tsx create mode 100644 packages/ui-react/src/components/FinalizePayment/FinalizeStripePpvPayment.tsx diff --git a/packages/common/src/controllers/CheckoutController.ts b/packages/common/src/controllers/CheckoutController.ts index c1a5b3d7d..c75540db4 100644 --- a/packages/common/src/controllers/CheckoutController.ts +++ b/packages/common/src/controllers/CheckoutController.ts @@ -224,6 +224,8 @@ export default class CheckoutController { return response.responseData; }; + finalizeStripePpvPayment = (paymentIntent: string) => this.checkoutService?.finalizeStripePpvPayment?.({ paymentIntent }); + paypalPayment = async ({ successUrl, waitingUrl, diff --git a/packages/common/src/services/integrations/CheckoutService.ts b/packages/common/src/services/integrations/CheckoutService.ts index 483422073..ecce38568 100644 --- a/packages/common/src/services/integrations/CheckoutService.ts +++ b/packages/common/src/services/integrations/CheckoutService.ts @@ -7,6 +7,7 @@ import type { GetDirectPostCardPayment, GetEntitlements, GetFinalizeAdyenPayment, + GetFinalizeStripePpvPayment, GetInitialAdyenPayment, GetOffer, GetOffers, @@ -54,6 +55,8 @@ export default abstract class CheckoutService { abstract finalizeAdyenPayment?: GetFinalizeAdyenPayment; + abstract finalizeStripePpvPayment?: GetFinalizeStripePpvPayment; + abstract updatePaymentMethodWithPayPal?: UpdatePaymentWithPayPal; abstract deletePaymentMethod?: DeletePaymentMethod; diff --git a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts index ae4338076..3d8b0046b 100644 --- a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts +++ b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts @@ -162,6 +162,8 @@ export default class CleengCheckoutService extends CheckoutService { finalizeAdyenPayment: GetFinalizeAdyenPayment = async (payload) => this.cleengService.post('/connectors/adyen/initial-payment/finalize', JSON.stringify(payload), { authenticate: true }); + finalizeStripePpvPayment: undefined; + updatePaymentMethodWithPayPal: UpdatePaymentWithPayPal = async (payload) => { return this.cleengService.post('/connectors/paypal/v1/payment_details/tokens', JSON.stringify(payload), { authenticate: true }); }; diff --git a/packages/common/src/services/integrations/jwp/JWPAccountService.ts b/packages/common/src/services/integrations/jwp/JWPAccountService.ts index 31315a6e5..c4c96e504 100644 --- a/packages/common/src/services/integrations/jwp/JWPAccountService.ts +++ b/packages/common/src/services/integrations/jwp/JWPAccountService.ts @@ -140,14 +140,6 @@ export default class JWPAccountService extends AccountService { }; } - private async handlePpvPaymentIntent(pi_id: string) { - try { - await this.apiService.post<CommonResponse>('/payments', { pi_id }, { withAuthentication: true }); - } catch { - throw new Error('Failed to confirm payment'); - } - } - initialize = async (config: Config, url: string, _logoutFn: () => Promise<void>) => { const jwpConfig = config.integrations?.jwp; @@ -175,21 +167,16 @@ export default class JWPAccountService extends AccountService { } // restore session from URL params - const hashParams = new URLSearchParams(url.split('#')[1]); - const token = hashParams.get('token'); - const refreshToken = hashParams.get('refresh_token'); - const expires = hashParams.get('expires'); - - const searchParams = new URLSearchParams(url.split('?')[1].split('#')[0]); - const paymentIntent = searchParams.get('payment_intent'); + const searchParams = new URLSearchParams(url.split('#')[1]); + const token = searchParams.get('token'); + const refreshToken = searchParams.get('refresh_token'); + const expires = searchParams.get('expires'); - if (token && refreshToken && expires) { - await this.apiService.setToken(token, refreshToken, parseInt(expires)); + if (!token || !refreshToken || !expires) { + return; } - if (paymentIntent) { - await this.handlePpvPaymentIntent(paymentIntent); - } + await this.apiService.setToken(token, refreshToken, parseInt(expires)); }; getAuthData = async () => { diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 18a5b10a3..25be61b21 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -303,6 +303,14 @@ export default class JWPCheckoutService extends CheckoutService { } }; + finalizeStripePpvPayment = async ({ paymentIntent }: { paymentIntent: string }) => { + try { + await this.apiService.post<CommonResponse>('/payments', { pi_id: paymentIntent }, { withAuthentication: true }); + } catch { + throw new Error('Failed to confirm payment'); + } + }; + getSubscriptionSwitches = undefined; getOrder = undefined; diff --git a/packages/common/types/checkout.ts b/packages/common/types/checkout.ts index c286f4ec6..3df79281d 100644 --- a/packages/common/types/checkout.ts +++ b/packages/common/types/checkout.ts @@ -94,10 +94,12 @@ export type Order = { requiredPaymentDetails: boolean; }; +export type PaymentProvider = 'stripe' | 'adyen'; + export type PaymentMethod = { id: number; methodName: 'card' | 'paypal'; - provider?: 'stripe' | 'adyen'; + provider?: PaymentProvider; paymentGateway?: 'adyen' | 'paypal'; // @todo: merge with provider logoUrl: string; }; @@ -383,6 +385,7 @@ export type GetEntitlements = EnvironmentServiceRequest<GetEntitlementsPayload, export type GetAdyenPaymentSession = EnvironmentServiceRequest<AdyenPaymentMethodPayload, AdyenPaymentSession>; export type GetInitialAdyenPayment = EnvironmentServiceRequest<InitialAdyenPaymentPayload, InitialAdyenPayment>; export type GetFinalizeAdyenPayment = EnvironmentServiceRequest<FinalizeAdyenPaymentPayload, FinalizeAdyenPayment>; +export type GetFinalizeStripePpvPayment = PromiseRequest<{ paymentIntent: string }, void>; export type UpdatePaymentWithPayPal = EnvironmentServiceRequest<UpdatePaymentWithPayPalPayload, PaymentWithPayPalResponse>; export type DeletePaymentMethod = EnvironmentServiceRequest<DeletePaymentMethodPayload, DeletePaymentMethodResponse>; export type AddAdyenPaymentDetails = EnvironmentServiceRequest<AddAdyenPaymentDetailsPayload, AddAdyenPaymentDetailsResponse>; diff --git a/packages/ui-react/src/components/FinalizePayment/FinalizeAdyenPayment.tsx b/packages/ui-react/src/components/FinalizePayment/FinalizeAdyenPayment.tsx new file mode 100644 index 000000000..05722fe11 --- /dev/null +++ b/packages/ui-react/src/components/FinalizePayment/FinalizeAdyenPayment.tsx @@ -0,0 +1,84 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate } from 'react-router'; +import { useSearchParams } from 'react-router-dom'; +import { getModule } from '@jwp/ott-common/src/modules/container'; +import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; +import AccountController from '@jwp/ott-common/src/controllers/AccountController'; +import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; +import { ACCESS_MODEL } from '@jwp/ott-common/src/constants'; +import useEventCallback from '@jwp/ott-hooks-react/src/useEventCallback'; + +import Button from '../Button/Button'; +import { modalURLFromLocation } from '../../utils/location'; +import { useAriaAnnouncer } from '../../containers/AnnouncementProvider/AnnoucementProvider'; + +import styles from './FinalizePayment.module.scss'; + +type FinalizeAdyenPaymentProps = { + onError: () => void; +}; + +const FinalizeAdyenPayment = ({ onError }: FinalizeAdyenPaymentProps) => { + const accountController = getModule(AccountController); + const checkoutController = getModule(CheckoutController); + + const { t } = useTranslation('account'); + const announce = useAriaAnnouncer(); + const navigate = useNavigate(); + const location = useLocation(); + + const { accessModel } = useConfigStore(({ accessModel }) => ({ accessModel })); + const [searchParams] = useSearchParams(); + const redirectResult = searchParams.get('redirectResult'); + const orderIdQueryParam = searchParams.get('orderId'); + + const [errorMessage, setErrorMessage] = useState<string>(); + + const paymentSuccessUrl = useMemo(() => { + return modalURLFromLocation(location, accessModel === ACCESS_MODEL.SVOD ? 'welcome' : null); + }, [accessModel, location]); + + const checkPaymentResult = useEventCallback(async (redirectResult: string) => { + const orderId = orderIdQueryParam ? parseInt(orderIdQueryParam, 10) : undefined; + + try { + await checkoutController.finalizeAdyenPayment({ redirectResult: decodeURI(redirectResult) }, orderId); + await accountController.reloadSubscriptions({ retry: 10 }); + + announce(t('checkout.payment_success'), 'success'); + navigate(paymentSuccessUrl); + } catch (error: unknown) { + if (error instanceof Error) { + setErrorMessage(error.message); + onError(); + } + } + }); + + useEffect(() => { + if (!redirectResult) return; + + checkPaymentResult(redirectResult); + }, [checkPaymentResult, redirectResult]); + + return ( + <> + {errorMessage && ( + <> + <h2 className={styles.title}>{errorMessage}</h2> + <Button + label={t('checkout.go_back_to_checkout')} + variant="contained" + color="primary" + size="large" + onClick={() => navigate(modalURLFromLocation(location, 'checkout'))} + fullWidth + /> + </> + )} + </> + ); +}; + +export default FinalizeAdyenPayment; diff --git a/packages/ui-react/src/components/FinalizePayment/FinalizePayment.module.scss b/packages/ui-react/src/components/FinalizePayment/FinalizePayment.module.scss index afdaeb42e..8b0e88f77 100644 --- a/packages/ui-react/src/components/FinalizePayment/FinalizePayment.module.scss +++ b/packages/ui-react/src/components/FinalizePayment/FinalizePayment.module.scss @@ -10,4 +10,3 @@ font-weight: var(--body-font-weight-bold); font-size: 24px; } - diff --git a/packages/ui-react/src/components/FinalizePayment/FinalizePayment.tsx b/packages/ui-react/src/components/FinalizePayment/FinalizePayment.tsx index 979728f59..a48fba722 100644 --- a/packages/ui-react/src/components/FinalizePayment/FinalizePayment.tsx +++ b/packages/ui-react/src/components/FinalizePayment/FinalizePayment.tsx @@ -1,78 +1,30 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useLocation, useNavigate } from 'react-router'; -import { useSearchParams } from 'react-router-dom'; -import { getModule } from '@jwp/ott-common/src/modules/container'; -import { useConfigStore } from '@jwp/ott-common/src/stores/ConfigStore'; -import AccountController from '@jwp/ott-common/src/controllers/AccountController'; -import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; -import { ACCESS_MODEL } from '@jwp/ott-common/src/constants'; -import useEventCallback from '@jwp/ott-hooks-react/src/useEventCallback'; +import React, { useMemo, useState } from 'react'; +import type { PaymentProvider } from '@jwp/ott-common/types/checkout'; -import Button from '../Button/Button'; import Spinner from '../Spinner/Spinner'; -import { modalURLFromLocation } from '../../utils/location'; -import { useAriaAnnouncer } from '../../containers/AnnouncementProvider/AnnoucementProvider'; +import FinalizeAdyenPayment from './FinalizeAdyenPayment'; +import FinalizeStripePpvPayment from './FinalizeStripePpvPayment'; import styles from './FinalizePayment.module.scss'; -const FinalizePayment = () => { - const accountController = getModule(AccountController); - const checkoutController = getModule(CheckoutController); +type FinalizePaymentProps = { type: PaymentProvider }; - const { t } = useTranslation('account'); - const announce = useAriaAnnouncer(); - const navigate = useNavigate(); - const location = useLocation(); - - const { accessModel } = useConfigStore(({ accessModel }) => ({ accessModel })); - const [searchParams] = useSearchParams(); - const redirectResult = searchParams.get('redirectResult'); - const orderIdQueryParam = searchParams.get('orderId'); - - const [errorMessage, setErrorMessage] = useState<string>(); - - const paymentSuccessUrl = useMemo(() => { - return modalURLFromLocation(location, accessModel === ACCESS_MODEL.SVOD ? 'welcome' : null); - }, [accessModel, location]); - - const checkPaymentResult = useEventCallback(async (redirectResult: string) => { - const orderId = orderIdQueryParam ? parseInt(orderIdQueryParam, 10) : undefined; - - try { - await checkoutController.finalizeAdyenPayment({ redirectResult: decodeURI(redirectResult) }, orderId); - await accountController.reloadSubscriptions({ retry: 10 }); - - announce(t('checkout.payment_success'), 'success'); - navigate(paymentSuccessUrl); - } catch (error: unknown) { - if (error instanceof Error) { - setErrorMessage(error.message); - } - } - }); - - useEffect(() => { - if (!redirectResult) return; - - checkPaymentResult(redirectResult); - }, [checkPaymentResult, redirectResult]); +const FinalizePayment = ({ type }: FinalizePaymentProps) => { + const [isInProgress, setIsInProgress] = useState(true); return ( <div className={styles.container}> - {errorMessage ? ( - <> - <h2 className={styles.title}>{errorMessage}</h2> - <Button - label={t('checkout.go_back_to_checkout')} - variant="contained" - color="primary" - size="large" - onClick={() => navigate(modalURLFromLocation(location, 'checkout'))} - fullWidth - /> - </> - ) : ( + {useMemo(() => { + switch (type) { + case 'adyen': + return <FinalizeAdyenPayment onError={() => setIsInProgress(false)} />; + case 'stripe': + return <FinalizeStripePpvPayment />; + default: + return null; + } + }, [type])} + {isInProgress && ( <div> <Spinner /> </div> diff --git a/packages/ui-react/src/components/FinalizePayment/FinalizeStripePpvPayment.tsx b/packages/ui-react/src/components/FinalizePayment/FinalizeStripePpvPayment.tsx new file mode 100644 index 000000000..f91db3d08 --- /dev/null +++ b/packages/ui-react/src/components/FinalizePayment/FinalizeStripePpvPayment.tsx @@ -0,0 +1,37 @@ +import { useCallback, useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { getModule } from '@jwp/ott-common/src/modules/container'; +import CheckoutController from '@jwp/ott-common/src/controllers/CheckoutController'; + +const FinalizeStripePpvPayment = () => { + const checkoutController = getModule(CheckoutController); + + const [searchParams, setSearchParams] = useSearchParams(); + + const finalize = useCallback(async (paymentIntent: string) => { + try { + await checkoutController.finalizeStripePpvPayment(paymentIntent); + + setSearchParams({ u: 'waiting-for-payment' }); + } finally { + // we don't need to handle any outcome here, it is handled by notifications + // NotificationsTypes.CARD_SUCCESS and NotificationsTypes.CARD_FAILED + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const paymentIntent = searchParams.get('payment_intent'); + + if (paymentIntent) { + finalize(paymentIntent); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return null; +}; + +export default FinalizeStripePpvPayment; diff --git a/packages/ui-react/src/containers/AccountModal/AccountModal.tsx b/packages/ui-react/src/containers/AccountModal/AccountModal.tsx index 68e4ccfd4..42ecf453c 100644 --- a/packages/ui-react/src/containers/AccountModal/AccountModal.tsx +++ b/packages/ui-react/src/containers/AccountModal/AccountModal.tsx @@ -60,7 +60,8 @@ export type AccountModals = { 'payment-method': 'payment-method'; 'payment-method-success': 'payment-method-success'; 'waiting-for-payment': 'waiting-for-payment'; - 'finalize-payment': 'finalize-payment'; + 'finalize-payment-adyen': 'finalize-payment-adyen'; + 'finalize-payment-stripe-ppv': 'finalize-payment-stripe-ppv'; }; const AccountModal = () => { @@ -157,8 +158,10 @@ const AccountModal = () => { return <UpdatePaymentMethod onCloseButtonClick={closeHandler} />; case 'waiting-for-payment': return <WaitingForPayment />; - case 'finalize-payment': - return <FinalizePayment />; + case 'finalize-payment-adyen': + return <FinalizePayment type="adyen" />; + case 'finalize-payment-stripe-ppv': + return <FinalizePayment type="stripe" />; } }; diff --git a/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx b/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx index 55fd3dbbe..2959adc9f 100644 --- a/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx +++ b/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx @@ -153,7 +153,11 @@ const Checkout = () => { {isStripePayment && ( <PaymentForm onPaymentFormSubmit={async (cardPaymentPayload: PaymentFormData) => - await submitPaymentStripe.mutateAsync({ cardPaymentPayload, referrer, returnUrl: waitingUrl }) + await submitPaymentStripe.mutateAsync({ + cardPaymentPayload, + referrer, + returnUrl: modalURLFromWindowLocation('finalize-payment-stripe-ppv'), + }) } /> )} diff --git a/packages/ui-react/src/containers/AdyenInitialPayment/AdyenInitialPayment.tsx b/packages/ui-react/src/containers/AdyenInitialPayment/AdyenInitialPayment.tsx index 3a5312488..c8e1b1b1f 100644 --- a/packages/ui-react/src/containers/AdyenInitialPayment/AdyenInitialPayment.tsx +++ b/packages/ui-react/src/containers/AdyenInitialPayment/AdyenInitialPayment.tsx @@ -69,7 +69,7 @@ export default function AdyenInitialPayment({ setUpdatingOrder, type, paymentSuc const captchaValue = await getCaptchaValue(); const returnUrl = createURL(window.location.href, { - u: 'finalize-payment', + u: 'finalize-payment-adyen', orderId: orderId, }); const result = await checkoutController.initialAdyenPayment(state.data.paymentMethod, returnUrl, captchaValue); From 8babd30225b89078d47d526183171a810a6667c3 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski <vladimir.dimitrovski@jwplayer.com> Date: Fri, 14 Mar 2025 12:57:26 +0100 Subject: [PATCH 4/5] fix: restore old name of variable --- .../src/services/integrations/jwp/JWPAccountService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/common/src/services/integrations/jwp/JWPAccountService.ts b/packages/common/src/services/integrations/jwp/JWPAccountService.ts index c4c96e504..274aaa8b9 100644 --- a/packages/common/src/services/integrations/jwp/JWPAccountService.ts +++ b/packages/common/src/services/integrations/jwp/JWPAccountService.ts @@ -167,10 +167,10 @@ export default class JWPAccountService extends AccountService { } // restore session from URL params - const searchParams = new URLSearchParams(url.split('#')[1]); - const token = searchParams.get('token'); - const refreshToken = searchParams.get('refresh_token'); - const expires = searchParams.get('expires'); + const queryParams = new URLSearchParams(url.split('#')[1]); + const token = queryParams.get('token'); + const refreshToken = queryParams.get('refresh_token'); + const expires = queryParams.get('expires'); if (!token || !refreshToken || !expires) { return; From c81ef1697f225ec26442e7dc44af19d84ae2d9b0 Mon Sep 17 00:00:00 2001 From: mirovladimitrovski <vladimir.dimitrovski@jwplayer.com> Date: Fri, 14 Mar 2025 12:58:11 +0100 Subject: [PATCH 5/5] fix: remove await from statement --- .../common/src/services/integrations/jwp/JWPAccountService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/services/integrations/jwp/JWPAccountService.ts b/packages/common/src/services/integrations/jwp/JWPAccountService.ts index 274aaa8b9..37a343f31 100644 --- a/packages/common/src/services/integrations/jwp/JWPAccountService.ts +++ b/packages/common/src/services/integrations/jwp/JWPAccountService.ts @@ -176,7 +176,7 @@ export default class JWPAccountService extends AccountService { return; } - await this.apiService.setToken(token, refreshToken, parseInt(expires)); + this.apiService.setToken(token, refreshToken, parseInt(expires)); }; getAuthData = async () => {