From 489d8e8a6fb358df8dbd4522aed69228ded1bf6e Mon Sep 17 00:00:00 2001 From: Kire Mitrov Date: Wed, 14 Dec 2022 13:27:43 +0100 Subject: [PATCH] feat!: InPlayer register and update user flows (#202) * feat: inplayer update account details * chore: disable update email button if the inplayer integration is active * chore: refactor cleeng register, add inplayer register * chore: merge with develop and handle consents * fix: using readOnly to hide the edit button for the email instead of disabling edit button * feat: registration flow * chore: update json metadata for consents * fix: using fullName insted of first and last name for the users * fix: handle firstname and surname metadata parameters * fix: trim name and handle multiple whitespaces * fix: update InPlayer SDK version * fix: added missing ? * fix: update flow for update email feature * fix: extend type for update account * fix: account uninfied types * Update src/stores/AccountController.ts Co-authored-by: Danny Budzinski * Update src/stores/AccountController.ts Co-authored-by: Danny Budzinski * Update src/stores/AccountController.ts Co-authored-by: Danny Budzinski * fix: trim first and last names * chore: handling not supported email update * chore: refactor and type fixing * chore: refactor from conversation * fix: delete duplicates in cleeng type * chore: conversation updates * chore: i18next update * chore: reset and forgot password implmentation * fix: executed i18next * fix: improved error response format * fix: small register bug fix, update yarn.lock * fix: fix account store loading bug with consents * fix: remove void option from form section and fix missing return * chore: skip cancel subscription tests due to cleeng bug * chore: remove unnecessary async await Co-authored-by: Darko Co-authored-by: Danny Budzinski Co-authored-by: Danny Budzinski BREAKING CHANGE: introduce InPlayer services --- package.json | 2 +- src/components/Account/Account.test.tsx | 2 +- src/components/Account/Account.tsx | 17 +- .../EditPasswordForm.test.tsx | 9 +- .../EditPasswordForm/EditPasswordForm.tsx | 73 +++-- .../EditPasswordForm.test.tsx.snap | 54 +++- src/components/Form/FormSection.tsx | 9 +- .../PasswordField/PasswordField.module.scss | 90 ++++++ .../PasswordField/PasswordField.test.tsx | 31 ++ .../PasswordField/PasswordField.tsx | 56 ++++ .../__snapshots__/PasswordField.test.tsx.snap | 57 ++++ .../AccountModal/forms/EditPassword.tsx | 39 ++- .../AccountModal/forms/Registration.tsx | 7 +- .../AccountModal/forms/ResetPassword.tsx | 17 +- src/i18n/locales/en_US/account.json | 5 + src/i18n/locales/en_US/user.json | 1 + src/i18n/locales/nl_NL/account.json | 5 + src/i18n/locales/nl_NL/user.json | 1 + src/pages/User/User.tsx | 7 +- .../User/__snapshots__/User.test.tsx.snap | 11 +- src/services/cleeng.account.service.ts | 165 +++++++++-- src/services/inplayer.account.service.ts | 275 +++++++++++++++++- src/stores/AccountController.ts | 259 ++++++++++------- src/stores/AccountStore.ts | 4 + src/utils/collection.ts | 3 +- test-e2e/tests/payments/coupons_test.ts | 7 +- test-e2e/tests/payments/subscription_test.ts | 6 +- types/account.d.ts | 112 +++++-- types/cleeng.d.ts | 10 +- types/inplayer.d.ts | 16 + yarn.lock | 8 +- 31 files changed, 1114 insertions(+), 244 deletions(-) create mode 100644 src/components/PasswordField/PasswordField.module.scss create mode 100644 src/components/PasswordField/PasswordField.test.tsx create mode 100644 src/components/PasswordField/PasswordField.tsx create mode 100644 src/components/PasswordField/__snapshots__/PasswordField.test.tsx.snap diff --git a/package.json b/package.json index 0e53c4a69..5c7b661d7 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "deploy:github": "node ./scripts/deploy-github.js" }, "dependencies": { - "@inplayer-org/inplayer.js": "^3.13.1", + "@inplayer-org/inplayer.js": "^3.13.3", "classnames": "^2.3.1", "date-fns": "^2.28.0", "dompurify": "^2.3.8", diff --git a/src/components/Account/Account.test.tsx b/src/components/Account/Account.test.tsx index 452fb128e..3d6b0e363 100644 --- a/src/components/Account/Account.test.tsx +++ b/src/components/Account/Account.test.tsx @@ -14,7 +14,7 @@ describe('', () => { publisherConsents: Array.of({ name: 'marketing', label: 'Receive Marketing Emails' } as Consent), }); - const { container } = renderWithRouter(); + const { container } = renderWithRouter(); // todo expect(container).toMatchSnapshot(); diff --git a/src/components/Account/Account.tsx b/src/components/Account/Account.tsx index c050e9d26..f95c72647 100644 --- a/src/components/Account/Account.tsx +++ b/src/components/Account/Account.tsx @@ -23,6 +23,7 @@ import { updateConsents, updateUser } from '#src/stores/AccountController'; type Props = { panelClassName?: string; panelHeaderClassName?: string; + canUpdateEmail?: boolean; }; interface FormErrors { @@ -33,17 +34,18 @@ interface FormErrors { form?: string; } -const Account = ({ panelClassName, panelHeaderClassName }: Props): JSX.Element => { +const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true }: Props): JSX.Element => { const { t } = useTranslation('user'); const navigate = useNavigate(); const location = useLocation(); const [viewPassword, toggleViewPassword] = useToggle(); - const { customer, customerConsents, publisherConsents } = useAccountStore( - ({ user, customerConsents, publisherConsents }) => ({ + const { customer, customerConsents, publisherConsents, canChangePasswordWithOldPassword } = useAccountStore( + ({ user, customerConsents, publisherConsents, canChangePasswordWithOldPassword }) => ({ customer: user, customerConsents, publisherConsents, + canChangePasswordWithOldPassword, }), shallow, ); @@ -73,7 +75,6 @@ const Account = ({ panelClassName, panelHeaderClassName }: Props): JSX.Element = function translateErrors(errors?: string[]): FormErrors { const formErrors: FormErrors = {}; - // Some errors are combined in a single CSV string instead of one string per error errors ?.flatMap((e) => e.split(',')) @@ -100,6 +101,10 @@ const Account = ({ panelClassName, panelHeaderClassName }: Props): JSX.Element = formErrors.lastName = t('account.errors.last_name_too_long'); break; } + case 'Email update not supported': { + formErrors.form = t('account.errors.email_update_not_supported'); + break; + } default: { formErrors.form = t('account.errors.unknown_error'); logDev('Unknown error', error); @@ -134,7 +139,8 @@ const Account = ({ panelClassName, panelHeaderClassName }: Props): JSX.Element = } const editPasswordClickHandler = () => { - navigate(addQueryParam(location, 'u', 'reset-password')); + const modal = canChangePasswordWithOldPassword ? 'edit-password' : 'reset-password'; + navigate(addQueryParam(location, 'u', modal)); }; return ( @@ -149,6 +155,7 @@ const Account = ({ panelClassName, panelHeaderClassName }: Props): JSX.Element = }), canSave: (values) => !!(values.email && values.confirmationPassword), editButton: t('account.edit_account'), + readOnly: !canUpdateEmail, content: (section) => ( <> ', () => { test('renders and matches snapshot', () => { const { container } = render( - , + , ); expect(container).toMatchSnapshot(); diff --git a/src/components/EditPasswordForm/EditPasswordForm.tsx b/src/components/EditPasswordForm/EditPasswordForm.tsx index fbcbc3e06..93112b817 100644 --- a/src/components/EditPasswordForm/EditPasswordForm.tsx +++ b/src/components/EditPasswordForm/EditPasswordForm.tsx @@ -1,62 +1,87 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import PasswordField from '../PasswordField/PasswordField'; +import TextField from '../TextField/TextField'; + import styles from './EditPasswordForm.module.scss'; import type { FormErrors } from '#types/form'; import type { EditPasswordFormData } from '#types/account'; import FormFeedback from '#components/FormFeedback/FormFeedback'; -import TextField from '#components/TextField/TextField'; import Button from '#components/Button/Button'; -import IconButton from '#components/IconButton/IconButton'; -import Visibility from '#src/icons/Visibility'; -import VisibilityOff from '#src/icons/VisibilityOff'; -import useToggle from '#src/hooks/useToggle'; -import PasswordStrength from '#components/PasswordStrength/PasswordStrength'; import LoadingOverlay from '#components/LoadingOverlay/LoadingOverlay'; import { testId } from '#src/utils/common'; type Props = { onSubmit: React.FormEventHandler; onChange: React.ChangeEventHandler; + setValue?: (key: keyof EditPasswordFormData, value: string) => void; onBlur: React.FocusEventHandler; error?: string; errors: FormErrors; value: EditPasswordFormData; submitting: boolean; + showOldPasswordField?: boolean; + showResetTokenField?: boolean; }; -const EditPasswordForm: React.FC = ({ onSubmit, onChange, onBlur, value, errors, submitting }: Props) => { +const EditPasswordForm: React.FC = ({ onSubmit, onChange, onBlur, showOldPasswordField, showResetTokenField, value, errors, submitting }: Props) => { const { t } = useTranslation('account'); - const [viewPassword, toggleViewPassword] = useToggle(); - return (

{t('reset.password_reset')}

{errors.form ? {errors.form} : null} - + ) : showResetTokenField ? ( + + ) : null} + + - - {t('reset.password_helper_text')} - - } + error={!!errors.password} name="password" - type={viewPassword ? 'text' : 'password'} - rightControl={ - toggleViewPassword()}> - {viewPassword ? : } - - } required /> + + + - + />
true; @@ -34,12 +43,41 @@ export const login: Login = async ({ config, email, password }) => { }; const { responseData: auth, errors }: ServiceResponse = await post(!!config.integrations.cleeng?.useSandbox, '/auths', JSON.stringify(payload)); + handleErrors(errors); - if (errors.length > 0) throw new Error(errors[0]); + const { user, customerConsents } = await getUser({ config, auth }); return { auth, - user: await getUser({ config, auth }), + user, + customerConsents, + }; +}; + +export const register: Register = async ({ config, email, password }) => { + const localesResponse = await getLocales(!!config.integrations.cleeng?.useSandbox); + + handleErrors(localesResponse.errors); + + const payload: RegisterPayload = { + email, + password, + locale: localesResponse.responseData.locale, + country: localesResponse.responseData.country, + currency: localesResponse.responseData.currency, + publisherId: config.integrations.cleeng?.id || '', + customerIP: getOverrideIP(), + }; + + const { responseData: auth, errors }: ServiceResponse = await post(!!config.integrations.cleeng?.useSandbox, '/customers', JSON.stringify(payload)); + handleErrors(errors); + + const { user, customerConsents } = await getUser({ config, auth }); + + return { + auth, + user, + customerConsents, }; }; @@ -49,47 +87,116 @@ export async function getUser({ config, auth }: { config: Config; auth: AuthData const decodedToken: JwtDetails = jwtDecode(auth.jwt); const customerId = decodedToken.customerId; const { responseData: user, errors } = await getCustomer({ customerId }, !!config.integrations.cleeng?.useSandbox, auth.jwt); + handleErrors(errors); + + const consentsPayload = { + config, + jwt: auth.jwt, + customer: user, + }; - if (errors.length > 0) throw new Error(errors[0]); + const { consents } = await getCustomerConsents(consentsPayload); - return user; + return { + user, + customerConsents: consents, + }; } export const getFreshJwtToken = async ({ config, auth }: { config: Config; auth: AuthData }) => { - const result = await refreshToken({ refreshToken: auth.refreshToken }, !!config.integrations.cleeng?.useSandbox); + const response = await refreshToken({ refreshToken: auth.refreshToken }, !!config.integrations.cleeng?.useSandbox); + + handleErrors(response.errors); + + return response?.responseData; +}; + +export const getPublisherConsents: GetPublisherConsents = async (config) => { + const { cleeng } = config.integrations; + const response = await get(!!cleeng?.useSandbox, `/publishers/${cleeng?.id}/consents`); - if (result.errors.length) throw new Error(result.errors[0]); + handleErrors(response.errors); - return result?.responseData; + return { + consents: response?.responseData?.consents || [], + }; }; -export const register: Register = async (payload, sandbox) => { - payload.customerIP = getOverrideIP(); - return post(sandbox, '/customers', JSON.stringify(payload)); +export const getCustomerConsents: GetCustomerConsents = async (payload) => { + const { config, customer, jwt } = payload; + const { cleeng } = config.integrations; + + const response: ServiceResponse = await get(!!cleeng?.useSandbox, `/customers/${customer?.id}/consents`, jwt); + handleErrors(response.errors); + + return { + consents: response?.responseData?.consents || [], + }; }; -export const fetchPublisherConsents: GetPublisherConsents = async (payload, sandbox) => { - return get(sandbox, `/publishers/${payload.publisherId}/consents`); +export const updateCustomerConsents: UpdateCustomerConsents = async (payload) => { + const { config, customer, jwt } = payload; + const { cleeng } = config.integrations; + + const params: UpdateCustomerConsentsPayload = { + id: customer.id, + consents: payload.consents, + }; + + const response: ServiceResponse = await put(!!cleeng?.useSandbox, `/customers/${customer?.id}/consents`, JSON.stringify(params), jwt); + handleErrors(response.errors); + + return await getCustomerConsents(payload); }; -export const fetchCustomerConsents: GetCustomerConsents = async (payload, sandbox, jwt) => { - return get(sandbox, `/customers/${payload.customerId}/consents`, jwt); +export const getCaptureStatus: GetCaptureStatus = async ({ customer }, sandbox, jwt) => { + const response: ServiceResponse = await get(sandbox, `/customers/${customer?.id}/capture/status`, jwt); + + handleErrors(response.errors); + + return response; +}; + +export const updateCaptureAnswers: UpdateCaptureAnswers = async ({ customer, ...payload }, sandbox, jwt) => { + const params: UpdateCaptureAnswersPayload = { + customerId: customer.id, + ...payload, + }; + + const response: ServiceResponse = await put(sandbox, `/customers/${customer.id}/capture`, JSON.stringify(params), jwt); + handleErrors(response.errors); + + const { responseData, errors } = await getCustomer({ customerId: customer.id }, sandbox, jwt); + handleErrors(errors); + + return { + errors: [], + responseData, + }; }; export const resetPassword: ResetPassword = async (payload, sandbox) => { return put(sandbox, '/customers/passwords', JSON.stringify(payload)); }; -export const changePassword: ChangePassword = async (payload, sandbox) => { +export const changePasswordWithResetToken: ChangePassword = async (payload, sandbox) => { return patch(sandbox, '/customers/passwords', JSON.stringify(payload)); }; -export const updateCustomer: UpdateCustomer = async (payload, sandbox, jwt) => { - return patch(sandbox, `/customers/${payload.id}`, JSON.stringify(payload), jwt); +export const changePasswordWithOldPassword: ChangePasswordWithOldPassword = async () => { + return { + errors: [], + responseData: {}, + }; }; -export const updateCustomerConsents: UpdateCustomerConsents = async (payload, sandbox, jwt) => { - return put(sandbox, `/customers/${payload.id}/consents`, JSON.stringify(payload), jwt); +export const updateCustomer: UpdateCustomer = async (payload, sandbox, jwt) => { + const { id, metadata, fullName, ...rest } = payload; + const params: UpdateCustomerPayload = { + id, + ...rest, + }; + return patch(sandbox, `/customers/${id}`, JSON.stringify(params), jwt); }; export const getCustomer: GetCustomer = async (payload, sandbox, jwt) => { @@ -104,10 +211,12 @@ export const getLocales: GetLocales = async (sandbox) => { return get(sandbox, `/locales${getOverrideIP() ? '?customerIP=' + getOverrideIP() : ''}`); }; -export const getCaptureStatus: GetCaptureStatus = async ({ customerId }, sandbox, jwt) => { - return get(sandbox, `/customers/${customerId}/capture/status`, jwt); +const handleErrors = (errors: ApiResponse['errors']) => { + if (errors.length > 0) { + throw new Error(errors[0]); + } }; -export const updateCaptureAnswers: UpdateCaptureAnswers = async ({ customerId, ...payload }, sandbox, jwt) => { - return put(sandbox, `/customers/${customerId}/capture`, JSON.stringify(payload), jwt); -}; +export const canUpdateEmail = true; + +export const canChangePasswordWithOldPassword = false; diff --git a/src/services/inplayer.account.service.ts b/src/services/inplayer.account.service.ts index f7cf63d17..013306ca2 100644 --- a/src/services/inplayer.account.service.ts +++ b/src/services/inplayer.account.service.ts @@ -1,8 +1,28 @@ -import InPlayer, { AccountData, Env } from '@inplayer-org/inplayer.js'; +import InPlayer, { AccountData, Env, GetRegisterField, UpdateAccountData } from '@inplayer-org/inplayer.js'; -import type { AuthData, Customer, Login } from '#types/account'; +import type { + AuthData, + Capture, + ChangePassword, + ChangePasswordWithOldPassword, + Consent, + Customer, + CustomerConsent, + GetCaptureStatus, + GetCustomerConsents, + GetCustomerConsentsResponse, + GetPublisherConsents, + Login, + Register, + ResetPassword, + ServiceResponse, + UpdateCaptureAnswers, + UpdateCustomer, + UpdateCustomerArgs, + UpdateCustomerConsents, +} from '#types/account'; import type { Config } from '#types/Config'; -import type { InPlayerAuthData } from '#types/inplayer'; +import type { InPlayerAuthData, InPlayerError, InPlayerResponse } from '#types/inplayer'; enum InPlayerEnv { Development = 'development', @@ -24,15 +44,43 @@ export const login: Login = async ({ config, email, password }) => { referrer: window.location.href, }); + const user = processAccount(data.account); + return { - auth: processInPlayerAuth(data), - user: processInplayerAccount(data.account), + auth: processAuth(data), + user, + customerConsents: parseJson(user?.metadata?.consents as string, []), }; } catch { throw new Error('Failed to authenticate user.'); } }; +export const register: Register = async ({ config, email, password }) => { + try { + const { data } = await InPlayer.Account.signUpV2({ + email, + password, + passwordConfirmation: password, + fullName: email, + type: 'consumer', + clientId: config.integrations.inplayer?.clientId || '', + referrer: window.location.href, + }); + + const user = processAccount(data.account); + + return { + auth: processAuth(data), + user, + customerConsents: parseJson(user?.metadata?.consents as string, []), + }; + } catch (error: unknown) { + const { response } = error as InPlayerError; + throw new Error(response.data.message); + } +}; + export const logout = async () => { try { InPlayer.Account.signOut(); @@ -41,10 +89,15 @@ export const logout = async () => { } }; -export const getUser = async (): Promise => { +export const getUser = async () => { try { const { data } = await InPlayer.Account.getAccountInfo(); - return processInplayerAccount(data); + + const user = processAccount(data); + return { + user, + customerConsents: parseJson(user?.metadata?.consents as string, []) as CustomerConsent[], + }; } catch { throw new Error('Failed to fetch user data.'); } @@ -52,24 +105,189 @@ export const getUser = async (): Promise => { export const getFreshJwtToken = async ({ auth }: { auth: AuthData }) => auth; -// responsible to convert the InPlayer object to be compatible to the store -function processInplayerAccount(account: AccountData): Customer { +export const updateCustomer: UpdateCustomer = async (customer) => { + try { + const response: InPlayerResponse = await InPlayer.Account.updateAccount(processUpdateAccount(customer)); + + return { + errors: [], + responseData: processAccount(response.data), + }; + } catch { + throw new Error('Failed to update user data.'); + } +}; + +export const getPublisherConsents: GetPublisherConsents = async (config) => { + try { + const { inplayer } = config.integrations; + const { data } = await InPlayer.Account.getRegisterFields(inplayer?.clientId || ''); + + // @ts-ignore + // wrong data type from InPlayer SDK (will be updated in the SDK) + const result: Consent[] = data?.collection + .filter((field: GetRegisterField) => field.type === 'checkbox') + .map((consent: GetRegisterField) => processPublisherConsents(consent)); + + return { + consents: [getTermsConsent(), ...result], + }; + } catch { + throw new Error('Failed to fetch publisher consents.'); + } +}; + +export const getCustomerConsents: GetCustomerConsents = async (payload) => { + try { + if (!payload?.customer) { + return { + consents: [], + }; + } + + const { customer } = payload; + const consents: GetCustomerConsentsResponse = parseJson(customer.metadata?.consents as string, []); + + return consents; + } catch { + throw new Error('Unable to fetch Customer consents.'); + } +}; + +export const updateCustomerConsents: UpdateCustomerConsents = async (payload) => { + try { + const { customer, consents } = payload; + const params = { ...processUpdateAccount(customer), ...{ metadata: { consents: JSON.stringify(consents) } } }; + + const { data }: InPlayerResponse = await InPlayer.Account.updateAccount(params); + + return { + consents: parseJson(data?.metadata?.consents as string, []), + }; + } catch { + throw new Error('Unable to update Customer consents'); + } +}; + +export const getCaptureStatus: GetCaptureStatus = async ({ customer }) => { + return { + errors: [], + responseData: { + isCaptureEnabled: true, + shouldCaptureBeDisplayed: true, + settings: [ + { + answer: { + firstName: customer.firstName || null, + lastName: customer.lastName || null, + }, + enabled: true, + key: 'firstNameLastName', + required: true, + }, + ], + }, + }; +}; + +export const updateCaptureAnswers: UpdateCaptureAnswers = async ({ ...metadata }) => { + return (await updateCustomer(metadata, true, '')) as ServiceResponse; +}; + +export const changePasswordWithOldPassword: ChangePasswordWithOldPassword = async (payload) => { + const { oldPassword, newPassword, newPasswordConfirmation } = payload; + try { + await InPlayer.Account.changePassword({ + oldPassword, + password: newPassword, + passwordConfirmation: newPasswordConfirmation, + }); + return { + errors: [], + responseData: {}, + }; + } catch { + throw new Error('Failed to change password.'); + } +}; + +export const changePasswordWithResetToken: ChangePassword = async (payload) => { + const { resetPasswordToken = '', newPassword, newPasswordConfirmation = '' } = payload; + try { + await InPlayer.Account.setNewPassword( + { + password: newPassword, + passwordConfirmation: newPasswordConfirmation, + brandingId: 0, + }, + resetPasswordToken, + ); + return { + errors: [], + responseData: {}, + }; + } catch { + throw new Error('Failed to change password.'); + } +}; + +export const resetPassword: ResetPassword = async ({ customerEmail, publisherId }) => { + try { + await InPlayer.Account.requestNewPassword({ + email: customerEmail, + merchantUuid: publisherId || '', + brandingId: 0, + }); + return { + errors: [], + responseData: {}, + }; + } catch { + throw new Error('Failed to reset password.'); + } +}; + +function processAccount(account: AccountData): Customer { const { id, email, full_name: fullName, metadata, created_at: createdAt } = account; const regDate = new Date(createdAt * 1000).toLocaleString(); + let firstName = metadata?.first_name as string; + let lastName = metadata?.surname as string; + if (!firstName && !lastName) { + const nameParts = fullName.split(' '); + firstName = nameParts[0] || ''; + lastName = nameParts.slice(1)?.join(' '); + } return { id: id.toString(), email, fullName, - firstName: metadata?.first_name as string, - lastName: metadata?.last_name as string, + firstName, + lastName, + metadata, regDate, country: '', lastUserIp: '', }; } -function processInPlayerAuth(auth: InPlayerAuthData): AuthData { +function processUpdateAccount(customer: UpdateCustomerArgs) { + const firstName = customer.firstName?.trim() || ''; + const lastName = customer.lastName?.trim() || ''; + const fullName = `${firstName} ${lastName}`; + + const data: UpdateAccountData = { + fullName, + metadata: { + first_name: firstName, + surname: lastName, + }, + }; + + return data; +} + +function processAuth(auth: InPlayerAuthData): AuthData { const { access_token: jwt } = auth; return { jwt, @@ -77,3 +295,36 @@ function processInPlayerAuth(auth: InPlayerAuthData): AuthData { refreshToken: '', }; } + +function processPublisherConsents(consent: Partial) { + return { + broadcasterId: 0, + enabledByDefault: false, + label: consent.label, + name: consent.name, + required: consent.required, + value: '', + version: '1', + } as Consent; +} + +function getTermsConsent(): Consent { + const label = 'I accept the Terms and Conditions of InPlayer.'; + return processPublisherConsents({ + required: true, + name: 'terms', + label, + }); +} + +function parseJson(value: string, fallback = {}) { + try { + return JSON.parse(value); + } catch { + return fallback; + } +} + +export const canUpdateEmail = false; + +export const canChangePasswordWithOldPassword = true; diff --git a/src/stores/AccountController.ts b/src/stores/AccountController.ts index 0a56fa679..bffd112a6 100644 --- a/src/stores/AccountController.ts +++ b/src/stores/AccountController.ts @@ -6,7 +6,17 @@ import * as cleengAccountService from '#src/services/cleeng.account.service'; import * as inplayerAccountService from '#src/services/inplayer.account.service'; import { useFavoritesStore } from '#src/stores/FavoritesStore'; import { useWatchHistoryStore } from '#src/stores/WatchHistoryStore'; -import type { AuthData, Capture, Customer, CustomerConsent, JwtDetails } from '#types/account'; +import type { + AuthData, + Capture, + Customer, + CustomerConsent, + GetCaptureStatusResponse, + GetCustomerConsentsResponse, + GetPublisherConsentsResponse, + JwtDetails, + ServiceResponse, +} from '#types/account'; import { useConfigStore } from '#src/stores/ConfigStore'; import * as persist from '#src/utils/persist'; import { useAccountStore } from '#src/stores/AccountStore'; @@ -60,7 +70,11 @@ export const handleVisibilityChange = () => { export const initializeAccount = async () => { await withAccountService(async ({ accountService, config }) => { - useAccountStore.setState({ loading: true }); + useAccountStore.setState({ + loading: true, + canUpdateEmail: accountService.canUpdateEmail, + canChangePasswordWithOldPassword: accountService.canChangePasswordWithOldPassword, + }); accountService.setEnvironment(config); const storedSession: AuthData | null = persist.getItem(PERSIST_KEY_ACCOUNT) as AuthData | null; @@ -99,24 +113,34 @@ export const initializeAccount = async () => { }); }; -export async function updateUser(values: { firstName: string; lastName: string } | { email: string; confirmationPassword: string }) { - const { auth, user } = useAccountStore.getState(); +export async function updateUser( + values: { firstName: string; lastName: string } | { email: string; confirmationPassword: string }, +): Promise> { + return await withAccountService(async ({ accountService, sandbox }) => { + useAccountStore.setState({ loading: true }); - if (!auth || !user) throw new Error('no auth'); + const { auth, user, canUpdateEmail } = useAccountStore.getState(); - const { cleengSandbox } = useConfigStore.getState().getCleengData(); + if (Object.prototype.hasOwnProperty.call(values, 'email') && !canUpdateEmail) { + throw new Error('Email update not supported'); + } - const response = await cleengAccountService.updateCustomer({ ...values, id: user.id.toString() }, cleengSandbox, auth.jwt); + if (!auth || !user) { + throw new Error('no auth'); + } - if (!response) { - return { errors: Array.of('Unknown error') }; - } + const response = await accountService.updateCustomer({ ...values, id: user.id.toString() }, sandbox, auth.jwt); - if (response.errors?.length === 0) { - useAccountStore.setState({ user: response.responseData }); - } + if (!response) { + throw new Error('Unknown error'); + } - return response; + if (response.errors?.length === 0) { + useAccountStore.setState({ user: response.responseData }); + } + + return response; + }); } export const refreshJwtToken = async (auth: AuthData) => { @@ -137,7 +161,7 @@ export const getAccount = async (auth: AuthData) => { await withAccountService(async ({ accountService, config, accessModel }) => { const response = await accountService.getUser({ config, auth }); - await afterLogin(auth, response, accessModel); + await afterLogin(auth, response.user, response.customerConsents, accessModel); useAccountStore.setState({ loading: false }); }); @@ -149,7 +173,7 @@ export const login = async (email: string, password: string) => { const response = await accountService.login({ config, email, password }); - await afterLogin(response.auth, response.user, accessModel); + await afterLogin(response.auth, response.user, response.customerConsents, accessModel); await restoreFavorites(); await restoreWatchHistory(); @@ -176,33 +200,23 @@ export const logout = async () => { await restoreFavorites(); await restoreWatchHistory(); + + // it's needed for the InPlayer SDK await accountService.logout(); }); }; export const register = async (email: string, password: string) => { - await useConfig(async ({ cleengId, cleengSandbox }) => { - const localesResponse = await cleengAccountService.getLocales(cleengSandbox); - - if (localesResponse.errors.length > 0) throw new Error(localesResponse.errors[0]); - - const responseRegister = await cleengAccountService.register( - { - email: email, - password: password, - locale: localesResponse.responseData.locale, - country: localesResponse.responseData.country, - currency: localesResponse.responseData.currency, - publisherId: cleengId, - }, - cleengSandbox, - ); - - if (responseRegister.errors.length) throw new Error(responseRegister.errors[0]); + await withAccountService(async ({ accountService, accessModel, config }) => { + useAccountStore.setState({ loading: true }); + const { auth, user, customerConsents } = await accountService.register({ config, email, password }); - await getAccount(responseRegister.responseData); + await afterLogin(auth, user, customerConsents, accessModel); - await updatePersonalShelves(); + // @todo statement will be removed once the fav and history are done on InPlayer side + if (auth.refreshToken) { + await updatePersonalShelves(); + } }); }; @@ -229,79 +243,93 @@ export const updatePersonalShelves = async () => { }); }; -export const updateConsents = async (customerConsents: CustomerConsent[]) => { - return await useLoginContext(async ({ cleengSandbox, customerId, auth: { jwt } }) => { - const response = await cleengAccountService.updateCustomerConsents( - { - id: customerId, - consents: customerConsents, - }, - cleengSandbox, - jwt, - ); - - await getCustomerConsents(); +export const updateConsents = async (customerConsents: CustomerConsent[]): Promise> => { + return await useAccountContext(async ({ customer, auth: { jwt } }) => { + return await withAccountService(async ({ accountService, config }) => { + useAccountStore.setState({ loading: true }); + + try { + const response = await accountService.updateCustomerConsents({ + jwt, + config, + customer, + consents: customerConsents, + }); + + if (response?.consents) { + useAccountStore.setState({ customerConsents: response.consents }); + } - return response; + return { + responseData: response.consents, + errors: [], + }; + } finally { + useAccountStore.setState({ loading: false }); + } + }); }); }; -export async function getCustomerConsents() { - return await useLoginContext(async ({ cleengSandbox, customerId, auth: { jwt } }) => { - const response = await cleengAccountService.fetchCustomerConsents({ customerId }, cleengSandbox, jwt); +// TODO: Decide if it's worth keeping this or just leave combined with getUser +// noinspection JSUnusedGlobalSymbols +export async function getCustomerConsents(): Promise { + return await useAccountContext(async ({ customer, auth: { jwt } }) => { + return await withAccountService(async ({ accountService, config }) => { + const response = await accountService.getCustomerConsents({ config, customer, jwt }); - if (response && !response.errors?.length) { - useAccountStore.setState({ customerConsents: response.responseData.consents }); - } + if (response?.consents) { + useAccountStore.setState({ customerConsents: response.consents }); + } - return response; + return response; + }); }); } -export async function getPublisherConsents() { - return await useConfig(async ({ cleengId, cleengSandbox }) => { - const response = await cleengAccountService.fetchPublisherConsents({ publisherId: cleengId }, cleengSandbox); +export const getPublisherConsents = async (): Promise => { + return await withAccountService(async ({ accountService, config }) => { + const response = await accountService.getPublisherConsents(config); - if (response && !response.errors?.length) { - useAccountStore.setState({ publisherConsents: response.responseData.consents }); - } + useAccountStore.setState({ publisherConsents: response.consents }); return response; }); -} - -export const getCaptureStatus = async () => { - return await useLoginContext(async ({ cleengSandbox, customerId, auth: { jwt } }) => { - const response = await cleengAccountService.getCaptureStatus({ customerId }, cleengSandbox, jwt); +}; - if (response.errors.length > 0) throw new Error(response.errors[0]); +export const getCaptureStatus = async (): Promise => { + return await useAccountContext(async ({ customer, auth: { jwt } }) => { + return await withAccountService(async ({ accountService, sandbox }) => { + const { responseData } = await accountService.getCaptureStatus({ customer }, sandbox, jwt); - return response.responseData; + return responseData; + }); }); }; -export const updateCaptureAnswers = async (capture: Capture) => { - return await useLoginContext(async ({ cleengSandbox, customerId, auth }) => { - const response = await cleengAccountService.updateCaptureAnswers({ customerId, ...capture }, cleengSandbox, auth.jwt); +export const updateCaptureAnswers = async (capture: Capture): Promise => { + return await useAccountContext(async ({ customer, auth, customerConsents }) => { + return await withAccountService(async ({ accountService, accessModel, sandbox }) => { + const response = await accountService.updateCaptureAnswers({ customer, ...capture }, sandbox, auth.jwt); - if (response.errors.length > 0) throw new Error(response.errors[0]); + if (response.errors.length > 0) throw new Error(response.errors[0]); - // @todo why is this needed? - await getAccount(auth); + await afterLogin(auth, response.responseData as Customer, customerConsents, accessModel); - return response.responseData; + return response.responseData; + }); }); }; export const resetPassword = async (email: string, resetUrl: string) => { - return await useConfig(async ({ cleengId, cleengSandbox }) => { - const response = await cleengAccountService.resetPassword( + return await withAccountService(async ({ accountService, sandbox, authProviderId }) => { + const response = await accountService.resetPassword( { customerEmail: email, - publisherId: cleengId, + publisherId: authProviderId, resetUrl, }, - cleengSandbox, + sandbox, ); if (response.errors.length > 0) throw new Error(response.errors[0]); @@ -310,21 +338,24 @@ export const resetPassword = async (email: string, resetUrl: string) => { }); }; -export const changePassword = async (customerEmail: string, newPassword: string, resetPasswordToken: string) => { - return await useConfig(async ({ cleengId, cleengSandbox }) => { - const response = await cleengAccountService.changePassword( - { - publisherId: cleengId, - customerEmail, - newPassword, - resetPasswordToken, - }, - cleengSandbox, - ); +export const changePasswordWithOldPassword = async (oldPassword: string, newPassword: string, newPasswordConfirmation: string) => { + return await withAccountService(async ({ accountService, sandbox }) => { + const response = await accountService.changePasswordWithOldPassword({ oldPassword, newPassword, newPasswordConfirmation }, sandbox); + if (response?.errors?.length > 0) throw new Error(response.errors[0]); - if (response.errors.length > 0) throw new Error(response.errors[0]); + return response?.responseData; + }); +}; - return response.responseData; +export const changePasswordWithToken = async (customerEmail: string, newPassword: string, resetPasswordToken: string, newPasswordConfirmation: string) => { + return await withAccountService(async ({ accountService, sandbox, authProviderId }) => { + const response = await accountService.changePasswordWithResetToken( + { publisherId: authProviderId, customerEmail, newPassword, resetPasswordToken, newPasswordConfirmation }, + sandbox, + ); + if (response?.errors?.length > 0) throw new Error(response.errors[0]); + + return response?.responseData; }); }; @@ -397,17 +428,14 @@ export async function getMediaItems(watchlistId: string | undefined | null, medi return getMediaByWatchlist(watchlistId, mediaIds); } -async function getAccountExtras(accessModel: string) { - return await Promise.allSettled([accessModel === 'SVOD' ? reloadActiveSubscription() : Promise.resolve(), getCustomerConsents(), getPublisherConsents()]); -} - -async function afterLogin(auth: AuthData, response: Customer, accessModel: string) { +async function afterLogin(auth: AuthData, user: Customer, customerConsents: CustomerConsent[] | null, accessModel: string) { useAccountStore.setState({ - auth: auth, - user: response, + auth, + user, + customerConsents, }); - return await getAccountExtras(accessModel); + return await Promise.allSettled([accessModel === 'SVOD' ? reloadActiveSubscription() : Promise.resolve(), getPublisherConsents()]); } async function getActiveSubscription({ cleengSandbox, customerId, jwt }: { cleengSandbox: boolean; customerId: string; jwt: string }) { @@ -450,17 +478,38 @@ function useLoginContext(callback: (args: { cleengId: string; cleengSandbox: return useConfig((config) => callback({ ...config, customerId: user.id, auth })); } +function useAccountContext( + callback: (args: { customerId: string; customer: Customer; customerConsents: CustomerConsent[] | null; auth: AuthData }) => T, +): T { + const { user, auth, customerConsents } = useAccountStore.getState(); + + if (!user?.id || !auth?.jwt) throw new Error('user not logged in'); + + return callback({ customerId: user.id, customer: user, auth, customerConsents }); +} + function withAccountService( - callback: (args: { accountService: typeof inplayerAccountService | typeof cleengAccountService; config: Config; accessModel: AccessModel }) => T, + callback: (args: { + accountService: typeof inplayerAccountService | typeof cleengAccountService; + config: Config; + accessModel: AccessModel; + sandbox: boolean; + authProviderId: string; + }) => T, ): T { const { config, accessModel } = useConfigStore.getState(); - const { cleeng, inplayer } = config.integrations; if (inplayer?.clientId) { - return callback({ accountService: inplayerAccountService, config, accessModel }); + return callback({ + accountService: inplayerAccountService, + config, + accessModel, + sandbox: !!inplayer.useSandbox, + authProviderId: inplayer?.clientId?.toString(), + }); } else if (cleeng?.id) { - return callback({ accountService: cleengAccountService, config, accessModel }); + return callback({ accountService: cleengAccountService, config, accessModel, sandbox: !!cleeng.useSandbox, authProviderId: cleeng?.id }); } throw new Error('No account service available'); diff --git a/src/stores/AccountStore.ts b/src/stores/AccountStore.ts index 0f7f8f508..0f27f1d9f 100644 --- a/src/stores/AccountStore.ts +++ b/src/stores/AccountStore.ts @@ -11,6 +11,8 @@ type AccountStore = { activePayment: PaymentDetail | null; customerConsents: CustomerConsent[] | null; publisherConsents: Consent[] | null; + canUpdateEmail: boolean; + canChangePasswordWithOldPassword: boolean; setLoading: (loading: boolean) => void; }; @@ -23,5 +25,7 @@ export const useAccountStore = createStore('AccountStore', (set) = activePayment: null, customerConsents: null, publisherConsents: null, + canUpdateEmail: false, + canChangePasswordWithOldPassword: false, setLoading: (loading: boolean) => set({ loading }), })); diff --git a/src/utils/collection.ts b/src/utils/collection.ts index 34eda13ad..e165130c1 100644 --- a/src/utils/collection.ts +++ b/src/utils/collection.ts @@ -65,11 +65,10 @@ const generatePlaylistPlaceholder = (playlistLength: number = 15): Playlist => ( ), }); -const formatConsentValues = (publisherConsents: Consent[] | null, customerConsents: CustomerConsent[] | null) => { +const formatConsentValues = (publisherConsents: Consent[] | null = [], customerConsents: CustomerConsent[] | null = []) => { if (!publisherConsents || !customerConsents) { return {}; } - const values: Record = {}; publisherConsents?.forEach((publisherConsent) => { if (customerConsents?.find((customerConsent) => customerConsent.name === publisherConsent.name && customerConsent.state === 'accepted')) { diff --git a/test-e2e/tests/payments/coupons_test.ts b/test-e2e/tests/payments/coupons_test.ts index c1faaaa95..4d5f683fd 100644 --- a/test-e2e/tests/payments/coupons_test.ts +++ b/test-e2e/tests/payments/coupons_test.ts @@ -1,6 +1,7 @@ import { LoginContext } from '#utils/password_utils'; import { overrideIP, goToCheckout, formatPrice, finishAndCheckSubscription, addYear, cancelPlan, renewPlan } from '#utils/payments'; import { testConfigs } from '#test/constants'; +import skipped = CodeceptJS.output.test.skipped; let couponLoginContext: LoginContext; @@ -45,13 +46,15 @@ Scenario('I can redeem coupons', async ({ I }) => { await finishAndCheckSubscription(I, addYear(today), today); }); -Scenario('I can cancel a free subscription', async ({ I }) => { +// TODO: Re-enable this when the cleeng bug is fixed +Scenario.todo('I can cancel a free subscription', async ({ I }) => { couponLoginContext = await I.registerOrLogin(couponLoginContext); cancelPlan(I, addYear(today)); }); -Scenario('I can renew a free subscription', async ({ I }) => { +// TODO: Re-enable this when the cleeng bug is fixed +Scenario.todo('I can renew a free subscription', async ({ I }) => { couponLoginContext = await I.registerOrLogin(couponLoginContext); renewPlan(I, addYear(today)); diff --git a/test-e2e/tests/payments/subscription_test.ts b/test-e2e/tests/payments/subscription_test.ts index 7ced4d95c..46d5874c8 100644 --- a/test-e2e/tests/payments/subscription_test.ts +++ b/test-e2e/tests/payments/subscription_test.ts @@ -164,7 +164,8 @@ Scenario('I can finish my subscription', async ({ I }) => { I.seeAll(cardInfo); }); -Scenario('I can cancel my subscription', async ({ I }) => { +// TODO: Re-enable this when the cleeng bug is fixed +Scenario.todo('I can cancel my subscription', async ({ I }) => { paidLoginContext = await I.registerOrLogin(paidLoginContext); cancelPlan(I, addDays(today, 365)); @@ -173,7 +174,8 @@ Scenario('I can cancel my subscription', async ({ I }) => { I.seeAll(cardInfo); }); -Scenario('I can renew my subscription', async ({ I }) => { +// TODO: Re-enable this when the cleeng bug is fixed +Scenario.todo('I can renew my subscription', async ({ I }) => { paidLoginContext = await I.registerOrLogin(paidLoginContext); renewPlan(I, addDays(today, 365)); diff --git a/types/account.d.ts b/types/account.d.ts index 11e7ea225..aa31daddf 100644 --- a/types/account.d.ts +++ b/types/account.d.ts @@ -19,12 +19,22 @@ export type PayloadWithIPOverride = { customerIP?: string; }; -export type LoginArgs = { +export type RefreshTokenPayload = { + refreshToken: string; +}; + +export type AuthArgs = { config: Config; email: string; password: string; }; +export type AuthResponse = { + auth: AuthData; + user: Customer; + customerConsents: CustomerConsent[]; +}; + export type LoginPayload = PayloadWithIPOverride & { email: string; password: string; @@ -47,7 +57,11 @@ export type ForgotPasswordFormData = { }; export type EditPasswordFormData = { + email?: string; + oldPassword?: string; password: string; + passwordConfirmation: string; + resetPasswordToken?: string; }; export type OfferType = 'svod' | 'tvod'; @@ -70,6 +84,10 @@ export type RegisterPayload = PayloadWithIPOverride & { externalData?: string; }; +export type RegisterArgs = { + config: Config; + user: RegisterPayload; +}; export type CaptureFirstNameLastName = { firstName: string; lastName: string; @@ -135,6 +153,20 @@ export type ChangePasswordPayload = { newPassword: string; }; +export type ChangePasswordWithTokenPayload = { + customerEmail?: string; + publisherId?: string; + resetPasswordToken: string; + newPassword: string; + newPasswordConfirmation: string; +}; + +export type changePasswordWithOldPasswordPayload = { + oldPassword: string; + newPassword: string; + newPasswordConfirmation: string; +}; + export type GetCustomerPayload = { customerId: string; }; @@ -158,10 +190,6 @@ export type UpdateCustomerConsentsPayload = { consents: CustomerConsent[]; }; -export type RefreshTokenPayload = { - refreshToken: string; -}; - export type Customer = { id: string; email: string; @@ -170,12 +198,24 @@ export type Customer = { lastLoginDate?: string; lastUserIp: string; firstName?: string; + metadata?: Record; lastName?: string; fullName?: string; externalId?: string; externalData?: ExternalData; }; +export type UpdateCustomerArgs = { + id?: string | undefined; + email?: string | undefined; + confirmationPassword?: string | undefined; + firstName?: string | undefined; + lastName?: string | undefined; + externalData?: ExternalData | undefined; + metadata?: Record; + fullName?: string; +}; + export type Consent = { broadcasterId: number; name: string; @@ -198,6 +238,20 @@ export type CustomerConsent = { version: string; }; +export type CustomerConsentArgs = { + config: Config; + jwt: string; + customerId?: string; + customer?: Customer; +}; + +export type UpdateCustomerConsentsArgs = { + jwt: string; + config: Config; + customer: Customer; + consents: CustomerConsent[]; +}; + export type LocalesData = { country: string; currency: string; @@ -235,21 +289,41 @@ export type Capture = { customAnswers?: CaptureCustomAnswer[]; }; +export type GetCaptureStatusArgs = { + customer: Customer; +}; + +export type UpdateCaptureStatusArgs = { + customer: Customer; +} & Capture; + export type UpdateCaptureAnswersPayload = { customerId: string; } & Capture; -// TODO: Convert these all to generic non-cleeng calls -type Login = (args: LoginArgs) => Promise<{ auth: AuthData; user: Customer }>; -type Register = CleengRequest; -type GetPublisherConsents = CleengRequest; -type GetCustomerConsents = CleengAuthRequest; -type ResetPassword = CleengRequest>; -type ChangePassword = CleengRequest>; -type GetCustomer = CleengAuthRequest; -type UpdateCustomer = CleengAuthRequest; -type UpdateCustomerConsents = CleengAuthRequest; -type RefreshToken = CleengRequest; -type GetLocales = CleengEmptyRequest; -type GetCaptureStatus = CleengAuthRequest; -type UpdateCaptureAnswers = CleengAuthRequest; +interface ApiResponse { + errors: string[]; +} + +type ServiceResponse = { responseData: R } & ApiResponse; +type Request = (payload: P) => Promise; +type EmptyServiceRequest = (sandbox: boolean) => Promise>; +type ServiceRequest = (payload: P) => Promise>; +type EnvironmentServiceRequest = (payload: P, sandbox: boolean) => Promise>; +type AuthRequest = (payload: P, sandbox: boolean, jwt: string) => Promise; +type AuthServiceRequest = (payload: P, sandbox: boolean, jwt: string) => Promise>; + +type Login = Request; +type Register = Request; +type GetCustomer = AuthServiceRequest; +type UpdateCustomer = AuthServiceRequest; +type GetPublisherConsents = Request; +type GetCustomerConsents = Request; +type UpdateCustomerConsents = Request; +type GetCaptureStatus = AuthServiceRequest; +type UpdateCaptureAnswers = AuthServiceRequest; +type ResetPassword = EnvironmentServiceRequest>; +type ChangePassword = EnvironmentServiceRequest>; +type ChangePasswordWithOldPassword = EnvironmentServiceRequest>; +type RefreshToken = EnvironmentServiceRequest; +type GetLocales = EmptyServiceRequest; diff --git a/types/cleeng.d.ts b/types/cleeng.d.ts index 69aa45728..302d7822a 100644 --- a/types/cleeng.d.ts +++ b/types/cleeng.d.ts @@ -1,8 +1,8 @@ interface ApiResponse { errors: string[]; } -type ServiceResponse = { responseData: R } & ApiResponse; -type CleengEmptyRequest = (sandbox: boolean) => Promise>; -type CleengEmptyAuthRequest = (sandbox: boolean, jwt: string) => Promise>; -type CleengRequest = (payload: P, sandbox: boolean) => Promise>; -type CleengAuthRequest = (payload: P, sandbox: boolean, jwt: string) => Promise>; +type CleengResponse = { responseData: R } & ApiResponse; +type CleengEmptyRequest = (sandbox: boolean) => Promise>; +type CleengEmptyAuthRequest = (sandbox: boolean, jwt: string) => Promise>; +type CleengRequest = (payload: P, sandbox: boolean) => Promise>; +type CleengAuthRequest = (payload: P, sandbox: boolean, jwt: string) => Promise>; diff --git a/types/inplayer.d.ts b/types/inplayer.d.ts index 326ee1a6d..67d651e56 100644 --- a/types/inplayer.d.ts +++ b/types/inplayer.d.ts @@ -2,3 +2,19 @@ export type InPlayerAuthData = { access_token: string; expires?: number; }; + +export type InPlayerError = { + response: { + data: { + code: number; + message: string; + }; + }; +}; + +export type InPlayerResponse = { + data: Record; + status: number; + statusText: string; + config: AxiosRequestConfig; +}; diff --git a/yarn.lock b/yarn.lock index 9f31e8942..797eee130 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1557,10 +1557,10 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== -"@inplayer-org/inplayer.js@^3.13.1": - version "3.13.1" - resolved "https://registry.yarnpkg.com/@inplayer-org/inplayer.js/-/inplayer.js-3.13.1.tgz#aa7969b2b10e0692773b7064d048c177bc45ba2c" - integrity sha512-UUJkIRSlbqHetPHaXxogbsGXnBgQzTi3fkgVG9ZQQGiuPYPsR9C0MdpSyAg4xFlOsGqDKXBuJcVHPCiY5sfHTg== +"@inplayer-org/inplayer.js@^3.13.3": + version "3.13.3" + resolved "https://registry.yarnpkg.com/@inplayer-org/inplayer.js/-/inplayer.js-3.13.3.tgz#7f0dd352885f7c057fdf2c12f427e7ee5d89b297" + integrity sha512-a/Rtq3oVe1AgUnvOpxbTrNZITNNNmSBHfXkO8iUWQDeolPdYAxmuvWM2Iu00hqbvQfV4Vuc01cpDtAKvi+c6cw== dependencies: aws-iot-device-sdk "^2.2.6" axios "^0.19.2"