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

Refactor / move entitlement logic to entitlement controller #657

Merged
merged 8 commits into from
Dec 5, 2024
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
Loading