Skip to content

Commit

Permalink
feat(PAYMENTS-15245): add legal component
Browse files Browse the repository at this point in the history
  • Loading branch information
a.kornienko committed Jul 14, 2023
1 parent 5f8ead8 commit f044f22
Show file tree
Hide file tree
Showing 40 changed files with 976 additions and 131 deletions.
264 changes: 233 additions & 31 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"devDependencies": {
"@commitlint/cli": "^17.6.5",
"@commitlint/config-conventional": "^17.6.5",
"@types/jasmine": "^4.3.2",
"@types/i18next": "^13.0.0",
"@types/jasmine": "^4.3.2",
"@types/prettier": "^2.7.2",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
Expand All @@ -56,6 +56,7 @@
"stylelint-config-prettier-scss": "^1.0.0",
"stylelint-config-standard-scss": "^9.0.0",
"stylelint-order": "^6.0.3",
"svgo-loader": "^4.0.0",
"ts-loader": "^9.4.2",
"typescript": "^5.0.4",
"webpack": "^5.82.1",
Expand Down
8 changes: 8 additions & 0 deletions src/assets/icons/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/images.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '*.svg';
3 changes: 3 additions & 0 deletions src/core/event-name.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export const enum EventName {
getSavedMethods = 'getSavedMethods',
getUserBalance = 'getUserBalance',
submitButton = 'submitButton',
getLegalComponentConfig = 'getLegalComponentConfig',
legalComponentPing = 'legalComponentPing',
legalComponentPong = 'legalComponentPong',
}
16 changes: 16 additions & 0 deletions src/core/guards/legal-config-event-message.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Message } from '../../core/message.interface';
import { isEventMessage } from './event-message.guard';
import { EventName } from '../../core/event-name.enum';
import { LegalComponentConfig } from '../../features/headless-checkout/web-components/legal/legal-component.config.interface';

export const isLegalConfigEventMessage = (
messageData: unknown
): messageData is Message<{ config: LegalComponentConfig }> => {
if (isEventMessage(messageData)) {
return (
messageData.name === EventName.getLegalComponentConfig &&
(messageData.data as { [key: string]: unknown })?.config !== undefined
);
}
return false;
};
1 change: 1 addition & 0 deletions src/core/web-components/web-component-tag-name.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export enum WebComponentTagName {
CardNumberComponent = 'psdk-card-number',
SubmitButtonComponent = 'psdk-submit-button',
PaymentMethodsComponent = 'psdk-payment-methods',
LegalComponent = 'psdk-legal',
}
2 changes: 2 additions & 0 deletions src/core/web-components/web-components.map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { CardNumberComponent } from '../../features/headless-checkout/web-compon
import { SubmitButtonComponent } from '../../features/headless-checkout/web-components/submit-button/submit-button.component';
import { WebComponentTagName } from './web-component-tag-name.enum';
import { PaymentMethodsComponent } from '../../features/headless-checkout/web-components/payment-methods/payment-methods.component';
import { LegalComponent } from '../../features/headless-checkout/web-components/legal/legal.component';

export const webComponents: {
[key in WebComponentTagName]: CustomElementConstructor;
} = {
[WebComponentTagName.CardNumberComponent]: CardNumberComponent,
[WebComponentTagName.SubmitButtonComponent]: SubmitButtonComponent,
[WebComponentTagName.PaymentMethodsComponent]: PaymentMethodsComponent,
[WebComponentTagName.LegalComponent]: LegalComponent,
};
3 changes: 1 addition & 2 deletions src/features/headless-checkout/environment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export const headlessCheckoutAppUrl =
'https://secure.xsolla.com/headless-checkout';
export const headlessCheckoutAppUrl = 'http://localhost:4200';
export const cdnUrl = 'https://cdn3.xsolla.com';
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { EventName } from '../../../core/event-name.enum';
import { Message } from '../../../core/message.interface';
import { getLegalComponentConfigHandler } from './get-legal-component-config.handler';
import { LegalComponentConfig } from '../web-components/legal/legal-component.config.interface';

const mockMessage: Message<{ config: LegalComponentConfig }> = {
name: EventName.getLegalComponentConfig,
data: { config: {} as unknown as LegalComponentConfig },
};
describe('getLegalComponentConfigHandler', () => {
it('Should handle data', () => {
expect(getLegalComponentConfigHandler(mockMessage)).toEqual({
isHandled: true,
value: {} as unknown as LegalComponentConfig,
});
});
it('Should return null', () => {
expect(
getLegalComponentConfigHandler({ name: EventName.initPayment })
).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Handler } from '../../../core/post-messages-client/handler.type';
import { Message } from '../../../core/message.interface';
import { EventName } from '../../../core/event-name.enum';
import { LegalComponentConfig } from '../web-components/legal/legal-component.config.interface';
import { isLegalConfigEventMessage } from '../../../core/guards/legal-config-event-message.guard';

export const getLegalComponentConfigHandler: Handler<LegalComponentConfig> = (
message: Message
): { isHandled: boolean; value: LegalComponentConfig } | null => {
if (
isLegalConfigEventMessage(message) &&
message.name === EventName.getLegalComponentConfig
) {
const config = message.data?.config;
return {
isHandled: true,
value: config!,
};
}
return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface LegalComponentConfig {
isJapanUser: boolean;
refundPolicyUrl: string;
sctlPolicyUrl?: string;
secureConnection: {
secureConnectionUrl?: string;
isWhiteLabel?: boolean;
};
disclaimer?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { container } from 'tsyringe';
import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum';
import { HeadlessCheckoutSpy } from '../../../../core/headless-checkout-spy/headless-checkout-spy';
import { noopStub } from '../../../../tests/stubs/noop.stub';
import { HeadlessCheckout } from '../../headless-checkout';
import { LegalComponent } from './legal.component';
import { PostMessagesClient } from '../../../../core/post-messages-client/post-messages-client';
import { EventName } from '../../../../core/event-name.enum';
import { LegalComponentConfig } from './legal-component.config.interface';

function createComponent(): void {
const element = document.createElement(WebComponentTagName.LegalComponent);
element.setAttribute('id', 'test');
(document.getElementById('container')! as HTMLElement).appendChild(element);
}

const mockConfig: LegalComponentConfig = {
isJapanUser: false,
refundPolicyUrl: 'refundPolicyUrl',
secureConnection: {
secureConnectionUrl: 'secureConnectionUrl',
},
};

const delay = async (): Promise<boolean> =>
new Promise((resolve) => {
setTimeout(() => {
resolve(true);
});
});

describe('LegalComponent', () => {
let headlessCheckout: HeadlessCheckout;
let headlessCheckoutSpy: HeadlessCheckoutSpy;
let postMessagesClient: PostMessagesClient;
let windowService: Window;

window.customElements.define(
WebComponentTagName.LegalComponent,
LegalComponent
);

beforeEach(() => {
document.body.innerHTML = '<div id="container"></div>';

headlessCheckout = {
getRegularMethods: noopStub,
} as unknown as HeadlessCheckout;

headlessCheckoutSpy = {
listenAppInit: noopStub,
get appWasInit() {
return;
},
} as unknown as HeadlessCheckoutSpy;

postMessagesClient = {
send: noopStub,
} as unknown as PostMessagesClient;

windowService = window;

container
.register<HeadlessCheckoutSpy>(HeadlessCheckoutSpy, {
useValue: headlessCheckoutSpy,
})
.register<HeadlessCheckout>(HeadlessCheckout, {
useValue: headlessCheckout,
})
.register<PostMessagesClient>(PostMessagesClient, {
useValue: postMessagesClient,
})
.register<Window>(Window, { useValue: windowService });
});

afterEach(() => {
document.body.innerHTML = '';
});

it('Should create component', () => {
createComponent();
expect(
document.querySelector(WebComponentTagName.LegalComponent)
).toBeDefined();
});

it('Should load legal component config', () => {
const spy = spyOn(postMessagesClient, 'send').and.returnValue(
Promise.resolve({})
);
spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue(
true
);
createComponent();
expect(spy).toHaveBeenCalled();
});

it('Should load legal component config after init', () => {
const spy = spyOn(postMessagesClient, 'send').and.returnValue(
Promise.resolve({})
);
const appWasInitSpy = spyOnProperty(
headlessCheckoutSpy,
'appWasInit',
'get'
);
const listenAppInitSpy = spyOn(headlessCheckoutSpy, 'listenAppInit');
listenAppInitSpy.and.callFake((callback: () => void) => {
appWasInitSpy.and.returnValue(true);
callback();
});
appWasInitSpy.and.returnValue(false);
createComponent();
expect(spy).toHaveBeenCalled();
});

it('Should call addEventListener', async () => {
spyOn(postMessagesClient, 'send').and.returnValue(
Promise.resolve(mockConfig)
);
spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue(
true
);
const spy = spyOn(windowService, 'addEventListener');
createComponent();
await delay();
expect(spy).toHaveBeenCalled();
});

it('Should not call addEventListener', async () => {
spyOn(postMessagesClient, 'send').and.returnValue(Promise.resolve(null));
spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue(
true
);
const spy = spyOn(windowService, 'addEventListener');
createComponent();
await delay();
expect(spy).not.toHaveBeenCalled();
});

it('Should send pong message', async () => {
spyOn(postMessagesClient, 'send').and.returnValue(
Promise.resolve(mockConfig)
);
spyOnProperty(headlessCheckoutSpy, 'appWasInit', 'get').and.returnValue(
true
);
spyOn(windowService, 'addEventListener').and.callFake(
(name: string, callback: (event: unknown) => void) => {
const messageEvent = {
data: JSON.stringify({
name: EventName.legalComponentPing,
}),
origin: '',
source: {
postMessage: noopStub,
},
};
const spy = spyOn(messageEvent.source, 'postMessage');
callback(messageEvent);
expect(spy).toHaveBeenCalled();
}
);
createComponent();
await delay();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { LegalComponentConfig } from './legal-component.config.interface';
import { getSecureConnectionTemplate } from './secure-connection.component.template';
import i18next from 'i18next';

export const getLegalComponentTemplate = (
config: LegalComponentConfig
): string => {
const {
isJapanUser,
refundPolicyUrl,
sctlPolicyUrl,
secureConnection,
disclaimer,
} = config;
return `
${
disclaimer
? `
<div class="disclaimer">
${i18next.t('disclaimer')}
</div>`
: ''
}
${getSecureConnectionTemplate(secureConnection)}
<div class="legal-links">
<a
class="link link-legal"
href="https://xsolla.com/legal-agreements"
target="_blank"
>
${i18next.t('legal')}
</a>
<div class="divider"></div>
<a
class="link link-legal"
href="https://xsolla.com/cookie"
target="_blank"
>
${i18next.t('cookie-policy')}
</a>
<div class="divider"></div>
<a
class="link link-legal"
href="https://xsolla.com/privacypolicy"
target="_blank"
>
${i18next.t('privacy-policy')}
</a>
<div class="divider"></div>
<a
class="link link-refund"
href="${refundPolicyUrl}"
target="_blank"
>
${i18next.t('refund-policy')}
</a>
${
isJapanUser && sctlPolicyUrl
? `
<div class="divider"></div>
<a
class="link sctl-link"
href="${sctlPolicyUrl}"
target="_blank"
>
${i18next.t('sctl-indications')}
</a>`
: ''
}
</div>`;
};
Loading

0 comments on commit f044f22

Please sign in to comment.