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

feat(PAYMENTS-15245): add legal component #15

Merged
merged 1 commit into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
};
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