Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle multiple plans from the AppConfig in the Web App #550

Open
wants to merge 49 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
efc334f
fix: add support for multiple plan offers and prices grouped by acces…
mirovladimitrovski Jun 11, 2024
f9246eb
fix: update snaps
mirovladimitrovski Jun 11, 2024
b3c3141
fix: replace any with unknown
mirovladimitrovski Jun 11, 2024
2ed2d7f
fix: adjust modal size
mirovladimitrovski Jun 12, 2024
ce9f1a9
fix(payment): waiting for payment not working for jwp ppv
ChristiaanScheermeijer Jun 7, 2024
5a8f0bd
Remove Profiles feature from WebApp (#543)
borkopetrovicc Jun 11, 2024
71c3977
Merge branch 'develop' into PS-231-multiple-plans
mirovladimitrovski Jun 12, 2024
6065e05
fix: remove duplicate code
mirovladimitrovski Jun 12, 2024
6b0bfe4
fix: restore formatOfferId method
mirovladimitrovski Jun 12, 2024
5379492
fix: fix failing check access
mirovladimitrovski Jun 12, 2024
abfa6ac
fix: update access checking with getJWPMediaToken
mirovladimitrovski Jun 12, 2024
a6164a2
Merge branch 'develop' into PS-226-multiple-plans
mirovladimitrovski Jun 17, 2024
e7ff32d
fix: change base path in jwpbaseservice
mirovladimitrovski Jun 17, 2024
f76c4fe
feat(project): multiple plans
mirovladimitrovski Jun 18, 2024
c9d626d
fix: update jwp implementation of getActiveSubscription
mirovladimitrovski Jun 19, 2024
99b12eb
fix: implement list-plans modal
mirovladimitrovski Jun 19, 2024
9387066
fix: update snaps
mirovladimitrovski Jun 19, 2024
91f6623
fix: update test file
mirovladimitrovski Jun 19, 2024
68c4d3b
fix: retrieve mediaid with useMatch
mirovladimitrovski Jun 20, 2024
2677514
fix: button in list-plans leads to /u/payments
mirovladimitrovski Jun 20, 2024
4e40d29
fix: remove free trial text from plan box
mirovladimitrovski Jun 20, 2024
f1c6d76
fix: incorrect logic for isPreEntitled
mirovladimitrovski Jun 20, 2024
2b0cd63
fix: subscriptions section
mirovladimitrovski Jun 20, 2024
6986e0e
fix: changed jwp access model to svod
mirovladimitrovski Jun 20, 2024
c87a532
fix: update sdk version
mirovladimitrovski Jun 20, 2024
f8765d1
fix: improvements in css
mirovladimitrovski Jun 20, 2024
d79452d
fix: updated a snapshot
mirovladimitrovski Jun 20, 2024
aa9bd60
fix: do not display change subscription button for plans
mirovladimitrovski Jun 24, 2024
2db2e07
fix: implement redirection to media
mirovladimitrovski Jun 24, 2024
bb64cb8
fix: invoke useoffers in startwatchingbutton
mirovladimitrovski Jun 24, 2024
c2cc3b3
Merge branch 'develop' into PS-226-multiple-plans
mirovladimitrovski Jun 24, 2024
12f70cb
Merge branch 'develop' into PS-226-multiple-plans
mirovladimitrovski Jun 24, 2024
9592547
fix: remove a redundant comment
mirovladimitrovski Jun 24, 2024
a8aa865
fix: add comment about 'any'
mirovladimitrovski Jun 24, 2024
b4fa729
fix: remove obsolete workaround
mirovladimitrovski Jun 24, 2024
ae8d4ec
fix: move invalidation into existing dedicated hook
mirovladimitrovski Jun 24, 2024
469fc8f
fix: remove invalidation from component
mirovladimitrovski Jun 24, 2024
14d8ee5
fix: only display plans that have prices
mirovladimitrovski Jun 24, 2024
8c31012
fix: eliminate direct usage of jw services
mirovladimitrovski Jun 24, 2024
8390893
fix: remove no longer used property
mirovladimitrovski Jun 24, 2024
b73ad23
fix: remove planOriginalId from Offer
mirovladimitrovski Jun 25, 2024
196bc1c
Merge branch 'develop' into PS-226-multiple-plans
mirovladimitrovski Jun 25, 2024
a6a749c
fix: split ListPlans into a container and a component
mirovladimitrovski Jun 25, 2024
41f9cd4
fix: fix failing unit tests for ChoosePlanForm
mirovladimitrovski Jun 25, 2024
9d251b1
fix: replace any with unknown
mirovladimitrovski Jun 25, 2024
265c732
fix: switch env from daily back to dev
mirovladimitrovski Jun 25, 2024
5800750
fix: safety guard
mirovladimitrovski Jun 25, 2024
1095505
fix: replace last safety guard with a fallback value
mirovladimitrovski Jun 25, 2024
ebe78be
fix: go back to daily env
mirovladimitrovski Jun 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"test-watch": "TZ=UTC LC_ALL=en_US.UTF-8 vitest"
},
"dependencies": {
"@inplayer-org/inplayer.js": "^3.13.24",
"@inplayer-org/inplayer.js": "^3.13.28",
"broadcast-channel": "^7.0.0",
"date-fns": "^2.28.0",
"fast-xml-parser": "^4.3.2",
Expand Down
2 changes: 2 additions & 0 deletions packages/common/src/controllers/AccountController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export default class AccountController {

await this.profileController?.loadPersistedProfile();
await this.accountService.initialize(config, url, this.logout);
await this.checkoutService.initialize(config);
await this.subscriptionService.initialize(config);

// set the accessModel before restoring the user session
useConfigStore.setState({ accessModel: this.accountService.accessModel });
Expand Down
11 changes: 10 additions & 1 deletion packages/common/src/controllers/CheckoutController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { inject, injectable } from 'inversify';
import i18next from 'i18next';

import type {
AccessMethod,
AddAdyenPaymentDetailsResponse,
AdyenPaymentSession,
CardPaymentData,
Expand Down Expand Up @@ -45,7 +46,7 @@ export default class CheckoutController {
initialiseOffers = async () => {
const requestedMediaOffers = useCheckoutStore.getState().requestedMediaOffers;
const mediaOffers = requestedMediaOffers ? await this.getOffers({ offerIds: requestedMediaOffers.map(({ offerId }) => offerId) }) : [];
useCheckoutStore.setState({ mediaOffers });
useCheckoutStore.setState({ mediaOffers, accessMethod: this.getAccessMethod() });

if (!useCheckoutStore.getState().subscriptionOffers.length && this.accountService.svodOfferIds) {
const subscriptionOffers = await this.getOffers({ offerIds: this.accountService.svodOfferIds });
Expand Down Expand Up @@ -352,7 +353,15 @@ export default class CheckoutController {
return this.checkoutService.getOffers(payload);
};

getPlansWithPriceOffers = (searchString: string) => {
return this.checkoutService.getPlansWithPriceOffers(searchString);
};

getEntitlements: GetEntitlements = (payload) => {
return this.checkoutService.getEntitlements(payload);
};

getAccessMethod = (): AccessMethod => {
return this.checkoutService.accessMethod;
};
}
4 changes: 2 additions & 2 deletions packages/common/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export type Env = {

const env: Env = {
APP_VERSION: '',
APP_API_BASE_URL: 'https://cdn.jwplayer.com',
APP_PLAYER_ID: 'M4qoGvUk',
APP_API_BASE_URL: 'https://content-portal.jwplatform.com',
APP_PLAYER_ID: 'ov7MiL14',
APP_FOOTER_TEXT: '',
APP_DEFAULT_LANGUAGE: 'en',
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
AccessMethod,
AddAdyenPaymentDetails,
CreateOrder,
DeletePaymentMethod,
Expand All @@ -10,6 +11,7 @@ import type {
GetInitialAdyenPayment,
GetOffer,
GetOffers,
GetPlansWithPriceOffers,
GetOrder,
GetPaymentMethods,
GetSubscriptionSwitch,
Expand All @@ -20,10 +22,17 @@ import type {
UpdateOrder,
UpdatePaymentWithPayPal,
} from '../../../types/checkout';
import type { Config } from '../../../types/config';

export default abstract class CheckoutService {
abstract initialize: (config: Config) => Promise<void>;

abstract accessMethod: AccessMethod;

abstract getOffers: GetOffers;

abstract getPlansWithPriceOffers: GetPlansWithPriceOffers;

abstract createOrder: CreateOrder;

abstract updateOrder: UpdateOrder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import type {
UpdateCardDetails,
UpdateSubscription,
} from '../../../types/subscription';
import type { Config } from '../../../types/config';

export default abstract class SubscriptionService {
abstract initialize: (config: Config) => Promise<void>;

abstract getActiveSubscription: GetActiveSubscription;

abstract getAllTransactions: GetAllTransactions;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { inject, injectable } from 'inversify';

import type {
AccessMethod,
AddAdyenPaymentDetails,
CreateOrder,
CreateOrderPayload,
Expand Down Expand Up @@ -35,12 +36,16 @@ export default class CleengCheckoutService extends CheckoutService {
private readonly cleengService: CleengService;
private readonly getCustomerIP: GetCustomerIP;

accessMethod: AccessMethod = 'offer';

constructor(cleengService: CleengService, @inject(GET_CUSTOMER_IP) getCustomerIP: GetCustomerIP) {
super();
this.cleengService = cleengService;
this.getCustomerIP = getCustomerIP;
}

initialize = async () => {};

getOffers: GetOffers = async (payload) => {
return await Promise.all(
payload.offerIds.map(async (offerId) => {
Expand All @@ -55,6 +60,8 @@ export default class CleengCheckoutService extends CheckoutService {
);
};

getPlansWithPriceOffers = async () => [];

getOffer: GetOffer = async (payload) => {
const customerIP = await this.getCustomerIP();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export default class CleengSubscriptionService extends SubscriptionService {
this.cleengService = cleengService;
}

initialize = async () => {};

getActiveSubscription: GetActiveSubscription = async ({ customerId }) => {
const response = await this.getSubscriptions({ customerId });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default class JWPAccountService extends AccountService {
private readonly storageService;
private clientId = '';

accessModel: AccessModel = ACCESS_MODEL.AUTHVOD;
accessModel: AccessModel = ACCESS_MODEL.SVOD;
assetId: number | null = null;
svodOfferIds: string[] = [];
sandbox = false;
Expand Down Expand Up @@ -135,18 +135,14 @@ export default class JWPAccountService extends AccountService {
// set environment
this.sandbox = !!jwpConfig.useSandbox;

const env: string = this.sandbox ? InPlayerEnv.Development : InPlayerEnv.Production;
const env: string = this.sandbox ? InPlayerEnv.Daily : InPlayerEnv.Production;
InPlayer.setConfig(env as Env);

// calculate access model
if (jwpConfig.clientId) {
this.clientId = jwpConfig.clientId;
}
this.clientId = jwpConfig.clientId;

if (jwpConfig.assetId) {
this.accessModel = ACCESS_MODEL.SVOD;
this.assetId = jwpConfig.assetId;
this.svodOfferIds = jwpConfig.assetId ? [String(jwpConfig.assetId)] : [];
if (jwpConfig.planId) {
this.svodOfferIds = jwpConfig.planId ? jwpConfig.planId.split(',') : [];
}

// restore session from URL params
Expand Down
128 changes: 102 additions & 26 deletions packages/common/src/services/integrations/jwp/JWPCheckoutService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import InPlayer, { type AccessFee, type MerchantPaymentMethod } from '@inplayer-org/inplayer.js';
import InPlayer, { type MerchantPaymentMethod } from '@inplayer-org/inplayer.js';
import { injectable } from 'inversify';

import type { PlanPrice } from '../../../../../../packages/common/types/jw';
import { isSVODOffer } from '../../../utils/offers';
import type {
AccessMethod,
CardPaymentData,
CreateOrder,
CreateOrderArgs,
Expand All @@ -19,13 +21,21 @@ import type {
PaymentWithPayPal,
UpdateOrder,
} from '../../../../types/checkout';
import type { Config } from '../../../../types/config';
import CheckoutService from '../CheckoutService';
import type { ServiceResponse } from '../../../../types/service';
import { isCommonError } from '../../../utils/api';

@injectable()
export default class JWPCheckoutService extends CheckoutService {
private readonly cardPaymentProvider = 'stripe';
siteId = '';

accessMethod: AccessMethod = 'plan';

initialize = async (config: Config) => {
this.siteId = config.siteId;
};

private formatPaymentMethod = (method: MerchantPaymentMethod, cardPaymentProvider: string): PaymentMethod => {
return {
Expand All @@ -50,10 +60,8 @@ export default class JWPCheckoutService extends CheckoutService {
* 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'];

return ppvOffers.includes(offer.access_type.name) ? `C${offer.item_id}_${offer.id}` : `S${offer.item_id}_${offer.id}`;
private formatOfferId(offer: PlanPrice & { planOriginalId: number }) {
return `${offer.access.type === 'subscription' ? 'S' : 'C'}${offer.planOriginalId}_${offer.id}`;
}

/**
Expand All @@ -74,18 +82,18 @@ export default class JWPCheckoutService extends CheckoutService {
return offerId;
}

private formatOffer = (offer: AccessFee): Offer => {
private formatOffer = ({ title, ...offer }: PlanPrice & { title: string; planOriginalId: number }): Offer => {
return {
id: offer.id,
id: offer.original_id,
offerId: this.formatOfferId(offer),
offerCurrency: offer.currency,
customerPriceInclTax: offer.amount,
customerCurrency: offer.currency,
offerTitle: offer.description,
offerCurrency: offer.metadata.currency,
customerPriceInclTax: offer.metadata.amount,
customerCurrency: offer.metadata.currency,
offerTitle: title,
active: true,
period: offer.access_type.period === 'month' && offer.access_type.quantity === 12 ? 'year' : offer.access_type.period,
freePeriods: offer.trial_period ? 1 : 0,
planSwitchEnabled: offer.item.plan_switch_enabled ?? false,
period: offer.access.period,
freePeriods: offer.metadata.trial?.quantity ?? 0,
planSwitchEnabled: false,
} as Offer;
};

Expand Down Expand Up @@ -120,20 +128,88 @@ export default class JWPCheckoutService extends CheckoutService {
};
};

getPlans = async (searchString: string) => {
const response = await InPlayer.Payment.getSitePlans(this.siteId, searchString);

const plans = (response.data.plans || []).filter((plan) => plan.metadata.access_model === 'svod');

return plans;
};

getPlanPrices = async (planId: string) => {
const response = await InPlayer.Payment.getSitePlanPrices(this.siteId, planId);

return response.data.prices || [];
};

getPlansWithPriceOffers = async (searchString: string) => {
try {
const plans = await this.getPlans(searchString);

const plansWithPrices = await Promise.all(
plans.map(async (plan) => {
try {
const prices = await this.getPlanPrices(plan.id);

const planProps = { id: plan.id, name: plan.metadata.name };

if (prices.length) {
const offers = prices.map((offer) => this.formatOffer({ ...offer, planOriginalId: plan.original_id, title: plan.metadata.name }));

return [planProps, offers] as const;
}

return [planProps, [] as Offer[]] as const;
} catch {
throw new Error();
}
}),
);

return plansWithPrices.filter(([, offers]) => offers.length > 0);
} catch {
throw new Error('Failed to get plans');
}
};

getAppPlansPriceOffers = async (searchString: string) => {
try {
const plans = await this.getPlans(searchString);

const offers = await Promise.all(
plans.map(async (plan) => {
try {
const prices = await this.getPlanPrices(plan.id);

if (prices.length) {
return prices.map((offer) => this.formatOffer({ ...offer, planOriginalId: plan.original_id, title: plan.metadata.name }));
}

return [];
} catch {
throw new Error();
}
}),
);

return offers;
} catch {
throw new Error('Failed to get plans');
}
};

getOffers: GetOffers = async (payload) => {
const offers = await Promise.all(
payload.offerIds.map(async (offerId) => {
try {
const { data } = await InPlayer.Asset.getAssetAccessFees(this.parseOfferId(offerId));

return data?.map((offer) => this.formatOffer(offer));
} catch {
throw new Error('Failed to get offers');
}
}),
);
if (!payload.offerIds.length) {
return [];
}

return offers.flat();
try {
const offers = await this.getAppPlansPriceOffers(`q=id:(${payload.offerIds.map((planId) => `"${planId}"`).join(' OR ')})`);

return offers.flat();
} catch {
throw new Error('Failed to get offers');
}
};

getPaymentMethods: GetPaymentMethods = async () => {
Expand Down
Loading