From cee54fbe346e31551a105703409c1812017c6fde Mon Sep 17 00:00:00 2001 From: Christiaan Scheermeijer Date: Tue, 4 Jun 2024 11:50:53 +0200 Subject: [PATCH 1/5] fix(project): waiting for payment not working for ppv --- .../src/controllers/AccountController.ts | 2 +- .../integrations/jwp/JWPCheckoutService.ts | 37 ++++++++++++++++--- packages/hooks-react/src/useCheckAccess.ts | 20 +++++++--- .../WaitingForPayment/WaitingForPayment.tsx | 11 +++++- yarn.lock | 33 ++--------------- 5 files changed, 59 insertions(+), 44 deletions(-) diff --git a/packages/common/src/controllers/AccountController.ts b/packages/common/src/controllers/AccountController.ts index af9cd99ee..5c876c941 100644 --- a/packages/common/src/controllers/AccountController.ts +++ b/packages/common/src/controllers/AccountController.ts @@ -419,7 +419,7 @@ export default class AccountController { let pendingOffer: Offer | null = null; if (!activeSubscription && !!retry && retry > 0) { - const retryDelay = 1500; // Any initial delay has already occured, so we can set this to a fixed value + const retryDelay = 1500; // Any initial delay has already occurred, so we can set this to a fixed value return await this.reloadSubscriptions({ delay: retryDelay, retry: retry - 1 }); } diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 678c50cd5..c22c678ed 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -46,13 +46,38 @@ export default class JWPCheckoutService extends CheckoutService { }; }; - private formatOffer = (offer: AccessFee): Offer => { + /** + * Format a (Cleeng like) offer id for the given access fee (pricing option). For JWP, we need the asset id and + * access fee id in some cases. + */ + private formatOfferId(offer: AccessFee) { const ppvOffers = ['ppv', 'ppv_custom']; - const offerId = ppvOffers.includes(offer.access_type.name) ? `C${offer.id}` : `S${offer.id}`; + return ppvOffers.includes(offer.access_type.name) ? `C${offer.item_id}_${offer.id}` : `S${offer.item_id}_${offer.id}`; + } + + /** + * Parse the given offer id and extract the asset id. + * The offer id might be the Cleeng format (`S_`) or the asset id as string. + */ + private parseOfferId(offerId: string | number) { + if (typeof offerId === 'string') { + // offer id format `S_` + if (offerId.startsWith('C') || offerId.startsWith('S')) { + return parseInt(offerId.slice(1).split('_')[0]); + } + + // offer id format `` + return parseInt(offerId); + } + + return offerId; + } + + private formatOffer = (offer: AccessFee): Offer => { return { id: offer.id, - offerId, + offerId: this.formatOfferId(offer), offerCurrency: offer.currency, customerPriceInclTax: offer.amount, customerCurrency: offer.currency, @@ -97,9 +122,9 @@ export default class JWPCheckoutService extends CheckoutService { getOffers: GetOffers = async (payload) => { const offers = await Promise.all( - payload.offerIds.map(async (assetId) => { + payload.offerIds.map(async (offerId) => { try { - const { data } = await InPlayer.Asset.getAssetAccessFees(parseInt(`${assetId}`)); + const { data } = await InPlayer.Asset.getAssetAccessFees(this.parseOfferId(offerId)); return data?.map((offer) => this.formatOffer(offer)); } catch { @@ -217,7 +242,7 @@ export default class JWPCheckoutService extends CheckoutService { getEntitlements: GetEntitlements = async ({ offerId }) => { try { - const response = await InPlayer.Asset.checkAccessForAsset(parseInt(offerId)); + const response = await InPlayer.Asset.checkAccessForAsset(this.parseOfferId(offerId)); return this.formatEntitlements(response.data.expires_at, true); } catch { return this.formatEntitlements(); diff --git a/packages/hooks-react/src/useCheckAccess.ts b/packages/hooks-react/src/useCheckAccess.ts index de50537e4..d559689cd 100644 --- a/packages/hooks-react/src/useCheckAccess.ts +++ b/packages/hooks-react/src/useCheckAccess.ts @@ -8,7 +8,7 @@ type IntervalCheckAccessPayload = { interval?: number; iterations?: number; offerId?: string; - callback?: (hasAccess: boolean) => void; + callback?: ({ hasAccess, offerId }: { hasAccess: boolean; offerId: string }) => void; }; const useCheckAccess = () => { @@ -23,20 +23,28 @@ const useCheckAccess = () => { const intervalCheckAccess = useCallback( ({ interval = 3000, iterations = 5, offerId, callback }: IntervalCheckAccessPayload) => { - if (!offerId && offers?.[0]) { - offerId = offers[0]; + if (!offerId) { + offerId = offers?.[0] || ''; + } + + if (!offerId) { + callback?.({ hasAccess: false, offerId: '' }); + return; } intervalRef.current = window.setInterval(async () => { const hasAccess = await accountController.checkEntitlements(offerId); if (hasAccess) { - await accountController.reloadSubscriptions({ retry: 10, delay: 2000 }); - callback?.(true); + window.clearInterval(intervalRef.current); + // No duplicate retry mechanism. This can also be a TVOD offer which isn't validated using the + // reloadSubscriptions method. + await accountController.reloadSubscriptions(); + callback?.({ hasAccess: true, offerId: offerId || '' }); } else if (--iterations === 0) { window.clearInterval(intervalRef.current); setErrorMessage(t('payment.longer_than_usual')); - callback?.(false); + callback?.({ hasAccess: false, offerId: offerId || '' }); } }, interval); }, diff --git a/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx b/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx index 9e40b7319..4c7390c58 100644 --- a/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx +++ b/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx @@ -23,11 +23,18 @@ const WaitingForPayment = () => { interval: 3000, iterations: 5, offerId, - callback: (hasAccess) => { + callback: ({ hasAccess, offerId }) => { if (!hasAccess) return; announce(t('checkout.payment_success'), 'success'); - navigate(modalURLFromLocation(location, 'welcome')); + + // close the modal for PPV/TVOD offers + if (offerId.startsWith('C') || offerId.startsWith('P')) { + // should we show a dedicated modal for TVOD access? + navigate(modalURLFromLocation(location, null)); + } else { + navigate(modalURLFromLocation(location, 'welcome')); + } }, }); //eslint-disable-next-line diff --git a/yarn.lock b/yarn.lock index bd70ffb7f..5f8926a09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1879,7 +1879,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.2.1": +"@jridgewell/set-array@^1.0.1", "@jridgewell/set-array@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== @@ -9925,7 +9925,7 @@ string-argv@0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9942,15 +9942,6 @@ string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -10048,7 +10039,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10069,13 +10060,6 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -11689,16 +11673,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 9cfbfc6df2501918c0669d859d496449ccfc74ff Mon Sep 17 00:00:00 2001 From: Christiaan Scheermeijer Date: Tue, 4 Jun 2024 11:52:01 +0200 Subject: [PATCH 2/5] fix: slow choose offers due to get switch subscription error --- .../common/src/controllers/CheckoutController.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/common/src/controllers/CheckoutController.ts b/packages/common/src/controllers/CheckoutController.ts index 917699af9..04f4f9e09 100644 --- a/packages/common/src/controllers/CheckoutController.ts +++ b/packages/common/src/controllers/CheckoutController.ts @@ -27,6 +27,7 @@ import { useCheckoutStore } from '../stores/CheckoutStore'; import { useAccountStore } from '../stores/AccountStore'; import { FormValidationError } from '../errors/FormValidationError'; import { determineSwitchDirection } from '../utils/subscription'; +import { logDev } from '../utils/common'; @injectable() export default class CheckoutController { @@ -52,10 +53,14 @@ export default class CheckoutController { useCheckoutStore.setState({ subscriptionOffers }); } - if (!useCheckoutStore.getState().switchSubscriptionOffers.length) { - const subscriptionSwitches = await this.getSubscriptionSwitches(); - const switchSubscriptionOffers = subscriptionSwitches ? await this.getOffers({ offerIds: subscriptionSwitches }) : []; - useCheckoutStore.setState({ switchSubscriptionOffers }); + try { + if (!useCheckoutStore.getState().switchSubscriptionOffers.length) { + const subscriptionSwitches = await this.getSubscriptionSwitches(); + const switchSubscriptionOffers = subscriptionSwitches ? await this.getOffers({ offerIds: subscriptionSwitches }) : []; + useCheckoutStore.setState({ switchSubscriptionOffers }); + } + } catch (error) { + logDev('Failed to get subscription switches', error); } }; From bdbfa1b1fa534985647849db5c095bdd36e7c102 Mon Sep 17 00:00:00 2001 From: Christiaan Scheermeijer Date: Wed, 5 Jun 2024 16:02:36 +0200 Subject: [PATCH 3/5] chore: pr feedback --- packages/hooks-react/src/useCheckAccess.ts | 6 +----- .../src/components/WaitingForPayment/WaitingForPayment.tsx | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/hooks-react/src/useCheckAccess.ts b/packages/hooks-react/src/useCheckAccess.ts index d559689cd..701e9330d 100644 --- a/packages/hooks-react/src/useCheckAccess.ts +++ b/packages/hooks-react/src/useCheckAccess.ts @@ -22,11 +22,7 @@ const useCheckAccess = () => { const offers = checkoutController.getSubscriptionOfferIds(); const intervalCheckAccess = useCallback( - ({ interval = 3000, iterations = 5, offerId, callback }: IntervalCheckAccessPayload) => { - if (!offerId) { - offerId = offers?.[0] || ''; - } - + ({ interval = 3000, iterations = 5, offerId = offers?.[0], callback }: IntervalCheckAccessPayload) => { if (!offerId) { callback?.({ hasAccess: false, offerId: '' }); return; diff --git a/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx b/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx index 4c7390c58..a11b4820a 100644 --- a/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx +++ b/packages/ui-react/src/components/WaitingForPayment/WaitingForPayment.tsx @@ -30,7 +30,7 @@ const WaitingForPayment = () => { // close the modal for PPV/TVOD offers if (offerId.startsWith('C') || offerId.startsWith('P')) { - // should we show a dedicated modal for TVOD access? + // @TODO should we show a dedicated modal for TVOD access? navigate(modalURLFromLocation(location, null)); } else { navigate(modalURLFromLocation(location, 'welcome')); From 75f40bb26da47ac6b9267c938db6184a83a19170 Mon Sep 17 00:00:00 2001 From: Christiaan Scheermeijer Date: Thu, 6 Jun 2024 12:01:02 +0200 Subject: [PATCH 4/5] chore: prevent expected error --- .../src/controllers/CheckoutController.ts | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/common/src/controllers/CheckoutController.ts b/packages/common/src/controllers/CheckoutController.ts index 04f4f9e09..3695e4af9 100644 --- a/packages/common/src/controllers/CheckoutController.ts +++ b/packages/common/src/controllers/CheckoutController.ts @@ -27,7 +27,6 @@ import { useCheckoutStore } from '../stores/CheckoutStore'; import { useAccountStore } from '../stores/AccountStore'; import { FormValidationError } from '../errors/FormValidationError'; import { determineSwitchDirection } from '../utils/subscription'; -import { logDev } from '../utils/common'; @injectable() export default class CheckoutController { @@ -53,14 +52,10 @@ export default class CheckoutController { useCheckoutStore.setState({ subscriptionOffers }); } - try { - if (!useCheckoutStore.getState().switchSubscriptionOffers.length) { - const subscriptionSwitches = await this.getSubscriptionSwitches(); - const switchSubscriptionOffers = subscriptionSwitches ? await this.getOffers({ offerIds: subscriptionSwitches }) : []; - useCheckoutStore.setState({ switchSubscriptionOffers }); - } - } catch (error) { - logDev('Failed to get subscription switches', error); + if (!useCheckoutStore.getState().switchSubscriptionOffers.length) { + const subscriptionSwitches = await this.getSubscriptionSwitches(); + const switchSubscriptionOffers = subscriptionSwitches ? await this.getOffers({ offerIds: subscriptionSwitches }) : []; + useCheckoutStore.setState({ switchSubscriptionOffers }); } }; @@ -247,13 +242,11 @@ export default class CheckoutController { const { getAccountInfo } = useAccountStore.getState(); const { customerId } = getAccountInfo(); - - assertModuleMethod(this.checkoutService.getSubscriptionSwitches, 'getSubscriptionSwitches is not available in checkout service'); - assertModuleMethod(this.checkoutService.getOffer, 'getOffer is not available in checkout service'); - const { subscription } = useAccountStore.getState(); - if (!subscription) return null; + if (!subscription || !this.checkoutService.getSubscriptionSwitches) return null; + + assertModuleMethod(this.checkoutService.getOffer, 'getOffer is not available in checkout service'); const response = await this.checkoutService.getSubscriptionSwitches({ customerId: customerId, From 4cfbde8a2bdd75fdc79fc306a2f9386f95680777 Mon Sep 17 00:00:00 2001 From: Christiaan Scheermeijer Date: Thu, 6 Jun 2024 12:01:23 +0200 Subject: [PATCH 5/5] test(e2e): update inplayer offer selector --- platforms/web/test-e2e/utils/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platforms/web/test-e2e/utils/constants.ts b/platforms/web/test-e2e/utils/constants.ts index fa0f4be5a..fbc18d5c9 100644 --- a/platforms/web/test-e2e/utils/constants.ts +++ b/platforms/web/test-e2e/utils/constants.ts @@ -81,7 +81,7 @@ export default { paymentFee: formatPrice(0, 'EUR', 'NL'), }, inplayer: { - label: `label[for="S38279"]`, + label: `label[for="S118699_38279"]`, price: formatPrice(6.99, 'EUR'), paymentFee: formatPrice(0, 'EUR'), }, @@ -93,7 +93,7 @@ export default { paymentFee: formatPrice(0, 'EUR', 'NL'), }, inplayer: { - label: `label[for="S38280"]`, + label: `label[for="S118699_38280"]`, price: formatPrice(50, 'EUR'), paymentFee: formatPrice(0, 'EUR'), },