Skip to content

Commit

Permalink
feat(PAYMENTS-19009): add xsolla-number component
Browse files Browse the repository at this point in the history
  • Loading branch information
Aleksey-Kornienko-xsolla committed May 13, 2024
1 parent 6787c68 commit 86eb6b2
Show file tree
Hide file tree
Showing 34 changed files with 1,074 additions and 3 deletions.
4 changes: 3 additions & 1 deletion src/core/actions/next-action.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SpecialButtonAction } from './special-button.action.type';
import { ShowQrCodeAction } from './show-qr-code.action.type';
import { ShowMobilePaymentScreenAction } from './show-mobile-payment-screen.action.type';
import { HideFormAction } from './hide-form.action.type';
import { ShowCashPaymentAction } from './show-cash-payment.action.type';

export type NextAction =
| CheckStatusAction
Expand All @@ -19,4 +20,5 @@ export type NextAction =
| SpecialButtonAction
| ShowQrCodeAction
| ShowMobilePaymentScreenAction
| HideFormAction;
| HideFormAction
| ShowCashPaymentAction;
5 changes: 5 additions & 0 deletions src/core/actions/show-cash-payment.action.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Action } from './action.interface';

export type ShowCashPaymentActionType = 'show_cash_payment_instruction';

export type ShowCashPaymentAction = Action<ShowCashPaymentActionType, null>;
9 changes: 9 additions & 0 deletions src/core/cash-payment-data.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface CashPaymentData {
isCashPaymentMethod: boolean;
xsollaNumber: string;
pid: number;
publicId: string;
title: string;
projectName: string;
printUrl: string;
}
4 changes: 4 additions & 0 deletions src/core/event-name.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ export const enum EventName {
openApplePayPage = 'openApplePayPage',
submitApplePayForm = 'submitApplePayForm',
userCountryChanged = 'userCountryChanged',
getCashPaymentData = 'getCashPaymentData',
sendCashPaymentData = 'sendCashPaymentData',
sendCashPaymentButtonStatus = 'sendCashPaymentButtonStatus',
sendCashPaymentDataStatus = 'sendCashPaymentDataStatus',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Message } from '../../message.interface';
import { CashPaymentData } from '../../cash-payment-data.interface';
import { isEventMessage } from '../event-message.guard';
import { EventName } from '../../event-name.enum';

export const isGetCashPaymentDataEventMessage = (
messageData: unknown,
): messageData is Message<CashPaymentData | null | undefined> => {
if (isEventMessage(messageData)) {
return messageData.name === EventName.getCashPaymentData;
}
return false;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Message } from '../../message.interface';
import { isEventMessage } from '../event-message.guard';
import { EventName } from '../../event-name.enum';
import { SendCashPaymentDataStatus } from '../../send-cash-payment-data-status.interface';

export const isSentCashPaymentDataStatus = (
messageData: unknown,
): messageData is Message<SendCashPaymentDataStatus | null | undefined> => {
if (isEventMessage(messageData)) {
return messageData.name === EventName.sendCashPaymentDataStatus;
}
return false;
};
5 changes: 5 additions & 0 deletions src/core/send-cash-payment-data-status.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface SendCashPaymentDataStatus {
status: 'succeed' | 'failed';
type: 'sms' | 'email';
errors: string[];
}
2 changes: 2 additions & 0 deletions src/core/web-components/web-component-tag-name.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ export enum WebComponentTagName {
GooglePayButtonComponent = 'psdk-google-pay-button',
ApplePayComponent = 'psdk-apple-pay',
QrCodeComponent = 'psdk-qr-code',
CashPaymentInstructionComponent = 'psdk-cash-payment-instruction',
XsollaNumberComponent = 'psdk-xsolla-number',
}
5 changes: 5 additions & 0 deletions src/core/web-components/web-components.map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { QrCodeComponent } from '../../features/headless-checkout/web-components
import { ApplePayComponent } from '../../features/headless-checkout/web-components/apple-pay/apple-pay.component';
import { DefaultSubmitButtonComponent } from '../../features/headless-checkout/web-components/submit-button/default-submit-button/default-submit-button.component';
import { TotalComponent } from '../../features/headless-checkout/web-components/finance-details/total.component';
import { CashPaymentInstructionComponent } from '../../features/headless-checkout/web-components/cash-payment-instruction/cash-payment-instruction.component';
import { XsollaNumberComponent } from '../../features/headless-checkout/web-components/xsolla-number/xsolla-number.component';

export const webComponents: {
[key in WebComponentTagName]: CustomElementConstructor;
Expand All @@ -46,4 +48,7 @@ export const webComponents: {
[WebComponentTagName.ApplePayComponent]: ApplePayComponent,
[WebComponentTagName.DefaultSubmitButtonComponent]:
DefaultSubmitButtonComponent,
[WebComponentTagName.CashPaymentInstructionComponent]:
CashPaymentInstructionComponent,
[WebComponentTagName.XsollaNumberComponent]: XsollaNumberComponent,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { EventName } from '../../../../core/event-name.enum';
import { Message } from '../../../../core/message.interface';
import { getCashPaymentDataHandler } from './get-cash-payment-data.handler';

const mockMessage: Message = {
name: EventName.getCashPaymentData,
};
describe('getCashPaymentDataHandler', () => {
it('should handle status', () => {
expect(getCashPaymentDataHandler(mockMessage)).toEqual({
isHandled: true,
value: undefined,
});
});

it('should return null', () => {
expect(
getCashPaymentDataHandler({ name: EventName.getPaymentMethodsList }),
).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Handler } from '../../../../core/post-messages-client/handler.type';
import { Message } from '../../../../core/message.interface';
import { CashPaymentType } from '../../web-components/cash-payment-instruction/cash-payment.type';
import { isGetCashPaymentDataEventMessage } from '../../../../core/guards/get-cash-payment-data/get-cash-payment-data-event-message.guard';

export const getCashPaymentDataHandler: Handler<CashPaymentType | null> = (
message: Message,
): { isHandled: boolean; value: CashPaymentType | null | undefined } | null => {
if (!isGetCashPaymentDataEventMessage(message)) {
return null;
}

const cashPaymentData = message.data;
return {
isHandled: true,
value: cashPaymentData,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { EventName } from '../../../../core/event-name.enum';
import { Message } from '../../../../core/message.interface';
import { sendCashPaymentDataStatusHandler } from './send-cash-payment-data-status.handler';

const mockMessage: Message = {
name: EventName.sendCashPaymentDataStatus,
};
describe('sendCashPaymentDataStatusHandler', () => {
it('should handle status', () => {
expect(sendCashPaymentDataStatusHandler(mockMessage)).toEqual({
isHandled: true,
value: undefined,
});
});

it('should return null', () => {
expect(
sendCashPaymentDataStatusHandler({
name: EventName.getPaymentMethodsList,
}),
).toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Handler } from '../../../../core/post-messages-client/handler.type';
import { Message } from '../../../../core/message.interface';
import { isSentCashPaymentDataStatus } from '../../../../core/guards/send-cash-payment-data-status/send-cash-payment-data-event-message.guard';
import { SendCashPaymentDataStatus } from '../../../../core/send-cash-payment-data-status.interface';

export const sendCashPaymentDataStatusHandler: Handler<
SendCashPaymentDataStatus | null | undefined
> = (
message: Message,
): {
isHandled: boolean;
value: SendCashPaymentDataStatus | null | undefined;
} | null => {
if (!isSentCashPaymentDataStatus(message)) {
return null;
}

const cashPaymentData = message.data;
return {
isHandled: true,
value: cashPaymentData,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { container } from 'tsyringe';
import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum';
import { noopStub } from '../../../../tests/stubs/noop.stub';
import { FormSpy } from '../../../../core/spy/form-spy/form-spy';
import { PostMessagesClient } from '../../../../core/post-messages-client/post-messages-client';
import { CashPaymentData } from '../../../../core/cash-payment-data.interface';
import { CashPaymentInstructionComponent } from './cash-payment-instruction.component';
import { timeout } from '../../../../tests/stubs/timeout';

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

const cashPaymentData = {
isCashPaymentMethod: false,
} as CashPaymentData;

describe('CashPaymentInstructionComponent', () => {
let formSpy: FormSpy;
let postMessagesClient: PostMessagesClient;

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

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

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

formSpy = {
listenFormInit: noopStub,
get formWasInit() {
return;
},
} as unknown as FormSpy;

container.clearInstances();

container
.register<FormSpy>(FormSpy, {
useValue: formSpy,
})
.register<PostMessagesClient>(PostMessagesClient, {
useValue: postMessagesClient,
})
.register<Window>(Window, { useValue: window });
});

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

it('Should draw cash payment component', () => {
spyOnProperty(formSpy, 'formWasInit').and.returnValue(true);
spyOn(postMessagesClient, 'send').and.returnValue(Promise.resolve(null));
createComponent();
expect(
document.querySelector(
WebComponentTagName.CashPaymentInstructionComponent,
)!.innerHTML,
).not.toContain(WebComponentTagName.XsollaNumberComponent);
});

it('Should draw XsollaNumberComponent', async () => {
spyOnProperty(formSpy, 'formWasInit').and.returnValue(true);
spyOn(postMessagesClient, 'send').and.returnValue(
Promise.resolve(cashPaymentData),
);
createComponent();
const delay = 100;
await timeout(delay);
expect(
document.querySelector(
WebComponentTagName.CashPaymentInstructionComponent,
)!.innerHTML,
).toContain(WebComponentTagName.XsollaNumberComponent);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { container } from 'tsyringe';
import { FormSpy } from '../../../../core/spy/form-spy/form-spy';
import { EventName } from '../../../../core/event-name.enum';
import { PostMessagesClient } from '../../../../core/post-messages-client/post-messages-client';
import { Message } from '../../../../core/message.interface';
import { getCashPaymentDataHandler } from '../../post-messages-handlers/get-cash-payment-data/get-cash-payment-data.handler';
import { CashPaymentData } from '../../../../core/cash-payment-data.interface';
import { CashPaymentType } from './cash-payment.type';
import { WebComponentAbstract } from '../../../../core/web-components/web-component.abstract';
import { WebComponentTagName } from '../../../../core/web-components/web-component-tag-name.enum';

export class CashPaymentInstructionComponent extends WebComponentAbstract {
private readonly formSpy: FormSpy;
private readonly postMessagesClient: PostMessagesClient;
private cashPaymentData?: CashPaymentData | null;
public constructor() {
super();
this.formSpy = container.resolve(FormSpy);
this.postMessagesClient = container.resolve(PostMessagesClient);
}

protected connectedCallback(): void {
if (!this.formSpy.formWasInit) {
this.formSpy.listenFormInit(() => this.connectedCallback());
return;
}

void this.getCashPaymentData().then(this.configLoadedHandler);
}

protected getHtml(): string {
if (this.cashPaymentData?.isCashPaymentMethod) {
return '';
}

return `<${WebComponentTagName.XsollaNumberComponent}></${WebComponentTagName.XsollaNumberComponent}>`;
}

private async getCashPaymentData(): Promise<CashPaymentType> {
const msg: Message = {
name: EventName.getCashPaymentData,
};

return this.postMessagesClient.send<CashPaymentType>(
msg,
getCashPaymentDataHandler,
) as Promise<CashPaymentType>;
}

private readonly configLoadedHandler = (
cashPaymentData: CashPaymentType,
): void => {
this.cashPaymentData = cashPaymentData;
super.render();
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CashPaymentData } from '../../../../core/cash-payment-data.interface';

export type CashPaymentType = CashPaymentData | null;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface SendButtonStatus {
channelType: 'phone' | 'email';
isDisabled: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import i18next from 'i18next';

export const getInstructionTemplate = (
paymentMethodName: string,
projectName?: string,
): string => {
return `<div class='instruction-wrapper'>
<h3 class="title">${i18next.t('xsolla-number.instruction.how-to')}</h3>
<ul class="instruction">
<li class="paragraph">${i18next.t('xsolla-number.instruction.paragraph-one', {
paymentMethodName,
})}</li>
<li class="paragraph">
${i18next.t('xsolla-number.instruction.paragraph-two', { projectName })}
</li>
<li class="paragraph">
${i18next.t('xsolla-number.instruction.paragraph-three')}
</li>
<li class="paragraph">
${i18next.t('xsolla-number.instruction.paragraph-four')}
</li>
</ul>
<p class='notifier'>
${i18next.t('xsolla-number.instruction.notification')}
</p>
</div>`;
};
Loading

0 comments on commit 86eb6b2

Please sign in to comment.