Skip to content
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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -931,8 +931,9 @@ Integration flow:
- The `headlessCheckout.form.init` method returns the form object that can be used for future work with the payment form.
1. Subscribe to events of the `NextActions` form to receive notifications about the next payment flow steps:
- The `NextAction` with the `show_fields` type means that the form needs to render extra fields, e.g., for Brazilian credit cards. You must remove previously added fields and render new fields for this step.
- **3DS 1.0 (a deprecated method of payment authorization)**: The Next action with the `redirect` type means the form is redirected to complete payment according to the **3DS 1.0** secure procedure. See also [Special cases for 3DS 1.0](#special-cases-for-3ds-10).
- **3DS 2.0**: The `NextAction` with the `3DS` type means the user’s card must be verified according to the **3DS 2.0** procedure. When an additional user action (the challenge flow) is required, the `3DS` event is triggered along with the `data` object. Pass this object to the `ThreeDs` component as the `data-challenge` attribute (refer to the [example](./examples/credit-card)). The component opens a new browser tab where the user can authorize the payment.
- 3-D Secure verification can be handled either by the acquirer's built-in mechanism or through an external [MPI](https://en.wikipedia.org/wiki/Merchant_plug-in), which authenticates a cardholder and forwards the result to the acquirer.
- **3DS via external MPI**: The Next action with the `redirect` type means the form is redirected to complete payment according to the 3DS procedure. See also [Special cases for 3DS via external MPI](#special-cases-for-3ds-via-external-mpi).
- **3DS via built-in method**: The `NextAction` with the `3DS` type means the user's card must be verified according to the procedure triggered by acquirer. When an additional user action (the challenge flow) is required, the `3DS` event is triggered along with the `data` object. Pass this object to the `ThreeDs` component as the `data-challenge` attribute (refer to the [example](./examples/credit-card)). The component opens a new browser tab where the user can authorize the payment.
1. Add the form fields component to the HTML markup.
- Use the form object that was returned by `headlessCheckout.form.init` method to get form fields.
- Use fields with the `isMandatory` flag to get required fields only.
Expand All @@ -943,9 +944,9 @@ Integration flow:
1. Add the `<psdk-finance-details>`, `<psdk-status>` and `<psdk-legal>` components to the created `return` page to show a payment status.
1. Set accessToken at `headlessCheckout.setToken`. Run `headlessCheckout.init` to initialize the headless checkout library.

### Special cases for 3DS 1.0
### Special cases for 3DS via external MPI

If you embed the payment interface in an iframe, be aware that some ACS providers implement the 3DS 1.0 challenge flow by performing an additional redirect using:
If you embed the payment interface in an iframe, be aware that some ACS providers implement the 3DS challenge flow by performing an additional redirect using:

```js
window.top.location.href = 'redirect_url';
Expand Down
25 changes: 25 additions & 0 deletions src/core/environment/environment.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { singleton } from 'tsyringe';
import {
headlessCheckoutAppUrl,
headlessCheckoutSandboxAppUrl,
} from '../../features/headless-checkout/environment';

@singleton()
export class EnvironmentService {
private _isSandbox = false;

public set isSandbox(value: boolean) {
this._isSandbox = value;
}

public get isSandbox(): boolean {
return this._isSandbox;
}

public getHeadlessCheckoutAppUrl(): string {
return this._isSandbox
? headlessCheckoutSandboxAppUrl
: headlessCheckoutAppUrl;
}
}

Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { container } from 'tsyringe';
import { WebComponentAbstract } from '../web-component.abstract';
import { headlessCheckoutAppUrl } from '../../../features/headless-checkout/environment';
import { EnvironmentService } from '../../environment/environment.service';

export abstract class SecureComponentAbstract extends WebComponentAbstract {
protected componentName: string | null = null;
protected environmentService: EnvironmentService;

protected constructor() {
super();
this.environmentService = container.resolve(EnvironmentService);
}

protected getSecureHtml(): string {
if (!this.componentName) {
throw new Error('Component name is required');
}

return `<iframe src='${headlessCheckoutAppUrl}/secure-components/${this.componentName}'></iframe>`;
const appUrl = this.environmentService.getHeadlessCheckoutAppUrl();
return `<iframe src='${appUrl}/secure-components/${this.componentName}'></iframe>`;
}
}
3 changes: 3 additions & 0 deletions src/features/headless-checkout/environment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export const headlessCheckoutAppUrl =
'https://secure.xsolla.com/headless-checkout';

export const headlessCheckoutSandboxAppUrl =
'https://sandbox-secure.xsolla.com/headless-checkout';

export const cdnIconsUrl =
'https://cdn.xsolla.net/headless-checkout-prod/assets/icons';
10 changes: 10 additions & 0 deletions src/features/headless-checkout/headless-checkout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FormStatus } from '../../core/status/form-status.enum';
import { noopStub } from '../../tests/stubs/noop.stub';
import { headlessCheckoutAppUrl } from './environment';
import { ThemesLoader } from '../../core/customization/themes-loader';
import { EnvironmentService } from '../../core/environment/environment.service';
import { submitHandler } from './post-messages-handlers/submit/submit.handler';

const mockMessage: Message = {
Expand Down Expand Up @@ -43,6 +44,7 @@ describe('HeadlessCheckout', () => {
let postMessagesClient: PostMessagesClient;
let localizeService: LocalizeService;
let themesLoader: ThemesLoader;
let environmentService: EnvironmentService;

// eslint-disable-next-line @typescript-eslint/no-empty-function
const stub = (): void => {};
Expand Down Expand Up @@ -71,6 +73,11 @@ describe('HeadlessCheckout', () => {
getTheme: () => 'body { margin: 0;}',
} as unknown as ThemesLoader;

environmentService = {
isSandbox: false,
getHeadlessCheckoutAppUrl: () => headlessCheckoutAppUrl,
} as unknown as EnvironmentService;

container.clearInstances();

headlessCheckout = container
Expand All @@ -85,6 +92,9 @@ describe('HeadlessCheckout', () => {
.register<ThemesLoader>(ThemesLoader, {
useValue: themesLoader,
})
.register<EnvironmentService>(EnvironmentService, {
useValue: environmentService,
})
.registerSingleton(HeadlessCheckout)
.resolve(HeadlessCheckout);
});
Expand Down
21 changes: 13 additions & 8 deletions src/features/headless-checkout/headless-checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { getUserBalanceHandler } from './post-messages-handlers/get-user-balance
import { nextActionHandler } from './post-messages-handlers/next-action.handler';
import { Field } from '../../core/form/field.interface';
import { getPaymentStatusHandler } from './post-messages-handlers/get-payment-status/get-payment-status.handler';
import { headlessCheckoutAppUrl } from './environment';
import { EnvironmentService } from '../../core/environment/environment.service';
import { FinanceDetails } from '../../core/finance-details/finance-details.interface';
import { getFinanceDetailsHandler } from './post-messages-handlers/get-finance-details.handler';
import { FormStatus } from '../../core/status/form-status.enum';
Expand Down Expand Up @@ -176,14 +176,12 @@ export class HeadlessCheckout {

private formStatus: FormStatus = FormStatus.undefined;
private isWebView?: boolean;
private isSandbox?: boolean;
private theme?: string;
private topLevelDomain?: string;
private isApplePayInstantFlowEnabled = false;
private locale = Lang.EN;
private coreIframe!: HTMLIFrameElement;
private errorsSubscription?: () => void;
private readonly headlessAppUrl = headlessCheckoutAppUrl;
private _formConfiguration?: FormConfiguration;

public constructor(
Expand All @@ -194,11 +192,12 @@ export class HeadlessCheckout {
private readonly formSpy: FormSpy,
private readonly themesLoader: ThemesLoader,
private readonly formLoader: FormLoader,
private readonly environmentService: EnvironmentService,
) {}

public async init(environment: InitialOptions): Promise<void> {
this.isWebView = environment.isWebview;
this.isSandbox = environment.sandbox;
this.environmentService.isSandbox = !!environment.sandbox;
this.theme = environment.theme;
this.topLevelDomain = environment.topLevelDomain;
this.isApplePayInstantFlowEnabled =
Expand All @@ -207,11 +206,17 @@ export class HeadlessCheckout {

await this.localizeService.initDictionaries(environment.language);

this.postMessagesClient.init(this.coreIframe, this.headlessAppUrl);
this.postMessagesClient.init(
this.coreIframe,
this.environmentService.getHeadlessCheckoutAppUrl(),
);
await this.setupCoreIframe();
this.defineComponents();

this.postMessagesClient.init(this.coreIframe, this.headlessAppUrl);
this.postMessagesClient.init(
this.coreIframe,
this.environmentService.getHeadlessCheckoutAppUrl(),
);
void this.setupSecureStyles(this.theme);
this.errorsSubscription = this.postMessagesClient.listen<string>(
EventName.error,
Expand Down Expand Up @@ -245,7 +250,7 @@ export class HeadlessCheckout {
configuration: {
token,
isWebView: this.isWebView,
sandbox: this.isSandbox,
sandbox: this.environmentService.isSandbox,
topLevelDomain: this.topLevelDomain,
isApplePayInstantFlowEnabled: this.isApplePayInstantFlowEnabled,
locale: this.locale,
Expand Down Expand Up @@ -443,7 +448,7 @@ export class HeadlessCheckout {
this.coreIframe.height = '0px';
this.coreIframe.style.border = 'none';
this.coreIframe.style.position = 'absolute';
this.coreIframe.src = `${this.headlessAppUrl}/core`;
this.coreIframe.src = `${this.environmentService.getHeadlessCheckoutAppUrl()}/core`;
this.coreIframe.name = 'core';
this.window.document.body.appendChild(this.coreIframe);
return this.listenCoreIframeLoading();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { applePayErrors } from './apple-pay.errors.const';
import i18next from 'i18next';
import { FormSpy } from '../../../../core/spy/form-spy/form-spy';
import { finishLoadComponentHandler } from '../../post-messages-handlers/finish-load-component.handler';
import { headlessCheckoutAppUrl } from '../../environment';
import { openApplePayPageHandler } from '../../post-messages-handlers/apple-pay/open-apple-pay-page.handler';
import { getWaitingProcessingTemplate } from './waiting-processing.template';
import { waitingProcessingClassname } from './waiting-processing-classname.const';
Expand Down Expand Up @@ -160,7 +159,8 @@ export class ApplePayComponent extends SecureComponentAbstract {
}

protected getSecureHtml(): string {
return `<iframe id="apple-pay-iframe" src='${headlessCheckoutAppUrl}/secure-components/${this
const appUrl = this.environmentService.getHeadlessCheckoutAppUrl();
return `<iframe id="apple-pay-iframe" src='${appUrl}/secure-components/${this
.componentName!}' allow='payment'></iframe>`;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { SecureComponentAbstract } from '../../../../../core/web-components/secure-component/secure-component.abstract';
import { headlessCheckoutAppUrl } from '../../../environment';
import { GooglePayButtonColorType } from './google-pay-button-color.type';
import { EventName } from '../../../../../core/event-name.enum';
import { container } from 'tsyringe';
Expand Down Expand Up @@ -93,6 +92,7 @@ export class GooglePayButtonComponent extends SecureComponentAbstract {
}

protected getSecureHtml(): string {
return `<iframe allow='payment' src='${headlessCheckoutAppUrl}/secure-components/${this.componentName}?buttonColor=${this.buttonColor}'></iframe>`;
const appUrl = this.environmentService.getHeadlessCheckoutAppUrl();
return `<iframe allow='payment' src='${appUrl}/secure-components/${this.componentName}?buttonColor=${this.buttonColor}'></iframe>`;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { PhoneComponentAttributes } from './phone-component-attributes.enum';
import { headlessCheckoutAppUrl } from '../../environment';
import { TextComponent } from '../text-component/text.component';

export class PhoneComponent extends TextComponent {
Expand All @@ -19,7 +18,8 @@ export class PhoneComponent extends TextComponent {
throw new Error('Component name is required');
}

let src = `${headlessCheckoutAppUrl}/secure-components/${this.componentName}`;
const appUrl = this.environmentService.getHeadlessCheckoutAppUrl();
let src = `${appUrl}/secure-components/${this.componentName}`;
const showFlags = this.getAttribute(PhoneComponentAttributes.showFlags);

if (showFlags) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { isEventMessage } from '../../../../core/guards/event-message.guard';
import { CashPaymentData } from '../../../../core/cash-payment-data.interface';
import { getCashPaymentDataHandler } from '../../post-messages-handlers/get-cash-payment-data/get-cash-payment-data.handler';
import { PostMessagesClient } from '../../../../core/post-messages-client/post-messages-client';
import { headlessCheckoutAppUrl } from '../../environment';
import { getXsollaNumberComponentTemplate } from './templates/get-xsolla-number.compontent.template';
import './xsolla-number.component.scss';
import { SendButtonStatus } from './send-button-status.interface';
Expand Down Expand Up @@ -56,8 +55,9 @@ export class XsollaNumberComponent extends SecureComponentAbstract {
}

protected getHtml(): string {
const emailControl = `<iframe src='${headlessCheckoutAppUrl}/secure-components/xsolla-number/email'></iframe>`;
const phoneControl = `<iframe src='${headlessCheckoutAppUrl}/secure-components/xsolla-number/phone'></iframe>`;
const appUrl = this.environmentService.getHeadlessCheckoutAppUrl();
const emailControl = `<iframe src='${appUrl}/secure-components/xsolla-number/email'></iframe>`;
const phoneControl = `<iframe src='${appUrl}/secure-components/xsolla-number/phone'></iframe>`;
const title = this.cashPaymentData?.title;
const userName = this.cashPaymentData?.publicId;
const xsollaNumber = this.cashPaymentData?.xsollaNumber;
Expand Down