Skip to content

Commit

Permalink
feat: update custom entitlement configuration and apis (#657)
Browse files Browse the repository at this point in the history
* refactor(entitlement): move logic to entitlement controller

* chore: add missing params

* fix: geo error never showing

* fix: global site error when personal shelf schema is wrong

* fix: optional integration typings when disabled

* fix: translate protected media

* fix: handle all entitlement errors

* chore: update snapshots
  • Loading branch information
ChristiaanScheermeijer authored Dec 5, 2024
1 parent 00e4725 commit 7ee979e
Show file tree
Hide file tree
Showing 25 changed files with 605 additions and 235 deletions.
30 changes: 23 additions & 7 deletions packages/common/src/controllers/AccessController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const ACCESS_TOKENS = 'access_tokens';
export default class AccessController {
private readonly apiService: ApiService;
private readonly accessService: AccessService;
private readonly accountService: AccountService;
private readonly accountService?: AccountService;
private readonly storageService: StorageService;

private siteId: string = '';
Expand All @@ -33,7 +33,7 @@ export default class AccessController {
this.apiService = apiService;
this.accessService = accessService;
this.storageService = storageService;
this.accountService = getNamedModule(AccountService, integrationType);
this.accountService = getNamedModule(AccountService, integrationType, false);
}

initialize = async () => {
Expand All @@ -55,7 +55,7 @@ export default class AccessController {
* If no access tokens exist, it attempts to generate them, if the passport token is expired, it attempts to refresh them.
* If an access token retrieval fails or the user is not entitled to the content, an error is thrown.
*/
getMediaById = async (mediaId: string) => {
getMediaById = async (mediaId: string, language?: string) => {
const { entitledPlan } = useAccountStore.getState();

if (!this.siteId || !entitledPlan) {
Expand All @@ -67,13 +67,25 @@ export default class AccessController {
if (!accessTokens?.passport) {
throw new Error('Failed to get / generate access tokens and retrieve media.');
}
return await this.apiService.getMediaByIdWithPassport({ id: mediaId, siteId: this.siteId, planId: entitledPlan.id, passport: accessTokens.passport });
return await this.apiService.getMediaByIdWithPassport({
id: mediaId,
siteId: this.siteId,
planId: entitledPlan.id,
passport: accessTokens.passport,
language,
});
} catch (error: unknown) {
if (error instanceof ApiError && error.code === 403) {
// If the passport is invalid or expired, refresh the access tokens and try to get the media again.
const accessTokens = await this.refreshAccessTokens();
if (accessTokens?.passport) {
return await this.apiService.getMediaByIdWithPassport({ id: mediaId, siteId: this.siteId, planId: entitledPlan.id, passport: accessTokens.passport });
return await this.apiService.getMediaByIdWithPassport({
id: mediaId,
siteId: this.siteId,
planId: entitledPlan.id,
passport: accessTokens.passport,
language,
});
}

throw new Error('Failed to refresh access tokens and retrieve media.');
Expand Down Expand Up @@ -112,7 +124,7 @@ export default class AccessController {
return null;
}

const auth = await this.accountService.getAuthData();
const auth = await this.accountService?.getAuthData();

const accessTokens = await this.accessService.generateAccessTokens(this.siteId, auth?.jwt);
if (accessTokens) {
Expand Down Expand Up @@ -160,7 +172,11 @@ export default class AccessController {
* Retrieves the access tokens from local storage (if any) along with their expiration timestamp.
*/
getAccessTokens = async (): Promise<(AccessTokens & { expires: number }) | null> => {
const accessTokens = await this.storageService.getItem<AccessTokens & { expires: number }>(ACCESS_TOKENS, true, true);
const accessTokens = await this.storageService.getItem<
AccessTokens & {
expires: number;
}
>(ACCESS_TOKENS, true, true);
if (accessTokens) {
useAccessStore.setState({ passport: accessTokens.passport });
}
Expand Down
97 changes: 66 additions & 31 deletions packages/common/src/controllers/AccountController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { IntegrationType } from '../../types/config';
import CheckoutService from '../services/integrations/CheckoutService';
import AccountService, { type AccountServiceFeatures } from '../services/integrations/AccountService';
import SubscriptionService from '../services/integrations/SubscriptionService';
import JWPEntitlementService from '../services/JWPEntitlementService';
import JWPEntitlementService from '../services/entitlement/JWPEntitlementService';
import type { Offer } from '../../types/checkout';
import type { Plan } from '../../types/plans';
import type {
Expand All @@ -32,9 +32,9 @@ import AccessController from './AccessController';

@injectable()
export default class AccountController {
private readonly checkoutService: CheckoutService;
private readonly accountService: AccountService;
private readonly subscriptionService: SubscriptionService;
private readonly checkoutService?: CheckoutService;
private readonly accountService?: AccountService;
private readonly subscriptionService?: SubscriptionService;
private readonly entitlementService: JWPEntitlementService;
private readonly accessController: AccessController;
private readonly favoritesController: FavoritesController;
Expand All @@ -50,22 +50,22 @@ export default class AccountController {
favoritesController: FavoritesController,
watchHistoryController: WatchHistoryController,
) {
this.checkoutService = getNamedModule(CheckoutService, integrationType);
this.accountService = getNamedModule(AccountService, integrationType);
this.subscriptionService = getNamedModule(SubscriptionService, integrationType);
this.checkoutService = getNamedModule(CheckoutService, integrationType, false);
this.accountService = getNamedModule(AccountService, integrationType, false);
this.subscriptionService = getNamedModule(SubscriptionService, integrationType, false);
this.entitlementService = getModule(JWPEntitlementService);

// @TODO: Controllers shouldn't be depending on other controllers, but we've agreed to keep this as is for now
this.accessController = accessController;
this.favoritesController = favoritesController;
this.watchHistoryController = watchHistoryController;

this.features = integrationType ? this.accountService.features : DEFAULT_FEATURES;
this.features = integrationType && this.accountService ? this.accountService.features : DEFAULT_FEATURES;
}

loadUserData = async () => {
try {
const authData = await this.accountService.getAuthData();
const authData = await this.getAuthData();

if (authData) {
await this.getAccount();
Expand All @@ -85,24 +85,28 @@ export default class AccountController {
this.refreshEntitlements = refreshEntitlements;

useAccountStore.setState({ loading: true });
const config = useConfigStore.getState().config;

await this.accountService.initialize(config, url, this.logout);
if (this.accountService) {
const config = useConfigStore.getState().config;
await this.accountService.initialize(config, url, this.logout);

// set the accessModel before restoring the user session
useConfigStore.setState({ accessModel: this.accountService.accessModel });
// set the accessModel before restoring the user session
useConfigStore.setState({ accessModel: this.accountService.accessModel });
await this.loadUserData();
}

await this.loadUserData();
await this.getEntitledPlans();

useAccountStore.setState({ loading: false });
};

getSandbox() {
return this.accountService.sandbox;
return !!this.accountService?.sandbox;
}

updateUser = async (values: FirstLastNameInput | EmailConfirmPasswordInput): Promise<ServiceResponse<Customer>> => {
assertModuleMethod(this.accountService?.updateCustomer, 'AccountService#updateCustomer is not available');

useAccountStore.setState({ loading: true });

const { user } = useAccountStore.getState();
Expand Down Expand Up @@ -145,7 +149,7 @@ export default class AccountController {
const { config } = useConfigStore.getState();

try {
const response = await this.accountService.getUser({ config });
const response = await this.accountService?.getUser({ config });

if (response) {
await this.afterLogin(response.user, response.customerConsents);
Expand All @@ -166,6 +170,7 @@ export default class AccountController {
};

login = async (email: string, password: string, referrer: string) => {
assertModuleMethod(this.accountService?.login, 'AccountService#login is not available');
useAccountStore.setState({ loading: true });

try {
Expand Down Expand Up @@ -198,8 +203,16 @@ export default class AccountController {
};

register = async (email: string, password: string, referrer: string, consentsValues: CustomerConsent[], captchaValue?: string) => {
assertModuleMethod(this.accountService?.register, 'AccountService#register is not available');

try {
const response = await this.accountService.register({ email, password, consents: consentsValues, referrer, captchaValue });
const response = await this.accountService.register({
email,
password,
consents: consentsValues,
referrer,
captchaValue,
});

if (response) {
const { user, customerConsents } = response;
Expand Down Expand Up @@ -256,6 +269,8 @@ export default class AccountController {
// TODO: Decide if it's worth keeping this or just leave combined with getUser
// noinspection JSUnusedGlobalSymbols
getCustomerConsents = async () => {
assertModuleMethod(this.accountService?.getCustomerConsents, 'AccountService#getCustomerConsents is not available');

const { getAccountInfo } = useAccountStore.getState();
const { customer } = getAccountInfo();

Expand All @@ -269,6 +284,8 @@ export default class AccountController {
};

getPublisherConsents = async () => {
assertModuleMethod(this.accountService?.getPublisherConsents, 'AccountService#getPublisherConsents is not available');

const { config } = useConfigStore.getState();

useAccountStore.setState({ publisherConsentsLoading: true });
Expand All @@ -278,13 +295,17 @@ export default class AccountController {
};

getCaptureStatus = async (): Promise<GetCaptureStatusResponse> => {
assertModuleMethod(this.accountService?.getCaptureStatus, 'AccountService#getCaptureStatus is not available');

const { getAccountInfo } = useAccountStore.getState();
const { customer } = getAccountInfo();

return this.accountService.getCaptureStatus({ customer });
};

updateCaptureAnswers = async (capture: Capture): Promise<Capture> => {
assertModuleMethod(this.accountService?.updateCaptureAnswers, 'AccountService#updateCaptureAnswers is not available');

const { getAccountInfo } = useAccountStore.getState();
const { customer, customerConsents } = getAccountInfo();

Expand All @@ -299,13 +320,17 @@ export default class AccountController {
};

resetPassword = async (email: string, resetUrl: string) => {
assertModuleMethod(this.accountService?.resetPassword, 'AccountService#resetPassword is not available');

await this.accountService.resetPassword({
customerEmail: email,
resetUrl,
});
};

changePasswordWithOldPassword = async (oldPassword: string, newPassword: string, newPasswordConfirmation: string) => {
assertModuleMethod(this.accountService?.changePasswordWithOldPassword, 'AccountService#changePasswordWithOldPassword is not available');

await this.accountService.changePasswordWithOldPassword({
oldPassword,
newPassword,
Expand All @@ -314,6 +339,8 @@ export default class AccountController {
};

changePasswordWithToken = async (customerEmail: string, newPassword: string, resetPasswordToken: string, newPasswordConfirmation: string) => {
assertModuleMethod(this.accountService?.changePasswordWithResetToken, 'AccountService#changePasswordWithResetToken is not available');

await this.accountService.changePasswordWithResetToken({
customerEmail,
newPassword,
Expand All @@ -323,14 +350,15 @@ export default class AccountController {
};

updateSubscription = async (status: 'active' | 'cancelled'): Promise<unknown> => {
const { getAccountInfo } = useAccountStore.getState();
assertModuleMethod(this.subscriptionService?.updateSubscription, 'SubscriptionService#updateSubscription is not available');

const { getAccountInfo } = useAccountStore.getState();
const { customerId } = getAccountInfo();

const { subscription } = useAccountStore.getState();

if (!subscription) throw new Error('user has no active subscription');

const response = await this.subscriptionService?.updateSubscription({
const response = await this.subscriptionService.updateSubscription({
customerId,
offerId: subscription.offerId,
status,
Expand Down Expand Up @@ -359,12 +387,11 @@ export default class AccountController {
expYear: number;
currency: string;
}) => {
const { getAccountInfo } = useAccountStore.getState();
assertModuleMethod(this.subscriptionService?.updateCardDetails, 'SubscriptionService#updateCardDetails is not available');

const { getAccountInfo } = useAccountStore.getState();
const { customerId } = getAccountInfo();

assertModuleMethod(this.subscriptionService.updateCardDetails, 'updateCardDetails is not available in subscription service');

const response = await this.subscriptionService.updateCardDetails({
cardName,
cardNumber,
Expand All @@ -383,6 +410,8 @@ export default class AccountController {
};

checkEntitlements = async (offerId?: string): Promise<unknown> => {
assertModuleMethod(this.checkoutService?.getEntitlements, 'CheckoutService#getEntitlements is not available');

if (!offerId) {
return false;
}
Expand Down Expand Up @@ -420,6 +449,10 @@ export default class AccountController {
retry: 0,
},
): Promise<unknown> => {
assertModuleMethod(this.subscriptionService?.getActiveSubscription, 'SubscriptionService#getActiveSubscription is not available');
assertModuleMethod(this.subscriptionService?.getAllTransactions, 'SubscriptionService#getAllTransactions is not available');
assertModuleMethod(this.subscriptionService?.getActivePayment, 'SubscriptionService#getActivePayment is not available');

useAccountStore.setState({ loading: true });

const { getAccountInfo } = useAccountStore.getState();
Expand Down Expand Up @@ -460,8 +493,8 @@ export default class AccountController {
// resolve and fetch the pending offer after upgrade/downgrade
try {
if (activeSubscription?.pendingSwitchId) {
assertModuleMethod(this.checkoutService.getOffer, 'getOffer is not available in checkout service');
assertModuleMethod(this.checkoutService.getSubscriptionSwitch, 'getSubscriptionSwitch is not available in checkout service');
assertModuleMethod(this.checkoutService?.getOffer, 'getOffer is not available in checkout service');
assertModuleMethod(this.checkoutService?.getSubscriptionSwitch, 'getSubscriptionSwitch is not available in checkout service');

const switchOffer = await this.checkoutService.getSubscriptionSwitch({ switchId: activeSubscription.pendingSwitchId });
const offerResponse = await this.checkoutService.getOffer({ offerId: switchOffer.responseData.toOfferId });
Expand All @@ -487,16 +520,16 @@ export default class AccountController {
exportAccountData = async () => {
const { canExportAccountData } = this.getFeatures();

assertModuleMethod(this.accountService.exportAccountData, 'exportAccountData is not available in account service');
assertModuleMethod(this.accountService?.exportAccountData, 'exportAccountData is not available in account service');
assertFeature(canExportAccountData, 'Export account');

return this.accountService?.exportAccountData(undefined);
return this.accountService.exportAccountData(undefined);
};

getSocialLoginUrls = (redirectUrl: string) => {
const { hasSocialURLs } = this.getFeatures();

assertModuleMethod(this.accountService.getSocialUrls, 'getSocialUrls is not available in account service');
assertModuleMethod(this.accountService?.getSocialUrls, 'getSocialUrls is not available in account service');
assertFeature(hasSocialURLs, 'Social logins');

return this.accountService.getSocialUrls({ redirectUrl });
Expand All @@ -505,7 +538,7 @@ export default class AccountController {
deleteAccountData = async (password: string) => {
const { canDeleteAccount } = this.getFeatures();

assertModuleMethod(this.accountService.deleteAccount, 'deleteAccount is not available in account service');
assertModuleMethod(this.accountService?.deleteAccount, 'deleteAccount is not available in account service');
assertFeature(canDeleteAccount, 'Delete account');

try {
Expand All @@ -522,18 +555,20 @@ export default class AccountController {
};

getReceipt = async (transactionId: string) => {
assertModuleMethod(this.subscriptionService.fetchReceipt, 'fetchReceipt is not available in subscription service');
assertModuleMethod(this.subscriptionService?.fetchReceipt, 'fetchReceipt is not available in subscription service');

const { responseData } = await this.subscriptionService.fetchReceipt({ transactionId });

return responseData;
};

getAuthData = async () => {
return this.accountService.getAuthData();
return this.accountService?.getAuthData() || null;
};

subscribeToNotifications = async ({ uuid, onMessage }: SubscribeToNotificationsPayload) => {
assertModuleMethod(this.accountService?.subscribeToNotifications, 'AccountService#subscribeToNotifications is not available');

return this.accountService.subscribeToNotifications({ uuid, onMessage });
};

Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/controllers/AppController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class AppController {

let config = await this.configService.loadConfig(configLocation);

config.id = configSource;
config.id = configSource || '';
config.assets = config.assets || {};

// make sure the banner always defaults to the JWP banner when not defined in the config
Expand Down
Loading

0 comments on commit 7ee979e

Please sign in to comment.