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 () => {