From 66fe6eec0bd9fc741b8fd6191190b96fbbfa510c Mon Sep 17 00:00:00 2001 From: Yu Long Date: Mon, 16 Dec 2024 17:27:07 +0100 Subject: [PATCH 1/2] feat(analytics): track api errors - makePayments: '620' - submitPaymentDetails: '621' - submitThreeDS2Fingerprint: '622' - createOrder: '623' --- .changeset/fresh-lamps-raise.md | 9 ++++++ .../lib/src/components/Giftcard/Giftcard.tsx | 8 +++++- .../ThreeDS2/callSubmit3DS2Fingerprint.ts | 4 ++- .../internal/UIElement/UIElement.tsx | 15 +++++++--- .../lib/src/core/Errors/AdyenCheckoutError.ts | 6 ++-- packages/lib/src/core/Services/http.ts | 28 +++++++++++++++---- .../src/core/Services/sessions/constants.ts | 8 ++++++ .../core/Services/sessions/create-order.ts | 4 +-- .../core/Services/sessions/make-payment.ts | 4 +-- .../core/Services/sessions/submit-details.ts | 4 +-- 10 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 .changeset/fresh-lamps-raise.md diff --git a/.changeset/fresh-lamps-raise.md b/.changeset/fresh-lamps-raise.md new file mode 100644 index 0000000000..5c734e6d2c --- /dev/null +++ b/.changeset/fresh-lamps-raise.md @@ -0,0 +1,9 @@ +--- +'@adyen/adyen-web': minor +--- + +Start tracking API errors for the following endpoints for analytics purposes: +- `/sessions/${session.id}/payments` +- `/sessions/${session.id}/orders` +- `/sessions/${session.id}/paymentDetails` +- `v1/submitThreeDS2Fingerprint` \ No newline at end of file diff --git a/packages/lib/src/components/Giftcard/Giftcard.tsx b/packages/lib/src/components/Giftcard/Giftcard.tsx index 1c1f09feaa..3c567e33df 100644 --- a/packages/lib/src/components/Giftcard/Giftcard.tsx +++ b/packages/lib/src/components/Giftcard/Giftcard.tsx @@ -102,7 +102,13 @@ export class GiftcardElement extends UIElement { }) .catch(error => { this.setStatus(error?.message || 'error'); - if (this.props.onError) this.handleError(new AdyenCheckoutError('ERROR', error)); + if (this.props.onError) { + if (error instanceof AdyenCheckoutError) { + this.handleError(error); + } else { + this.handleError(new AdyenCheckoutError('ERROR', error)); + } + } }); }; diff --git a/packages/lib/src/components/ThreeDS2/callSubmit3DS2Fingerprint.ts b/packages/lib/src/components/ThreeDS2/callSubmit3DS2Fingerprint.ts index 91dcaccf5b..06c3703034 100644 --- a/packages/lib/src/components/ThreeDS2/callSubmit3DS2Fingerprint.ts +++ b/packages/lib/src/components/ThreeDS2/callSubmit3DS2Fingerprint.ts @@ -5,6 +5,7 @@ import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; import { THREEDS2_ERROR, THREEDS2_FINGERPRINT_SUBMIT } from './constants'; import { ANALYTICS_ERROR_TYPE, Analytics3DS2Errors } from '../../core/Analytics/constants'; import { SendAnalyticsObject } from '../../core/Analytics/types'; +import { API_ERROR_CODE } from '../../core/Services/sessions/constants'; /** * ThreeDS2DeviceFingerprint, onComplete, calls a new, internal, endpoint which @@ -15,7 +16,8 @@ export default function callSubmit3DS2Fingerprint({ data }): void { { path: `v1/submitThreeDS2Fingerprint?token=${this.props.clientKey}`, loadingContext: this.props.loadingContext, - errorLevel: 'fatal' + errorLevel: 'fatal', + errorCode: API_ERROR_CODE.submitThreeDS2Fingerprint }, { ...data diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index 945de0637b..d65253fd90 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -2,10 +2,10 @@ import { h } from 'preact'; import BaseElement from '../BaseElement/BaseElement'; import PayButton from '../PayButton'; import { assertIsDropin, cleanupFinalResult, getRegulatoryDefaults, sanitizeResponse, verifyPaymentDidNotFail } from './utils'; -import AdyenCheckoutError from '../../../core/Errors/AdyenCheckoutError'; +import AdyenCheckoutError, { NETWORK_ERROR } from '../../../core/Errors/AdyenCheckoutError'; import { hasOwnProperty } from '../../../utils/hasOwnProperty'; import { Resources } from '../../../core/Context/Resources'; -import { ANALYTICS_SUBMIT_STR } from '../../../core/Analytics/constants'; +import { ANALYTICS_ERROR_TYPE, ANALYTICS_EVENT, ANALYTICS_SUBMIT_STR } from '../../../core/Analytics/constants'; import type { AnalyticsInitialEvent, SendAnalyticsObject } from '../../../core/Analytics/types'; import type { CoreConfiguration, ICore, AdditionalDetailsData } from '../../../core/types'; @@ -249,8 +249,11 @@ export abstract class UIElement

exten try { return await this.core.session.submitPayment(data); } catch (error: unknown) { - if (error instanceof AdyenCheckoutError) this.handleError(error); - else this.handleError(new AdyenCheckoutError('ERROR', 'Error when making /payments call', { cause: error })); + if (error instanceof AdyenCheckoutError) { + this.handleError(error); + } else { + this.handleError(new AdyenCheckoutError('ERROR', 'Error when making /payments call', { cause: error })); + } return Promise.reject(error); } @@ -277,6 +280,10 @@ export abstract class UIElement

exten */ this.setElementStatus('ready'); + if (error.name === NETWORK_ERROR && error.options.code) { + this.submitAnalytics({ type: ANALYTICS_EVENT.error, errorType: ANALYTICS_ERROR_TYPE.apiError, code: error.options.code }); + } + if (this.props.onError) { this.props.onError(error, this.elementRef); } diff --git a/packages/lib/src/core/Errors/AdyenCheckoutError.ts b/packages/lib/src/core/Errors/AdyenCheckoutError.ts index 4b66aeba89..9e6618f3e0 100644 --- a/packages/lib/src/core/Errors/AdyenCheckoutError.ts +++ b/packages/lib/src/core/Errors/AdyenCheckoutError.ts @@ -1,5 +1,6 @@ interface CheckoutErrorOptions { cause?: any; + code?: string; } export const NETWORK_ERROR = 'NETWORK_ERROR'; @@ -35,13 +36,14 @@ class AdyenCheckoutError extends Error { }; public cause: unknown; + public options: CheckoutErrorOptions; constructor(type: keyof typeof AdyenCheckoutError.errorTypes, message?: string, options?: CheckoutErrorOptions) { super(message); this.name = AdyenCheckoutError.errorTypes[type]; - - this.cause = options?.cause; + this.options = options || {}; + this.cause = this.options.cause; } } diff --git a/packages/lib/src/core/Services/http.ts b/packages/lib/src/core/Services/http.ts index e5d2cdd5d8..9542249138 100644 --- a/packages/lib/src/core/Services/http.ts +++ b/packages/lib/src/core/Services/http.ts @@ -11,6 +11,14 @@ export interface HttpOptions { timeout?: number; errorLevel?: ErrorLevel; errorMessage?: string; + errorCode?: string; +} + +interface FetchErrorOptions { + message?: string; + level?: ErrorLevel; + cause?: unknown; + code?: string; } type ErrorLevel = 'silent' | 'info' | 'warn' | 'error' | 'fatal'; @@ -27,7 +35,15 @@ function isAdyenApiErrorResponse(data: any): data is AdyenApiErrorResponse { } export function http(options: HttpOptions, data?: any): Promise { - const { headers = [], errorLevel = 'warn', loadingContext = FALLBACK_CONTEXT, method = 'GET', path, timeout = DEFAULT_HTTP_TIMEOUT } = options; + const { + headers = [], + errorLevel = 'warn', + errorCode, + loadingContext = FALLBACK_CONTEXT, + method = 'GET', + path, + timeout = DEFAULT_HTTP_TIMEOUT + } = options; const request: RequestInit = { method, @@ -57,12 +73,12 @@ export function http(options: HttpOptions, data?: any): Promise { } if (isAdyenApiErrorResponse(data)) { - handleFetchError(data.message, errorLevel, data); + handleFetchError({ message: data.message, level: errorLevel, cause: data, code: errorCode }); return; } const errorMessage = options.errorMessage || `Service at ${url} is not available`; - handleFetchError(errorMessage, errorLevel, data); + handleFetchError({ message: errorMessage, level: errorLevel, cause: data, code: errorCode }); return; }) /** @@ -81,12 +97,12 @@ export function http(options: HttpOptions, data?: any): Promise { // eslint-disable-next-line @typescript-eslint/no-base-to-string,@typescript-eslint/restrict-template-expressions const errorMessage = options.errorMessage || `Call to ${url} failed. Error= ${error}`; - handleFetchError(errorMessage, errorLevel, error); + handleFetchError({ message: errorMessage, level: errorLevel, cause: error, code: errorCode }); }) ); } -function handleFetchError(message: string, level: ErrorLevel, error: unknown): void { +function handleFetchError({ message, level, cause, code }: FetchErrorOptions): void { switch (level) { case 'silent': { break; @@ -98,7 +114,7 @@ function handleFetchError(message: string, level: ErrorLevel, error: unknown): v break; } default: - throw new AdyenCheckoutError('NETWORK_ERROR', message, { cause: error }); + throw new AdyenCheckoutError('NETWORK_ERROR', message, { cause, code }); } } diff --git a/packages/lib/src/core/Services/sessions/constants.ts b/packages/lib/src/core/Services/sessions/constants.ts index ba1d38d499..8ece7c6a84 100644 --- a/packages/lib/src/core/Services/sessions/constants.ts +++ b/packages/lib/src/core/Services/sessions/constants.ts @@ -1 +1,9 @@ export const API_VERSION = 'v1'; + +// Same error code will be sent to the analytics +export const API_ERROR_CODE = { + makePayments: '620', + submitPaymentDetails: '621', + submitThreeDS2Fingerprint: '622', + createOrder: '623' +}; diff --git a/packages/lib/src/core/Services/sessions/create-order.ts b/packages/lib/src/core/Services/sessions/create-order.ts index 9c8171c1ed..da0803aa38 100644 --- a/packages/lib/src/core/Services/sessions/create-order.ts +++ b/packages/lib/src/core/Services/sessions/create-order.ts @@ -1,7 +1,7 @@ import { httpPost } from '../http'; import Session from '../../CheckoutSession'; import { CheckoutSessionOrdersResponse } from '../../CheckoutSession/types'; -import { API_VERSION } from './constants'; +import { API_ERROR_CODE, API_VERSION } from './constants'; /** */ @@ -11,7 +11,7 @@ function createOrder(session: Session): Promise { sessionData: session.data }; - return httpPost({ loadingContext: session.loadingContext, path, errorLevel: 'fatal' }, data); + return httpPost({ loadingContext: session.loadingContext, path, errorLevel: 'fatal', errorCode: API_ERROR_CODE.createOrder }, data); } export default createOrder; diff --git a/packages/lib/src/core/Services/sessions/make-payment.ts b/packages/lib/src/core/Services/sessions/make-payment.ts index 827a6435e4..d527591fa5 100644 --- a/packages/lib/src/core/Services/sessions/make-payment.ts +++ b/packages/lib/src/core/Services/sessions/make-payment.ts @@ -1,7 +1,7 @@ import { httpPost } from '../http'; import Session from '../../CheckoutSession'; import { CheckoutSessionPaymentResponse } from '../../CheckoutSession/types'; -import { API_VERSION } from './constants'; +import { API_ERROR_CODE, API_VERSION } from './constants'; /** */ @@ -12,7 +12,7 @@ function makePayment(paymentRequest, session: Session): Promise Date: Tue, 17 Dec 2024 17:11:00 +0100 Subject: [PATCH 2/2] test(UIElement): added tests for sending error event to the analytics --- .../lib/src/components/ANCV/ANCV.test.tsx | 50 ++++++++++++++ packages/lib/src/components/ANCV/ANCV.tsx | 8 ++- .../src/components/Giftcard/Giftcard.test.tsx | 39 +++++++++++ .../lib/src/components/Giftcard/Giftcard.tsx | 1 - .../internal/UIElement/UIElement.test.ts | 65 ++++++++++++++++++- .../internal/UIElement/UIElement.tsx | 2 +- 6 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 packages/lib/src/components/ANCV/ANCV.test.tsx diff --git a/packages/lib/src/components/ANCV/ANCV.test.tsx b/packages/lib/src/components/ANCV/ANCV.test.tsx new file mode 100644 index 0000000000..3a1bf780bd --- /dev/null +++ b/packages/lib/src/components/ANCV/ANCV.test.tsx @@ -0,0 +1,50 @@ +import { render } from '@testing-library/preact'; +import { mockDeep } from 'jest-mock-extended'; +import { AnalyticsModule } from '../../types/global-types'; +import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; +import { ANALYTICS_ERROR_TYPE, ANALYTICS_EVENT } from '../../core/Analytics/constants'; +import ANCV from './ANCV'; + +const flushPromises = () => new Promise(process.nextTick); + +describe('ANCV', () => { + const resources = global.resources; + const i18n = global.i18n; + + const baseProps = { + amount: { value: 1000, currency: 'EUR' }, + i18n, + loadingContext: 'mock' + }; + + describe('createOrder', () => { + test('should send an error event to the analytics if the createOrder call fails for the session flow', async () => { + const code = 'mockErrorCode'; + const analytics = mockDeep(); + const mockedSendAnalytics = analytics.sendAnalytics as jest.Mock; + + const ancv = new ANCV(global.core, { + ...baseProps, + modules: { + resources, + analytics + }, + onError: () => {}, + // @ts-ignore test only + session: { + createOrder: () => { + return Promise.reject(new AdyenCheckoutError('NETWORK_ERROR', '', { code })); + } + } + }); + render(ancv.render()); + await ancv.createOrder(); + await flushPromises(); + expect(mockedSendAnalytics).toHaveBeenCalledWith( + 'ancv', + { code, errorType: ANALYTICS_ERROR_TYPE.apiError, type: ANALYTICS_EVENT.error }, + undefined + ); + }); + }); +}); diff --git a/packages/lib/src/components/ANCV/ANCV.tsx b/packages/lib/src/components/ANCV/ANCV.tsx index 7f2c5e74d5..64662df326 100644 --- a/packages/lib/src/components/ANCV/ANCV.tsx +++ b/packages/lib/src/components/ANCV/ANCV.tsx @@ -56,7 +56,13 @@ export class ANCVElement extends UIElement { }) .catch(error => { this.setStatus(error?.message || 'error'); - if (this.props.onError) this.handleError(new AdyenCheckoutError('ERROR', error)); + if (this.props.onError) { + if (error instanceof AdyenCheckoutError) { + this.handleError(error); + } else { + this.handleError(new AdyenCheckoutError('ERROR', error)); + } + } }); }; diff --git a/packages/lib/src/components/Giftcard/Giftcard.test.tsx b/packages/lib/src/components/Giftcard/Giftcard.test.tsx index 5a4da203c6..337e27a896 100644 --- a/packages/lib/src/components/Giftcard/Giftcard.test.tsx +++ b/packages/lib/src/components/Giftcard/Giftcard.test.tsx @@ -1,6 +1,10 @@ import Giftcard from './Giftcard'; import { render, screen } from '@testing-library/preact'; import userEvent from '@testing-library/user-event'; +import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; +import { AnalyticsModule } from '../../types/global-types'; +import { mockDeep } from 'jest-mock-extended'; +import { ANALYTICS_ERROR_TYPE, ANALYTICS_EVENT } from '../../core/Analytics/constants'; const flushPromises = () => new Promise(process.nextTick); @@ -135,6 +139,41 @@ describe('Giftcard', () => { expect(onOrderRequest).toHaveBeenCalled(); }); + test('should send an error event to the analytics if the createOrder call fails for the session flow', async () => { + const code = 'mockErrorCode'; + const analytics = mockDeep(); + const mockedSendAnalytics = analytics.sendAnalytics as jest.Mock; + const onBalanceCheck = jest.fn(resolve => + resolve({ + balance: { value: 500, currency: 'EUR' } + }) + ); + const giftcard = new Giftcard(global.core, { + ...baseProps, + modules: { + resources, + analytics + }, + onError: () => {}, + onBalanceCheck, + // @ts-ignore test only + session: { + createOrder: () => { + return Promise.reject(new AdyenCheckoutError('NETWORK_ERROR', '', { code })); + } + } + }); + render(giftcard.render()); + giftcard.setState({ isValid: true }); + giftcard.balanceCheck(); + await flushPromises(); + expect(mockedSendAnalytics).toHaveBeenCalledWith( + 'giftcard', + { code, errorType: ANALYTICS_ERROR_TYPE.apiError, type: ANALYTICS_EVENT.error }, + undefined + ); + }); + test('if there is enough balance for checkout we should require confirmation', async () => { const onBalanceCheck = jest.fn(resolve => resolve({ diff --git a/packages/lib/src/components/Giftcard/Giftcard.tsx b/packages/lib/src/components/Giftcard/Giftcard.tsx index 3c567e33df..f8f57077a6 100644 --- a/packages/lib/src/components/Giftcard/Giftcard.tsx +++ b/packages/lib/src/components/Giftcard/Giftcard.tsx @@ -62,7 +62,6 @@ export class GiftcardElement extends UIElement { return new Promise((resolve, reject) => { void this.props.onOrderRequest(resolve, reject, data); }); - if (this.props.session) { return this.props.session.createOrder(); } diff --git a/packages/lib/src/components/internal/UIElement/UIElement.test.ts b/packages/lib/src/components/internal/UIElement/UIElement.test.ts index 9cba2b8b11..355d6a620b 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.test.ts +++ b/packages/lib/src/components/internal/UIElement/UIElement.test.ts @@ -4,7 +4,9 @@ import { any, mock, mockDeep } from 'jest-mock-extended'; import { AdyenCheckout, ThreeDS2Challenge, ThreeDS2DeviceFingerprint } from '../../../index'; import { UIElementProps } from './types'; import { Resources } from '../../../core/Context/Resources'; -import { PaymentActionsType } from '../../../types/global-types'; +import { AnalyticsModule, PaymentActionsType } from '../../../types/global-types'; +import AdyenCheckoutError from '../../../core/Errors/AdyenCheckoutError'; +import { ANALYTICS_ERROR_TYPE, ANALYTICS_EVENT } from '../../../core/Analytics/constants'; jest.mock('../../../core/Services/get-translations'); @@ -23,6 +25,9 @@ class MyElement extends UIElement { public callOnChange() { super.onChange(); } + public handleAdditionalDetails(data) { + super.handleAdditionalDetails(data); + } render() { return ''; } @@ -32,8 +37,15 @@ const submitMock = jest.fn(); (global as any).HTMLFormElement.prototype.submit = () => submitMock; let core; +let analytics; beforeEach(() => { core = mockDeep(); + analytics = mockDeep(); +}); + +afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); }); describe('UIElement', () => { @@ -231,7 +243,7 @@ describe('UIElement', () => { test('should trigger showValidation() and not call makePaymentsCall() if component is not valid', () => { const showValidation = jest.fn(); - const element = new MyElement(core); + const element = new MyElement(core, { modules: { analytics } }); // @ts-ignore Checking that internal method is not reached const makePaymentsCallSpy = jest.spyOn(element, 'makePaymentsCall'); @@ -263,6 +275,7 @@ describe('UIElement', () => { }); const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock, onPaymentCompleted: onPaymentCompletedMock }); @@ -275,7 +288,7 @@ describe('UIElement', () => { expect(onPaymentCompletedMock).toHaveBeenCalledWith({ resultCode: 'Authorized' }, element); }); - test('should make successfull payment using sessions flow', async () => { + test('should make successful payment using sessions flow', async () => { const onPaymentCompletedMock = jest.fn(); core.session.submitPayment.calledWith(any()).mockResolvedValue({ @@ -293,6 +306,7 @@ describe('UIElement', () => { }); const element = new MyElement(core, { + modules: { analytics }, onPaymentCompleted: onPaymentCompletedMock }); @@ -327,6 +341,7 @@ describe('UIElement', () => { }); const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock, onPaymentFailed: onPaymentFailedMock }); @@ -347,6 +362,7 @@ describe('UIElement', () => { jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock, onPaymentFailed: onPaymentFailedMock }); @@ -374,6 +390,7 @@ describe('UIElement', () => { jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock }); @@ -420,6 +437,7 @@ describe('UIElement', () => { }); const element = new MyElement(core, { + modules: { analytics }, onOrderUpdated: onOrderUpdatedMock }); @@ -469,6 +487,7 @@ describe('UIElement', () => { core.session = null; const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock, onPaymentMethodsRequest: onPaymentMethodsRequestMock, onOrderUpdated: onOrderUpdatedMock @@ -531,6 +550,7 @@ describe('UIElement', () => { core.session = null; const element = new MyElement(core, { + modules: { analytics }, onSubmit: onSubmitMock, onOrderUpdated: onOrderUpdatedMock, onError: onErrorMock @@ -551,6 +571,26 @@ describe('UIElement', () => { expect(onOrderUpdatedMock).toHaveBeenCalledTimes(1); expect(onOrderUpdatedMock).toHaveBeenCalledWith({ order }); }); + + test('should send an error event to analytic module with correct errorType and error code, if makePayment call fails', async () => { + const errorCode = 'mockedErrorCode'; + const txVariant = 'scheme'; + + core.session.submitPayment.mockImplementation(() => Promise.reject(new AdyenCheckoutError('NETWORK_ERROR', '', { code: errorCode }))); + const analytics = mock(); + const mockedSendAnalytics = analytics.sendAnalytics as jest.Mock; + jest.spyOn(MyElement.prototype, 'isValid', 'get').mockReturnValue(true); + + const element = new MyElement(core, { type: txVariant, modules: { analytics } }); + element.submit(); + await new Promise(process.nextTick); + + expect(mockedSendAnalytics).toHaveBeenCalledWith( + txVariant, + { code: errorCode, errorType: ANALYTICS_ERROR_TYPE.apiError, type: ANALYTICS_EVENT.error }, + undefined + ); + }); }); describe('[Internal] handleAdditionalDetails()', () => { @@ -684,5 +724,24 @@ describe('UIElement', () => { expect(onPaymentFailedMock).toHaveBeenCalledTimes(1); expect(onPaymentFailedMock).toHaveBeenCalledWith(undefined, element); }); + + test('should send an error event to analytic module with correct errorType and error code, if payment/details call fails', async () => { + const errorCode = 'mockedErrorCode'; + const txVariant = 'scheme'; + + core.session.submitDetails.mockImplementation(() => Promise.reject(new AdyenCheckoutError('NETWORK_ERROR', '', { code: errorCode }))); + + const mockedSendAnalytics = analytics.sendAnalytics as jest.Mock; + + const element = new MyElement(core, { type: txVariant, modules: { analytics } }); + element.handleAdditionalDetails({}); + await new Promise(process.nextTick); + + expect(mockedSendAnalytics).toHaveBeenCalledWith( + txVariant, + { code: errorCode, errorType: ANALYTICS_ERROR_TYPE.apiError, type: ANALYTICS_EVENT.error }, + undefined + ); + }); }); }); diff --git a/packages/lib/src/components/internal/UIElement/UIElement.tsx b/packages/lib/src/components/internal/UIElement/UIElement.tsx index d65253fd90..7c0e4497ac 100644 --- a/packages/lib/src/components/internal/UIElement/UIElement.tsx +++ b/packages/lib/src/components/internal/UIElement/UIElement.tsx @@ -177,7 +177,7 @@ export abstract class UIElement

exten if (this.constructor['type'] === 'scheme' || this.constructor['type'] === 'bcmc') { return this.constructor['type']; } - return this.props.type; + return this.type; } public submit(): void {