From dd00ff70f4ba4b7b133bb67f5648ac049ae024e0 Mon Sep 17 00:00:00 2001 From: skyflow-bharti <118584001+skyflow-bharti@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:26:10 +0530 Subject: [PATCH 01/47] Release/25.7.8.2 (#615) --- package.json | 2 +- src/core-utils/collect.ts | 147 +++- src/core-utils/reveal.ts | 109 +++ src/core/constants.ts | 24 + src/core/external/collect/collect-element.ts | 250 ++++--- .../collect/compose-collect-container.ts | 372 +++++++---- .../collect/compose-collect-element.ts | 57 +- src/core/external/common/iframe.ts | 2 +- .../reveal/composable-reveal-container.ts | 474 +++++++++++++ .../reveal/composable-reveal-element.ts | 66 ++ .../reveal/composable-reveal-internal.ts | 610 +++++++++++++++++ .../internal/composable-frame-element-init.ts | 369 +++++++++++ src/core/internal/frame-element-init.ts | 625 +++++++++++++++++- src/core/internal/iframe-form/index.ts | 181 ++++- src/core/internal/index.ts | 39 +- src/core/internal/reveal/reveal-frame.ts | 299 +++++++-- .../skyflow-frame/skyflow-frame-controller.ts | 8 +- src/index-internal.ts | 5 + src/libs/element-options.ts | 4 +- src/skyflow.ts | 62 ++ src/utils/common/index.ts | 12 + src/utils/constants.ts | 6 + src/utils/logs.ts | 6 + 23 files changed, 3443 insertions(+), 286 deletions(-) create mode 100644 src/core/external/reveal/composable-reveal-container.ts create mode 100644 src/core/external/reveal/composable-reveal-element.ts create mode 100644 src/core/external/reveal/composable-reveal-internal.ts create mode 100644 src/core/internal/composable-frame-element-init.ts diff --git a/package.json b/package.json index 8b9a7c65..c7281b16 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.2", + "version": "2.4.2-dev.6c87190", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", diff --git a/src/core-utils/collect.ts b/src/core-utils/collect.ts index e4672d8a..626dab2e 100644 --- a/src/core-utils/collect.ts +++ b/src/core-utils/collect.ts @@ -203,7 +203,7 @@ const updateRecordsInVault = ( options, ) => { const table = skyflowIdRecord.fields.table; - const skyflowID = skyflowIdRecord.skyflowID; + const skyflowID = skyflowIdRecord?.skyflowID; skyflowIdRecord.fields = omit(skyflowIdRecord.fields, 'table'); skyflowIdRecord.fields = omit(skyflowIdRecord.fields, 'skyflowID'); return client.request({ @@ -277,6 +277,151 @@ export const updateRecordsBySkyflowID = async ( }); }); +export const updateRecordsBySkyflowIDComposable = async ( + skyflowIdRecords, + client: Client, + options, + authToken: string, +) => new Promise((rootResolve, rootReject) => { + let updateResponseSet: Promise[]; + // eslint-disable-next-line prefer-const + updateResponseSet = skyflowIdRecords?.updateRecords?.map( + (skyflowIdRecord: IInsertRecord) => new Promise((resolve, reject) => { + updateRecordsInVault(skyflowIdRecord, client, authToken, options) + ?.then((resolvedResult: any) => { + const resp = constructFinalUpdateRecordResponse( + resolvedResult, + options?.tokens, + skyflowIdRecord, + ); + resolve(resp); + }, + (rejectedResult) => { + let errorResponse = rejectedResult; + if (rejectedResult?.error) { + errorResponse = { + error: { + code: rejectedResult?.error?.code, + description: rejectedResult?.error?.description, + }, + }; + } + printLog(rejectedResult?.error?.description ?? '', MessageType.ERROR, LogLevel.ERROR); + reject(errorResponse); + })?.catch((error) => { + reject(error); + }); + }), + ); + Promise.allSettled(updateResponseSet)?.then((resultSet: any) => { + const recordsResponse: any[] = []; + const errorsResponse: any[] = []; + resultSet?.forEach((result: { status: string; value: any; reason?: any; }) => { + if (result?.status === 'fulfilled') { + recordsResponse?.push(result?.value); + } else { + errorsResponse?.push(result?.reason); + } + }); + + if (errorsResponse?.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse?.length === 0) { + rootReject({ errors: errorsResponse }); + } else { + rootReject({ records: recordsResponse, errors: errorsResponse }); + } + }); +}); + +export const insertDataInCollect = async ( + records, + client: Client, + options, + finalInsertRecords, + authToken: string, +) => new Promise((resolve) => { + let insertResponse: any; + let insertErrorResponse: any; + client + ?.request({ + body: { + records, + }, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + ?.then((response: any) => { + insertResponse = constructInsertRecordResponse( + response, + options?.tokens, + finalInsertRecords?.records, + ); + resolve(insertResponse); + }) + ?.catch((error) => { + insertErrorResponse = { + errors: [ + { + error: { + code: error?.error?.code, + description: error?.error?.description, + }, + }, + ], + }; + resolve(insertErrorResponse); + }); +}); + +export const insertDataInMultipleFiles = async ( + records, + client: Client, + options, + finalInsertRecords, + authToken: string, +) => new Promise((resolve) => { + let insertResponse: any; + let insertErrorResponse: any; + client + ?.request({ + body: { + records, + }, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + ?.then((response: any) => { + insertResponse = constructInsertRecordResponse( + response, + options?.tokens, + finalInsertRecords?.records, + ); + resolve(insertResponse); + }) + ?.catch((error) => { + insertErrorResponse = { + errors: [ + { + error: { + code: error?.error?.code, + description: error?.error?.description, + }, + }, + ], + }; + resolve(insertErrorResponse); + }); +}); + export const checkForElementMatchRule = (validations: IValidationRule[]) => { if (!validations) return false; for (let i = 0; i < validations.length; i += 1) { diff --git a/src/core-utils/reveal.ts b/src/core-utils/reveal.ts index 7285716d..79361634 100644 --- a/src/core-utils/reveal.ts +++ b/src/core-utils/reveal.ts @@ -11,6 +11,7 @@ import { IRenderResponseType, IGetOptions, RenderFileResponse, + IRevealRecordComposable, } from '../utils/common'; import { printLog } from '../utils/logs-helper'; import { FILE_DOWNLOAD_URL_PARAM } from '../core/constants'; @@ -177,6 +178,27 @@ export const getFileURLFromVaultBySkyflowID = ( rootReject(err); } }); + +export const getFileURLFromVaultBySkyflowIDComposable = ( + skyflowIdRecord: IRevealRecord, + client: Client, + authToken: string, +): Promise => new Promise((rootResolve, rootReject) => { + try { + getFileURLForRender( + skyflowIdRecord, client, authToken as string, + ).then((resolvedResult: IRenderResponseType) => { + rootResolve(resolvedResult); + }).catch((err: any) => { + const errorData = formatForRenderFileFailure(err, skyflowIdRecord.skyflowID as string, + skyflowIdRecord.column as string); + printLog(errorData.error?.description || '', MessageType.ERROR, LogLevel.ERROR); + rootReject(errorData); + }); + } catch (err) { + rootReject(err); + } +}); export const fetchRecordsByTokenId = ( tokenIdRecords: IRevealRecord[], client: Client, @@ -230,6 +252,65 @@ export const fetchRecordsByTokenId = ( rootReject(err); }); }); + +export const fetchRecordsByTokenIdComposable = ( + tokenIdRecords: IRevealRecordComposable[], + client: Client, + authToken: string, +): Promise => new Promise((rootResolve, rootReject) => { + const vaultResponseSet: Promise[] = tokenIdRecords?.map( + (tokenRecord) => new Promise((resolve) => { + const apiResponse: any = []; + const redaction: RedactionType = tokenRecord?.redaction ?? RedactionType.PLAIN_TEXT; + + getTokenRecordsFromVault(tokenRecord?.token ?? '', redaction, client, authToken) + ?.then( + (response: IApiSuccessResponse) => { + const fieldsData = formatForPureJsSuccess(response); + apiResponse?.push({ + ...fieldsData, + frameId: tokenRecord?.iframeName ?? '', + }); + }, + (cause: any) => { + const errorData = formatForPureJsFailure(cause, tokenRecord?.token ?? ''); + printLog(errorData?.error?.description ?? '', MessageType.ERROR, LogLevel.ERROR); + apiResponse?.push({ + ...errorData, + frameId: tokenRecord?.iframeName ?? '', + }); + }, + ) + ?.finally(() => { + resolve(apiResponse); + }); + }), + ); + + Promise.allSettled(vaultResponseSet)?.then((resultSet) => { + const recordsResponse: Record[] = []; + const errorResponse: Record[] = []; + resultSet?.forEach((result) => { + if (result?.status === 'fulfilled') { + result?.value?.forEach((res: Record) => { + if (Object.prototype.hasOwnProperty.call(res, 'error')) { + errorResponse?.push(res); + } else { + recordsResponse?.push(res); + } + }); + } + }); + if (errorResponse?.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse?.length === 0) { + rootReject({ errors: errorResponse }); + } else { + rootReject({ records: recordsResponse, errors: errorResponse }); + } + }); +}); + export const formatRecordsForIframe = (response: IRevealResponseType) => { const result: Record = {}; if (response.records) { @@ -283,6 +364,34 @@ export const formatRecordsForClient = (response: IRevealResponseType) => { return { errors: response.errors }; }; +export const formatRecordsForClientComposable = (response) => { + let successRecords = []; + let errorRecords = []; + + if (response?.errors && response?.errors?.length > 0) { + errorRecords = response?.errors?.map((errors) => ({ + error: errors?.error ?? {}, + })); + } + + if (response?.records) { + successRecords = response?.records?.map((record) => ({ + token: record?.[0]?.token ?? '', + valueType: record?.[0]?.valueType ?? '', + })); + } + + if (successRecords?.length > 0 && errorRecords?.length > 0) { + return { success: successRecords, errors: errorRecords }; + } + + if (successRecords?.length > 0) { + return { success: successRecords }; + } + + return { errors: errorRecords }; +}; + export const fetchRecordsGET = async ( skyflowIdRecords: IGetRecord[], client: Client, diff --git a/src/core/constants.ts b/src/core/constants.ts index 57196408..7e2a2761 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -30,11 +30,13 @@ export const SDK_IFRAME_EVENT = 'SDK IFRAME EVENT'; export const DOMAIN = 'US2'; export const CORALOGIX_DOMAIN = 'https://cdn.rum-ingress-coralogix.com/coralogix/browser/latest/coralogix-browser-sdk.js'; export const FRAME_ELEMENT = 'element'; +export const COMPOSABLE_REVEAL = 'reveal-composable'; export const ELEMENT_TYPES = { COLLECT: 'COLLECT', REVEAL: 'REVEAL', COMPOSE: 'COMPOSABLE', + REVEAL_COMPOSE: 'REVEAL_COMPOSE', }; export const EVENT_TYPES = { @@ -104,8 +106,19 @@ export const ELEMENT_EVENTS_TO_CLIENT = { }; export const ELEMENT_EVENTS_TO_IFRAME = { + MULTIPLE_UPLOAD_FILES_RESPONSE: 'MULTIPLE_UPLOAD_FILES_RESPONSE', + RENDER_MOUNTED: 'RENDER_MOUNTED', + HEIGHT_CALLBACK: 'HEIGHT_CALLBACK', + HEIGHT_CALLBACK_COMPOSABLE: 'HEIGHT_CALLBACK_COMPOSABLE', + COMPOSABLE_REVEAL: 'COMPOSABLE_REVEAL', + MULTIPLE_UPLOAD_FILES: 'MULTIPLE_UPLOAD_FILES', COLLECT_CALL_REQUESTS: 'COLLECT_CALL_REQUESTS', + COMPOSABLE_CALL_REQUESTS: 'COMPOSABLE_CALL_REQUESTS', + COMPOSABLE_CALL_RESPONSE: 'COMPOSABLE_CALL_RESPONSE', + COMPOSABLE_FILE_CALL_RESPONSE: 'COMPOSABLE_FILE_CALL_RESPONSE', + COMPOSABLE_CONTAINER: 'COMPOSABLE_CONTAINER', REVEAL_CALL_REQUESTS: 'REVEAL_CALL_REQUESTS', + REVEAL_CALL_RESPONSE: 'REVEAL_CALL_RESPONSE', FRAME_READY: 'FRAME_READY', READY_FOR_CLIENT: 'READY_FOR_CLIENT', TOKENIZATION_REQUEST: 'TOKENIZATION_REQUEST', @@ -162,6 +175,7 @@ export enum ElementType { EXPIRATION_MONTH = 'EXPIRATION_MONTH', EXPIRATION_YEAR = 'EXPIRATION_YEAR', FILE_INPUT = 'FILE_INPUT', + MULTI_FILE_INPUT = 'MULTI_FILE_INPUT', } export enum CardType { @@ -324,6 +338,14 @@ export const ELEMENTS = { type: 'file', }, }, + [ElementType.MULTI_FILE_INPUT]: { + name: 'MULTI_FILE_INPUT', + sensitive: true, + attributes: { + type: 'file', + multiple: '', + }, + }, }; export const CARDNUMBER_INPUT_FORMAT = { @@ -637,6 +659,7 @@ export const DEFAULT_ERROR_TEXT_ELEMENT_TYPES = { [ElementType.EXPIRATION_MONTH]: 'Invalid expiration month', [ElementType.EXPIRATION_YEAR]: 'Invalid expiration year', [ElementType.FILE_INPUT]: logs.errorLogs.INVALID_COLLECT_VALUE, + [ElementType.MULTI_FILE_INPUT]: logs.errorLogs.INVALID_COLLECT_VALUE, }; export const DEFAULT_REQUIRED_TEXT_ELEMENT_TYPES = { @@ -649,6 +672,7 @@ export const DEFAULT_REQUIRED_TEXT_ELEMENT_TYPES = { [ElementType.EXPIRATION_MONTH]: 'expiration month is required', [ElementType.EXPIRATION_YEAR]: 'expiration year is required', [ElementType.FILE_INPUT]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, + [ElementType.MULTI_FILE_INPUT]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, }; export const INPUT_KEYBOARD_EVENTS = { diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index 298aa5d2..d2aa0ac7 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -119,7 +119,6 @@ class CollectElement extends SkyflowElement { // if (this.#isSingleElementAPI && this.#elements.length > 1) { // throw new SkyflowError(SKYFLOW_ERROR_CODE.UNKNOWN_ERROR, [], true); // } - this.#doesReturnValue = EnvOptions[this.#context.env].doesReturnValue; this.elementType = this.#isSingleElementAPI ? this.#elements[0].elementType @@ -164,18 +163,18 @@ class CollectElement extends SkyflowElement { this.#readyToMount = container.isMounted; if (container.type === ContainerType.COMPOSABLE) { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + + this.#iframe.name) { + this.#iframe.setIframeHeight(event.data.data.height); + } + }); this.#elements.forEach((element) => { - this.#bus.on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED - + formatFrameNameToId(element.elementName), (data) => { - if (data.name === element.elementName) { - updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, `${element.elementType}_${METRIC_TYPES.EVENTS.MOUNTED}`); + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + formatFrameNameToId(element.elementName)) { element.isMounted = true; this.#mounted = true; - this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT - + this.#iframe.name, - {}, (payload:any) => { - this.#iframe.setIframeHeight(payload.height); - }); } }); }); @@ -199,6 +198,7 @@ class CollectElement extends SkyflowElement { getID = () => this.#elementId; mount = (domElement: HTMLElement | string) => { + this.#mounted = true; if (!domElement) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['CollectElement'], true); } @@ -468,84 +468,168 @@ class CollectElement extends SkyflowElement { } }); this.#elements.forEach((element1) => { - this.#bus.on(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT - + element1.elementName, (data: any) => { - if ( - this.#isSingleElementAPI + const isComposableContainer = this.#elements.length > 1; + if (isComposableContainer) { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + + element1.elementName) { + const data = event.data.data; + if (data.name === element1.elementName) { + if ( + this.#isSingleElementAPI && data.event === ELEMENT_EVENTS_TO_CLIENT.READY && data.name === formatFrameNameToId(this.#iframe.name) - ) { - this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); - } else { - const isComposable = this.#elements.length > 1; - this.#elements.forEach((element, index) => { - if (data.name === element.elementName) { - let emitEvent = ''; - switch (data.event) { - case ELEMENT_EVENTS_TO_CLIENT.FOCUS: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.FOCUS; - break; - case ELEMENT_EVENTS_TO_CLIENT.BLUR: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.BLUR; - break; - case ELEMENT_EVENTS_TO_CLIENT.CHANGE: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.CHANGE; - break; - case ELEMENT_EVENTS_TO_CLIENT.READY: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.READY; - break; - case ELEMENT_EVENTS_TO_CLIENT.SUBMIT: - this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.SUBMIT); - return; - // case ELEMENT_EVENTS_TO_CLIENT.CREATED: - // this.#mounted = true; - // return; - // todo: need to implement the below events - // case ELEMENT_EVENTS_TO_CLIENT.ESCAPE: - // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ESCAPE); - // break; - // case ELEMENT_EVENTS_TO_CLIENT.CLICK: - // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.CLICK); - // break; - // case ELEMENT_EVENTS_TO_CLIENT.ERROR: - // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ERROR); - // break; - - default: - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_EVENT_TYPE, [], true); - } - this.#states[index].isEmpty = data.value.isEmpty; - this.#states[index].isValid = data.value.isValid; - this.#states[index].isComplete = data.value.isComplete; - this.#states[index].isFocused = data.value.isFocused; - this.#states[index].isRequired = data.value.isRequired; - this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; - - if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; - else this.#states[index].value = undefined; - - emitEvent = isComposable ? `${emitEvent}:${data.name}` : emitEvent; - - this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT - + this.#iframe.name, - {}, (payload:any) => { - this.#iframe.setIframeHeight(payload.height); - }); - - this.#updateState(); - const emitData = { - ...this.#states[index], - elementType: element.elementType, - }; - if (isComposable && this.#groupEmitter) { - this.#groupEmitter._emit(emitEvent, emitData); + ) { + this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); } else { - this.#eventEmitter._emit(emitEvent, emitData); + const isComposable = this.#elements.length > 1; + this.#elements.forEach((element, index) => { + if (data.name === element.elementName) { + let emitEvent = ''; + switch (data.event) { + case ELEMENT_EVENTS_TO_CLIENT.FOCUS: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.FOCUS; + break; + case ELEMENT_EVENTS_TO_CLIENT.BLUR: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.BLUR; + break; + case ELEMENT_EVENTS_TO_CLIENT.CHANGE: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.CHANGE; + break; + case ELEMENT_EVENTS_TO_CLIENT.READY: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.READY; + break; + case ELEMENT_EVENTS_TO_CLIENT.SUBMIT: + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.SUBMIT); + return; + + default: + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_EVENT_TYPE, [], true); + } + this.#states[index].isEmpty = data.value.isEmpty; + this.#states[index].isValid = data.value.isValid; + this.#states[index].isComplete = data.value.isComplete; + this.#states[index].isFocused = data.value.isFocused; + this.#states[index].isRequired = data.value.isRequired; + this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; + if (element.elementType === ElementType.MULTI_FILE_INPUT) { + this.#states[index].metaData = data?.value?.metaData || []; + } + if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; + else this.#states[index].value = undefined; + + emitEvent = isComposable ? `${emitEvent}:${data.name}` : emitEvent; + this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + + this.#updateState(); + const emitData = { + ...this.#states[index], + elementType: element.elementType, + }; + if (isComposable) { + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, { + iframeName: this.#iframe.name, + }); + } + if (isComposable && this.#groupEmitter) { + this.#groupEmitter._emit(emitEvent, emitData); + } else { + this.#eventEmitter._emit(emitEvent, emitData); + } + } + }); } } - }); - } - }); + } + }); + } else { + this.#bus.on(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + + element1.elementName, (data: any) => { + if ( + this.#isSingleElementAPI + && data.event === ELEMENT_EVENTS_TO_CLIENT.READY + && data.name === formatFrameNameToId(this.#iframe.name) + ) { + this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); + } else { + const isComposable = this.#elements.length > 1; + this.#elements.forEach((element, index) => { + if (data.name === element.elementName) { + let emitEvent = ''; + switch (data.event) { + case ELEMENT_EVENTS_TO_CLIENT.FOCUS: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.FOCUS; + break; + case ELEMENT_EVENTS_TO_CLIENT.BLUR: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.BLUR; + break; + case ELEMENT_EVENTS_TO_CLIENT.CHANGE: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.CHANGE; + break; + case ELEMENT_EVENTS_TO_CLIENT.READY: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.READY; + break; + case ELEMENT_EVENTS_TO_CLIENT.SUBMIT: + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.SUBMIT); + return; + // case ELEMENT_EVENTS_TO_CLIENT.CREATED: + // this.#mounted = true; + // return; + // todo: need to implement the below events + // case ELEMENT_EVENTS_TO_CLIENT.ESCAPE: + // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ESCAPE); + // break; + // case ELEMENT_EVENTS_TO_CLIENT.CLICK: + // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.CLICK); + // break; + // case ELEMENT_EVENTS_TO_CLIENT.ERROR: + // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ERROR); + // break; + + default: + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_EVENT_TYPE, [], true); + } + this.#states[index].isEmpty = data.value.isEmpty; + this.#states[index].isValid = data.value.isValid; + this.#states[index].isComplete = data.value.isComplete; + this.#states[index].isFocused = data.value.isFocused; + this.#states[index].isRequired = data.value.isRequired; + this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; + + if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; + else this.#states[index].value = undefined; + + emitEvent = isComposable ? `${emitEvent}:${data.name}` : emitEvent; + this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + + this.#updateState(); + const emitData = { + ...this.#states[index], + elementType: element.elementType, + }; + if (isComposable) { + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, { + iframeName: this.#iframe.name, + }); + } + if (isComposable && this.#groupEmitter) { + this.#groupEmitter._emit(emitEvent, emitData); + } else { + this.#eventEmitter._emit(emitEvent, emitData); + } + } + }); + } + }); + } }); }; diff --git a/src/core/external/collect/compose-collect-container.ts b/src/core/external/collect/compose-collect-container.ts index 4a9f0480..7c584f5d 100644 --- a/src/core/external/collect/compose-collect-container.ts +++ b/src/core/external/collect/compose-collect-container.ts @@ -21,6 +21,7 @@ import { CollectElementOptions, ICollectOptions, CollectResponse, + UploadFilesResponse, } from '../../../utils/common'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import logs from '../../../utils/logs'; @@ -38,6 +39,8 @@ import { import Container from '../common/container'; import CollectElement from './collect-element'; import ComposableElement from './compose-collect-element'; +import Client from '../../../client'; +import { getAccessToken } from '../../../utils/bus-events'; const CLASS_NAME = 'CollectContainer'; class ComposableContainer extends Container { @@ -71,7 +74,13 @@ class ComposableContainer extends Container { #clientDomain: string = ''; - #isSkyflowFrameReady: boolean = false; + #isComposableFrameReady: boolean = false; + + #shadowRoot: ShadowRoot | null = null; + + #iframeID: string = ''; + + #getSkyflowBearerToken: () => Promise | undefined; constructor(options, metaData, skyflowElements, context) { super(); @@ -89,8 +98,7 @@ class ComposableContainer extends Container { }, }, }; - this.#isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; - + this.#getSkyflowBearerToken = metaData.getSkyflowBearerToken; this.#skyflowElements = skyflowElements; this.#context = context; this.#options = options; @@ -110,6 +118,18 @@ class ComposableContainer extends Container { this.#context.logLevel); this.#containerMounted = true; this.#updateListeners(); + bus + // .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.#containerId, (data, callback) => { + printLog(parameterizedString(logs.infoLogs.INITIALIZE_COMPOSABLE_CLIENT, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + callback({ + client: this.#metaData.clientJSON, + context, + }); + this.#isComposableFrameReady = true; + }); } create = (input: CollectElementInput, options: CollectElementOptions = { @@ -135,7 +155,11 @@ class ComposableContainer extends Container { elementName, }); const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}:${this.#containerId}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`; - return new ComposableElement(elementName, this.#eventEmitter, controllerIframeName); + this.#iframeID = controllerIframeName; + return new ComposableElement( + elementName, this.#eventEmitter, controllerIframeName, + { ...this.#metaData, type: input.type }, + ); }; #createMultipleElement = ( @@ -301,156 +325,228 @@ class ComposableContainer extends Container { this.#containerElement.mount(domElement); this.#isMounted = true; } + this.#elementsList.forEach((element) => { + this.#eventEmitter.on(`${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${element.elementName}`, (data, callback) => { + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${element.elementName}`, + { + elementName: element.name, + data: { + type: COLLECT_TYPES.FILE_UPLOAD, + containerId: this.#containerId, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + options: { + ...data.options, + }, + }, + ); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + callback(err); + }); + }); + }); + if (domElement instanceof HTMLElement + && (domElement as HTMLElement).getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = domElement.getRootNode() as ShadowRoot; + } else if (typeof domElement === 'string') { + const element = document.getElementById(domElement); + if (element && element.getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = element.getRootNode() as ShadowRoot; + } + } + if (this.#shadowRoot !== null) { + this.#eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, (data) => { + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + data.iframeName, {}); + }); + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframeID, {}); + } }; unmount = () => { this.#containerElement.unmount(); }; - collect = (options: ICollectOptions = { tokens: true }) :Promise => { - this.#isSkyflowFrameReady = this.#metaData.skyflowContainer.isControllerFrameReady; - if (this.#isSkyflowFrameReady) { - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (!this.#elementsList || this.#elementsList.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); - } - if (!this.#isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); - } - const containerElements = getElements(this.#tempElements); - containerElements.forEach((element:any) => { - if (!element?.isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } - }); - const elementIds:{ frameId:string, elementId:string }[] = []; - const collectElements = Object.values(this.#elements); - collectElements.forEach((element) => { - element.isValidElement(); - }); - if (options && options.tokens && typeof options.tokens !== 'boolean') { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); - } - if (options?.additionalFields) { - validateAdditionalFieldsInCollect(options.additionalFields); - } - if (options?.upsert) { - validateUpsertOptions(options?.upsert); + collect = (options: ICollectOptions = { tokens: true }) : + Promise => new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + if (!this.#isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); + } + const containerElements = getElements(this.#tempElements); + containerElements.forEach((element:any) => { + if (!element?.isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); + } + }); + const elementIds:{ frameId:string, elementId:string }[] = []; + const collectElements = Object.values(this.#elements); + collectElements.forEach((element) => { + element.isValidElement(); + }); + if (options && options.tokens && typeof options.tokens !== 'boolean') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); + } + if (options?.additionalFields) { + validateAdditionalFieldsInCollect(options.additionalFields); + } + if (options?.upsert) { + validateUpsertOptions(options?.upsert); + } + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: this.#tempElements.elementName, + elementId: element.elementName, + }); + }); + const client = Client.fromJSON(this.#metaData.clientJSON.config) as any; + const clientId = client.toJSON()?.metaData?.uuid || ''; + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + this.#containerId, { + data: { + type: COLLECT_TYPES.COLLECT, + ...options, + tokens: options?.tokens !== undefined ? options.tokens : true, + elementIds, + containerId: this.#containerId, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + window.addEventListener('message', (event) => { + if (event.data?.type + === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.#containerId) { + const data = event.data.data; + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); + reject(data?.error); + } else if (data?.records) { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + resolve(data); + } else { + printLog(`${JSON.stringify(data)}`, MessageType.ERROR, this.#context.logLevel); + reject(data); } - this.#elementsList.forEach((element) => { - elementIds.push({ - frameId: this.#tempElements.elementName, - elementId: element.elementName, - }); - }); - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#metaData.uuid, - { - type: COLLECT_TYPES.COLLECT, - ...options, - tokens: options?.tokens !== undefined ? options.tokens : true, - elementIds, - containerId: this.#containerId, - }, - (data: any) => { - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - - resolve(data); - } - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), - MessageType.LOG, this.#context.logLevel); - } catch (err:any) { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); } }); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), + MessageType.LOG, this.#context.logLevel); + } catch (err:any) { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (!this.#elementsList || this.#elementsList.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); - } - if (!this.#isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); - } + }); + + #emitEvent = (eventName: string, options?: Record, callback?: any) => { + if (this.#shadowRoot) { + const iframe = this.#shadowRoot.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } else { + const iframe = document.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }; - const containerElements = getElements(this.#tempElements); - containerElements.forEach((element:any) => { - if (!element?.isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } + uploadFiles = (options: ICollectOptions): + Promise => new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + if (!this.#isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); + } + const elementIds:{ frameId:string, elementId:string }[] = []; + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: this.#tempElements.elementName, + elementId: element.elementName, }); - const elementIds:{ frameId:string, elementId:string }[] = []; - const collectElements = Object.values(this.#elements); - collectElements.forEach((element) => { - element.isValidElement(); + }); + const client = Client.fromJSON(this.#metaData.clientJSON.config) as any; + const clientId = client.toJSON()?.metaData?.uuid || ''; + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + this.#containerId, { + data: { + type: COLLECT_TYPES.FILE_UPLOAD, + ...options, + // tokens: options?.tokens !== undefined ? options.tokens : true, + elementIds, + containerId: this.#containerId, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, }); - - if (options && options.tokens && typeof options.tokens !== 'boolean') { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); - } - if (options?.additionalFields) { - validateAdditionalFieldsInCollect(options.additionalFields); - } - if (options?.upsert) { - validateUpsertOptions(options?.upsert); - } - this.#elementsList.forEach((element) => { - elementIds.push({ - frameId: this.#tempElements.elementName, - elementId: element.elementName, - }); + window.addEventListener('message', (event) => { + if (event.data?.type + === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.#containerId) { + const data = event.data.data; + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); + reject(data?.error); + } else if (data?.fileUploadResponse) { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + resolve(data); + } else { + printLog(`${JSON.stringify(data)}`, MessageType.ERROR, this.#context.logLevel); + reject(data); + } + } }); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.#containerId, () => { - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#metaData.uuid, - { - type: COLLECT_TYPES.COLLECT, - ...options, - tokens: options?.tokens !== undefined ? options.tokens : true, - elementIds, - containerId: this.#containerId, - }, - (data: any) => { - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - resolve(data); - } - }, - ); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), - MessageType.LOG, this.#context.logLevel); - } catch (err:any) { + }).catch((err:any) => { printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); reject(err); - } - }); - }; + }); + } catch (err:any) { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + } + }); #updateListeners = () => { this.#eventEmitter.on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_UPDATE_OPTIONS, (data) => { diff --git a/src/core/external/collect/compose-collect-element.ts b/src/core/external/collect/compose-collect-element.ts index 44a63a22..e0696be3 100644 --- a/src/core/external/collect/compose-collect-element.ts +++ b/src/core/external/collect/compose-collect-element.ts @@ -1,10 +1,15 @@ +import { Context } from 'vm'; import EventEmitter from '../../../event-emitter'; import { formatValidations } from '../../../libs/element-options'; import SkyflowError from '../../../libs/skyflow-error'; import { ContainerType } from '../../../skyflow'; -import { CollectElementUpdateOptions, EventName } from '../../../utils/common'; +import { + CollectElementUpdateOptions, EventName, MessageType, MetaData, +} from '../../../utils/common'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ElementType } from '../../constants'; +import { printLog } from '../../../utils/logs-helper'; +import logs from '../../../utils/logs'; class ComposableElement { #elementName: string; @@ -19,7 +24,13 @@ class ComposableElement { #isUpdateCalled = false; - constructor(name, eventEmitter, iframeName) { + #metaData: any; + + #context: Context; + + #elementType: ElementType; + + constructor(name, eventEmitter, iframeName, metaData) { this.#elementName = name; this.#iframeName = iframeName; this.#eventEmitter = eventEmitter; @@ -27,6 +38,12 @@ class ComposableElement { this.#eventEmitter.on(`${EventName.READY}:${this.#elementName}`, () => { this.#isMounted = true; }); + this.#metaData = metaData; + this.#context = { + logLevel: this.#metaData.clientJSON?.config?.options?.logLevel, + env: this.#metaData.clientJSON?.config?.options?.env, + }; + this.#elementType = this.#metaData?.type as ElementType; } on(eventName: string, handler: Function) { @@ -98,6 +115,40 @@ class ComposableElement { }); } }; -} + uploadMultipleFiles = (metaData?: MetaData) => new Promise((resolve, reject) => { + try { + if (this.#elementType !== ElementType.MULTI_FILE_INPUT) { + throw new SkyflowError( + SKYFLOW_ERROR_CODE.MULTI_FILE_NOT_SUPPORTED, + [], + true, + ); + } + // eslint-disable-next-line no-underscore-dangle + this.#eventEmitter._emit(`${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${this.#elementName}`, { + options: metaData, + }, (response: any) => { + if (response.error) { + reject(response); + } + }); + window.addEventListener('message', (event) => { + if (event?.data?.type === `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${this.#elementName}`) { + if (event?.data?.data?.errorResponse || event?.data?.data?.error) { + printLog(`${event?.data?.data.errorResponse || event?.data?.data.error}`, MessageType.ERROR, this.#context.logLevel); + reject(event?.data?.data); + } else { + printLog(logs.infoLogs.MULTI_UPLOAD_FILES_SUCCESS, + MessageType.LOG, this.#context.logLevel); + resolve(event?.data?.data); + } + } + }); + } catch (error) { + printLog(`${error}`, MessageType.ERROR, this.#context.logLevel); + reject(error); + } + }); +} export default ComposableElement; diff --git a/src/core/external/common/iframe.ts b/src/core/external/common/iframe.ts index b59e8bc6..8f7f8a17 100644 --- a/src/core/external/common/iframe.ts +++ b/src/core/external/common/iframe.ts @@ -30,7 +30,7 @@ export default class IFrame { } mount = (domElement, elementId?: string, data?: any) => { - this.unmount(); + // this.unmount(); try { if (typeof domElement === 'string') { this.container = document.querySelector(domElement) || undefined; diff --git a/src/core/external/reveal/composable-reveal-container.ts b/src/core/external/reveal/composable-reveal-container.ts new file mode 100644 index 00000000..2d829848 --- /dev/null +++ b/src/core/external/reveal/composable-reveal-container.ts @@ -0,0 +1,474 @@ +/* eslint-disable no-plusplus */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +import bus from 'framebus'; +import sum from 'lodash/sum'; +import EventEmitter from '../../../event-emitter'; +import iframer, { setAttributes, getIframeSrc, setStyles } from '../../../iframe-libs/iframer'; +import deepClone from '../../../libs/deep-clone'; +import SkyflowError from '../../../libs/skyflow-error'; +import uuid from '../../../libs/uuid'; +import properties from '../../../properties'; +import { ContainerType } from '../../../skyflow'; +import { + Context, MessageType, +} from '../../../utils/common'; +import SKYFLOW_ERROR_CODE from '../../../utils/constants'; +import logs from '../../../utils/logs'; +import { printLog, parameterizedString } from '../../../utils/logs-helper'; +import { + validateInitConfig, + validateInputFormatOptions, + validateRevealElementRecords, +} from '../../../utils/validators'; +import { + COLLECT_FRAME_CONTROLLER, + CONTROLLER_STYLES, ELEMENT_EVENTS_TO_IFRAME, + FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT, + COMPOSABLE_REVEAL, + REVEAL_TYPES, +} from '../../constants'; +import Container from '../common/container'; + +import ComposableRevealElement from './composable-reveal-element'; +import { RevealElementInput, RevealResponse } from '../../../index-node'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; +import ComposableRevealInternalElement from './composable-reveal-internal'; +import { formatRevealElementOptions } from '../../../utils/helpers'; + +const CLASS_NAME = 'ComposableRevealContainer'; +class ComposableRevealContainer extends Container { + #containerId: string; + + #elements: Record = {}; + + #metaData: any; + + #elementGroup: any = { rows: [] }; + + #elementsList:any = []; + + #context:Context; + + #skyflowElements:any; + + #eventEmitter: EventEmitter; + + #isMounted: boolean = false; + + #options: any; + + #containerElement:any; + + type:string = ContainerType.COMPOSE_REVEAL; + + #containerMounted: boolean = false; + + #tempElements: any = {}; + + #clientDomain: string = ''; + + #isComposableFrameReady: boolean = false; + + #shadowRoot: ShadowRoot | null = null; + + #iframeID: string = ''; + + #revealRecords: IRevealElementInput[] = []; + + #getSkyflowBearerToken: () => Promise | undefined; + + constructor(options, metaData, skyflowElements, context) { + super(); + this.#containerId = uuid(); + this.#metaData = { + ...metaData, + clientJSON: { + ...metaData.clientJSON, + config: { + ...metaData.clientJSON.config, + options: { + ...metaData.clientJSON.config?.options, + ...options, + }, + }, + }, + }; + this.#getSkyflowBearerToken = metaData.getSkyflowBearerToken; + this.#skyflowElements = skyflowElements; + this.#context = context; + this.#options = options; + this.#eventEmitter = new EventEmitter(); + + this.#clientDomain = this.#metaData.clientDomain || ''; + const iframe = iframer({ + name: `${COLLECT_FRAME_CONTROLLER}:${this.#containerId}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`, + referrer: this.#clientDomain, + }); + setAttributes(iframe, { + src: getIframeSrc(), + }); + setStyles(iframe, { ...CONTROLLER_STYLES }); + printLog(parameterizedString(logs.infoLogs.CREATE_COLLECT_CONTAINER, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#containerMounted = true; + bus + // .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.#containerId, (data, callback) => { + printLog(parameterizedString(logs.infoLogs.INITIALIZE_COMPOSABLE_CLIENT, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + callback({ + client: this.#metaData.clientJSON, + context, + }); + this.#isComposableFrameReady = true; + }); + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + this.#containerId) { + this.#isComposableFrameReady = true; + } + }); + } + + create = (input: RevealElementInput, options?: IRevealElementOptions) => { + const elementId = uuid(); + validateInputFormatOptions(options); + + const elementName = `${COMPOSABLE_REVEAL}:${btoa(elementId)}`; + this.#elementsList?.push({ + name: elementName, + ...input, + elementName, + elementId, + ...formatRevealElementOptions(options ?? {}), + }); + const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements ?? {})}:${this.#containerId}:${this.#context?.logLevel}:${btoa(this.#clientDomain ?? '')}`; + return new ComposableRevealElement(elementName, + this.#eventEmitter, + controllerIframeName); + }; + + #createMultipleElement = ( + multipleElements: any, + isSingleElementAPI: boolean = false, + ) => { + try { + const elements: any[] = []; + this.#tempElements = deepClone(multipleElements); + this.#tempElements?.rows?.forEach((row) => { + row?.elements?.forEach((element) => { + const options = element ?? {}; + const { elementType } = options; + options.isMounted = false; + + options.label = element?.label; + options.skyflowID = element?.skyflowID; + + elements.push(options); + }); + }); + + this.#tempElements.elementName = isSingleElementAPI + ? elements[0].elementName + : `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}`; + if ( + isSingleElementAPI + && !this.#elements[elements[0].elementName] + && this.#hasElementName(elements[0].name) + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0].name}`], true); + } + + let element = this.#elements[this.#tempElements.elementName]; + if (element) { + if (isSingleElementAPI) { + element.update(elements[0]); + } else { + element.update(this.#tempElements); + } + } else { + const elementId = uuid(); + try { + element = new ComposableRevealInternalElement( + elementId, + this.#tempElements, + this.#metaData, + { + containerId: this.#containerId, + isMounted: this.#containerMounted, + type: this.type, + eventEmitter: this.#eventEmitter, + }, + true, + this.#context, + ); + this.#elements[this.#tempElements.elementName] = element; + this.#skyflowElements[elementId] = element; + } catch (error: any) { + printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, + MessageType.ERROR, + this.#context.logLevel); + throw error; + } + } + this.#iframeID = element.iframeName(); + return element; + } catch (error: any) { + printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, + MessageType.ERROR, + this.#context.logLevel); + throw error; + } + }; + + #hasElementName = (name: string) => { + const tempElements = Object.keys(this.#elements); + for (let i = 0; i < tempElements.length; i += 1) { + if (atob(tempElements[i].split(':')[2]) === name) { + return true; + } + } + return false; + }; + + mount = (domElement: HTMLElement | string) => { + if (!domElement) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, + ['RevealElement'], true); + } + + const { layout } = this.#options; + if (sum(layout) !== this.#elementsList.length) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISMATCH_ELEMENT_COUNT_LAYOUT_SUM, [], true); + } + let count = 0; + layout.forEach((rowCount, index) => { + this.#elementGroup.rows = [ + ...this.#elementGroup.rows, + { elements: [] }, + ]; + for (let i = 0; i < rowCount; i++) { + this.#elementGroup.rows[index].elements.push( + this.#elementsList[count], + ); + count++; + } + }); + if (this.#options.styles) { + this.#elementGroup.styles = { + ...this.#options.styles, + }; + } + if (this.#options.errorTextStyles) { + this.#elementGroup.errorTextStyles = { + ...this.#options.errorTextStyles, + }; + } + if (this.#containerMounted) { + this.#containerElement = this.#createMultipleElement(this.#elementGroup, false); + this.#containerElement.mount(domElement); + this.#isMounted = true; + } + if (domElement instanceof HTMLElement + && (domElement as HTMLElement).getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = domElement.getRootNode() as ShadowRoot; + } else if (typeof domElement === 'string') { + const element = document.getElementById(domElement); + if (element && element.getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = element.getRootNode() as ShadowRoot; + } + } + if (this.#shadowRoot !== null) { + this.#eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, (data) => { + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + data.iframeName, {}); + }); + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframeID, {}); + } + }; + + unmount = () => { + this.#containerElement.unmount(); + }; + + #emitEvent = (eventName: string, options?: Record, callback?: any) => { + if (this.#shadowRoot) { + const iframe = this.#shadowRoot.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } else { + const iframe = document.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }; + + reveal(): Promise { + this.#revealRecords = []; + if (this.#isComposableFrameReady) { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + printLog(parameterizedString(logs.infoLogs.VALIDATE_REVEAL_RECORDS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#elementsList.forEach((currentElement) => { + // if (currentElement.isClientSetError()) { + // throw new SkyflowError(SKYFLOW_ERROR_CODE.REVEAL_ELEMENT_ERROR_STATE); + // } + if (!currentElement.skyflowID) { + this.#revealRecords.push(currentElement); + } + }); + validateRevealElementRecords(this.#revealRecords); + const elementIds:{ frameId:string, token:string }[] = []; + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: element.name, + token: element.token, + }); + }); + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + this.#containerId, + { + data: { + type: REVEAL_TYPES.REVEAL, + containerId: this.#containerId, + elementIds, + }, + clientConfig: { + vaultURL: this.#metaData?.clientJSON?.config?.vaultURL, + vaultID: this.#metaData?.clientJSON?.config?.vaultID, + authToken, + }, + context: this.#context, + }, + ); + + window?.addEventListener('message', (event) => { + if (event?.data?.type + === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.#containerId) { + const revealData = event?.data?.data; + if (revealData?.errors) { + printLog( + parameterizedString(logs?.errorLogs?.FAILED_REVEAL), + MessageType.ERROR, + this.#context?.logLevel, + ); + reject(revealData); + } else { + printLog( + parameterizedString(logs?.infoLogs?.REVEAL_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context?.logLevel, + ); + resolve(revealData); + } + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + } + }); + } + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + printLog(parameterizedString(logs.infoLogs.VALIDATE_REVEAL_RECORDS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#elementsList.forEach((currentElement) => { + // if (currentElement.isClientSetError()) { + // throw new SkyflowError(SKYFLOW_ERROR_CODE.REVEAL_ELEMENT_ERROR_STATE); + // } + if (!currentElement.skyflowID) { + this.#revealRecords.push(currentElement); + } + }); + validateRevealElementRecords(this.#revealRecords); + const elementIds:{ frameId:string, token:string }[] = []; + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: element.name, + token: element.token, + }); + }); + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + window.addEventListener('message', (messagEevent) => { + if (messagEevent.data.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + this.#containerId) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + this.#containerId, { + data: { + type: REVEAL_TYPES.REVEAL, + containerId: this.#containerId, + elementIds, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + context: this.#context, + }, + ); + window.addEventListener('message', (event) => { + if (event.data.type + === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.#containerId) { + const revealData = event.data.data; + if (revealData.errors) { + printLog(parameterizedString(logs.errorLogs.FAILED_REVEAL), + MessageType.ERROR, this.#context.logLevel); + reject(revealData); + } else { + printLog(parameterizedString(logs.infoLogs.REVEAL_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + resolve(revealData); + } + } + }); + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + } + }); + } +} +export default ComposableRevealContainer; diff --git a/src/core/external/reveal/composable-reveal-element.ts b/src/core/external/reveal/composable-reveal-element.ts new file mode 100644 index 00000000..0f3f328d --- /dev/null +++ b/src/core/external/reveal/composable-reveal-element.ts @@ -0,0 +1,66 @@ +import EventEmitter from '../../../event-emitter'; +import { ContainerType } from '../../../skyflow'; +import { EventName, RenderFileResponse } from '../../../utils/common'; +import { ELEMENT_EVENTS_TO_IFRAME, REVEAL_ELEMENT_OPTIONS_TYPES } from '../../constants'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; + +class ComposableRevealElement { + #elementName: string; + + #eventEmitter: EventEmitter; + + #iframeName: string; + + type: string = ContainerType?.COMPOSABLE ?? 'COMPOSABLE'; + + #isMounted: boolean = false; + + constructor(name, eventEmitter, iframeName) { + this.#elementName = name ?? ''; + this.#iframeName = iframeName ?? ''; + this.#eventEmitter = eventEmitter; + this.#eventEmitter?.on?.(`${EventName?.READY ?? 'READY'}:${this.#elementName}`, () => { + this.#isMounted = true; + }); + } + + iframeName(): string { + return this.#iframeName ?? ''; + } + + getID(): string { + return this.#elementName ?? ''; + } + + renderFile(): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line no-underscore-dangle + this.#eventEmitter?._emit?.( + `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST ?? ''}:${this.#elementName}`, + {}, + (response) => { + if (response?.errors) { + reject(response); + } else if (response?.error) { + reject({ errors: response?.error }); + } else { + resolve(response); + } + }, + ); + }); + } + + update = (options: IRevealElementInput | IRevealElementOptions) => { + // eslint-disable-next-line no-underscore-dangle + this.#eventEmitter?._emit?.( + `${ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS}:${this.#elementName}`, + { + options: options as IRevealElementInput | IRevealElementOptions, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + }, + ); + }; +} + +export default ComposableRevealElement; diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts new file mode 100644 index 00000000..a5584755 --- /dev/null +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -0,0 +1,610 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import bus from 'framebus'; +import SkyflowError from '../../../libs/skyflow-error'; +import uuid from '../../../libs/uuid'; +import { Context, MessageType, RenderFileResponse } from '../../../utils/common'; +import SKYFLOW_ERROR_CODE from '../../../utils/constants'; +import { + ELEMENT_EVENTS_TO_IFRAME, + ELEMENT_EVENTS_TO_CONTAINER, + REVEAL_ELEMENT_OPTIONS_TYPES, + METRIC_TYPES, + ELEMENT_EVENTS_TO_CLIENT, + EVENT_TYPES, + REVEAL_TYPES, + COMPOSABLE_REVEAL, +} from '../../constants'; +import IFrame from '../common/iframe'; +import SkyflowElement from '../common/skyflow-element'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; +import { + pushElementEventWithTimeout, + updateMetricObjectValue, +} from '../../../metrics'; +import logs from '../../../utils/logs'; +import { parameterizedString, printLog } from '../../../utils/logs-helper'; +import properties from '../../../properties'; +import { validateInitConfig, validateRenderElementRecord } from '../../../utils/validators'; +import EventEmitter from '../../../event-emitter'; +import { formatRevealElementOptions } from '../../../utils/helpers'; + +const CLASS_NAME = 'RevealElementInteranalElement'; + +export interface RevealComposableGroup{ + record: IRevealElementInput + options: IRevealElementOptions +} + +class ComposableRevealInternalElement extends SkyflowElement { + #iframe: IFrame; + + #metaData: any; + + #recordData: any; + + #containerId: any; + + #isMounted:boolean = false; + + #isClientSetError:boolean = false; + + #context: Context; + + #elementId: string; + + #readyToMount: boolean = false; + + #eventEmitter: EventEmitter; + + #isFrameReady: boolean; + + #domSelecter: string; + + #clientId: string; + + #isSkyflowFrameReady: boolean = false; + + #isSingleElementAPI: boolean; + + #shadowRoot: ShadowRoot | null = null; + + #getSkyflowBearerToken: () => Promise | undefined; + + #composableIframeName!: string; + + #isComposableFrameReady: boolean = false; + + constructor(elementId: string, + recordGroup: RevealComposableGroup[], + metaData: any, container: any, isSingleElementAPI: boolean = false, + context: Context) { + super(); + this.#elementId = elementId; + this.#metaData = metaData; + this.#clientId = this.#metaData?.uuid; + this.#isSingleElementAPI = isSingleElementAPI; + this.#recordData = recordGroup; + this.#containerId = container?.containerId; + this.#readyToMount = container?.isMounted ?? true; + this.#eventEmitter = container?.eventEmitter; + this.#context = context; + + this.#iframe = new IFrame( + `${COMPOSABLE_REVEAL}:${btoa(uuid())}`, + metaData, + this.#containerId, + this.#context?.logLevel, + ); + + this.#domSelecter = ''; + this.#isFrameReady = false; + this.#readyToMount = true; + this.#getSkyflowBearerToken = metaData?.getSkyflowBearerToken; + this.#isSkyflowFrameReady = metaData?.skyflowContainer?.isControllerFrameReady ?? false; + + bus?.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe?.name, (data) => { + this.#iframe?.setIframeHeight(data?.height); + }); + + window?.addEventListener('message', (event) => { + if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + this.#containerId) { + this.#isComposableFrameReady = true; + } + }); + + window?.addEventListener('message', (event) => { + if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + this.#iframe?.name) { + this.#iframe?.setIframeHeight(event?.data?.data?.height); + } + }); + + // eslint-disable-next-line max-len + if (this.#recordData?.rows) { + this.setupRenderFileEventListener(this.getRecordData()?.rows); + } + } + + private setupRenderFileEventListener(rows: any[]): void { + if (!rows?.length) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_REVEAL_COMPOSABLE_INPUT, ['COMPOSABLE_REVEAL'], true); + } + + try { + rows?.forEach((row, rowIndex) => { + row?.elements?.forEach((element: any, elementIndex: number) => { + if (!element?.name) return; + this.#eventEmitter?.on( + `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST}:${element?.name}`, + (data, callback) => { + this.renderFile(element)?.then((response) => { + callback?.(response); + })?.catch((error) => { + callback?.({ error }); + }); + }, + ); + this.#eventEmitter?.on( + `${ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS}:${element?.name}`, + (data) => { + if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS) { + // make this change in original elememt that is inside rows + const updatedElement = { + ...element, + ...data.options, + ...formatRevealElementOptions(data.options as IRevealElementOptions), + }; + + // Update element in this.#recordData.rows structure + if (this.#recordData?.rows?.[rowIndex]?.elements?.[elementIndex]) { + this.#recordData.rows[rowIndex].elements[elementIndex] = updatedElement; + } + + // Update local element reference + element = updatedElement; + + // Call update method + this.update(data.options, element); + } + }, + ); + }); + }); + } catch (error) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_REVEAL_COMPOSABLE_INPUT, ['COMPOSABLE_REVEAL'], true); + } + } + + getID() { + return this.#elementId; + } + + mount(domElementSelector: HTMLElement | string) { + if (!domElementSelector) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['RevealElement'], true); + } + updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElementSelector); + if ( + this.#metaData?.clientJSON?.config?.options?.trackMetrics + && this.#metaData.clientJSON.config?.options?.trackingKey + ) { + pushElementEventWithTimeout(this.#elementId); + } + + this.#readyToMount = true; + if (this.#readyToMount) { + this.#iframe.mount(domElementSelector, undefined, { + record: JSON.stringify({ + ...this.#metaData, + record: this.#recordData, + context: this.#context, + containerId: this.#containerId, + }), + }); + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + if (this.#recordData.skyflowID) { + bus + // .target(location.origin) + .emit( + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.#containerId, + { + skyflowID: this.#recordData.skyflowID, + containerId: this.#containerId, + }, + ); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.MOUNTED); + } else { + bus + // .target(location.origin) + .emit( + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.#containerId, + { + id: this.#recordData.token, + containerId: this.#containerId, + }, + ); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.MOUNTED); + } + if (Object.prototype.hasOwnProperty.call(this.#recordData, 'skyflowID')) { + bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + } + }); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.READY); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_START_TIME, Date.now()); + } + if (domElementSelector instanceof HTMLElement + && (domElementSelector as HTMLElement).getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = domElementSelector.getRootNode() as ShadowRoot; + } else if (typeof domElementSelector === 'string') { + const element = document.getElementById(domElementSelector); + if (element && element.getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = element.getRootNode() as ShadowRoot; + } + } + } + + #emitEvent = (eventName: string, options?: Record) => { + if (this.#shadowRoot) { + const iframe = this.#shadowRoot?.getElementById(this.#iframe?.name) as HTMLIFrameElement; + iframe?.contentWindow?.postMessage({ + name: eventName, + ...options, + }, properties?.IFRAME_SECURE_ORIGIN); + } else { + const iframe = document?.getElementById(this.#iframe?.name) as HTMLIFrameElement; + iframe?.contentWindow?.postMessage({ + name: eventName, + ...options, + }, properties?.IFRAME_SECURE_ORIGIN); + } + }; + + renderFile(recordData): Promise { + let altText = ''; + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + altText = recordData.altText; + } + this.setAltText('loading...'); + const loglevel = this.#context.logLevel; + if (this.#isComposableFrameReady) { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), + MessageType.LOG, + loglevel); + validateRenderElementRecord(recordData); + + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + recordData.name, + { + data: { + type: REVEAL_TYPES.RENDER_FILE, + containerId: this.#containerId, + iframeName: recordData.name, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + }, + ); + window?.addEventListener('message', (event) => { + if (event?.data && event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + + recordData.name) { + if (event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + const revealData = event?.data?.data?.result; + if (revealData?.error || revealData?.errors) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, + this.#context.logLevel); + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + this.setAltText(altText); + } + reject(revealData?.error || revealData?.errors); + } else { + printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, + CLASS_NAME, recordData.skyflowID), + MessageType.LOG, this.#context.logLevel); + resolve(revealData); + } + } + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), + MessageType.LOG, loglevel); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, + loglevel); + reject(err); + } + }); + } + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), + MessageType.LOG, + loglevel); + validateRenderElementRecord(recordData); + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + this.#containerId) { + this.#isMounted = true; + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + recordData.name, + { + data: { + type: REVEAL_TYPES.RENDER_FILE, + containerId: this.#containerId, + iframeName: recordData.name, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + }, + ); + window.addEventListener('message', (event1) => { + if (event1?.data + && event1?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + + this.#iframe.name) { + if (event1?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + const revealData = event1?.data?.data?.result; + if (revealData?.error) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, + this.#context.logLevel); + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + this.setAltText(altText); + } + reject(revealData); + } else { + // eslint-disable-next-line max-len + printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, + CLASS_NAME, recordData.skyflowID), + MessageType.LOG, this.#context.logLevel); + resolve(revealData); + } + } + } + }); + }).catch((err:any) => { + printLog(`${err?.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + } + }); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), + MessageType.LOG, loglevel); + } catch (err: any) { + printLog(`Error: ${err?.message}`, MessageType.ERROR, + loglevel); + reject(err); + } + }); + } + + iframeName(): string { + return this.#iframe.name; + } + + isMounted():boolean { + return this.#isMounted; + } + + hasToken():boolean { + if (this.#recordData.token) return true; + return false; + } + + isClientSetError():boolean { + return this.#isClientSetError; + } + + getRecordData() { + return this.#recordData; + } + + setErrorOverride(clientErrorText: string) { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: true, + clientErrorText, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: true, + clientErrorText, + }); + }); + } + this.#isClientSetError = true; + } + + setError(clientErrorText:string) { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: true, + clientErrorText, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: true, + clientErrorText, + }); + }); + } + this.#isClientSetError = true; + } + + resetError() { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: false, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: false, + }); + }); + } + this.#isClientSetError = false; + } + + setAltText(altText:string) { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: altText, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: altText, + }); + }); + } + } + + clearAltText() { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: null, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: null, + }); + }); + } + } + + setToken(token:string) { + this.#recordData = { + ...this.#recordData, + token, + }; + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.TOKEN, + updatedValue: token, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.TOKEN, + updatedValue: token, + }); + }); + } + } + + unmount() { + if (this.#recordData.skyflowID) { + this.#isMounted = false; + this.#iframe.container?.remove(); + } + this.#isMounted = false; + this.#iframe.unmount(); + } + + update(options: IRevealElementInput | IRevealElementOptions, record) { + if (this.#isComposableFrameReady) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: options, + }, + ); + } else { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + this.#containerId) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: options, + }, + ); + } + }); + } + } +} + +export default ComposableRevealInternalElement; diff --git a/src/core/internal/composable-frame-element-init.ts b/src/core/internal/composable-frame-element-init.ts new file mode 100644 index 00000000..51210e5a --- /dev/null +++ b/src/core/internal/composable-frame-element-init.ts @@ -0,0 +1,369 @@ +import injectStylesheet from 'inject-stylesheet'; +import bus from 'framebus'; +import { getValueAndItsUnit } from '../../libs/element-options'; +import { getFlexGridStyles } from '../../libs/styles'; +import { ContainerType } from '../../skyflow'; +import { + Context, IRevealRecordComposable, +} from '../../utils/common'; +import { + getContainerType, +} from '../../utils/helpers'; +import { + ALLOWED_MULTIPLE_FIELDS_STYLES, + ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ERROR_TEXT_STYLES, REVEAL_TYPES, STYLE_TYPE, +} from '../constants'; +import IFrameFormElement from './iframe-form'; +import getCssClassesFromJss, { generateCssWithoutClass } from '../../libs/jss-styles'; +import FrameElement from '.'; +import Client from '../../client'; +import RevealFrame from './reveal/reveal-frame'; +import { + fetchRecordsByTokenIdComposable, formatRecordsForClientComposable, +} from '../../core-utils/reveal'; + +export default class RevealComposableFrameElementInit { + iframeFormElement: IFrameFormElement | undefined; + + clientMetaData: any; + + #domForm: HTMLFormElement; + + frameElement!: FrameElement; + + private static frameEle?: any; + + containerId: string; + + group: any; + + frameList: FrameElement[] = []; + + iframeFormList: IFrameFormElement[] = []; + + #client!: Client; + + #context!: Context; + + revealFrameList: any[] = []; + + rootDiv: HTMLDivElement; + + constructor() { + this.containerId = ''; + this.#domForm = document?.createElement('form'); + this.#domForm.action = '#'; + this.#domForm.onsubmit = (event) => { + event?.preventDefault(); + }; + + this.rootDiv = document?.createElement('div'); + this.updateGroupData(); + this.createContainerDiv(this.group); + + window?.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + + this.containerId && event?.data?.data?.type === REVEAL_TYPES.REVEAL) { + this.#context = event?.data?.context; + const data = event?.data?.data ?? {}; + const elementIds = data?.elementIds ?? []; + const revealDataInput: IRevealRecordComposable[] = []; + this.#client = new Client(event?.data?.clientConfig ?? {}, {}); + + elementIds?.forEach((element) => { + this.revealFrameList?.forEach((revealFrame) => { + const data2 = revealFrame?.getData?.(); + if (data2?.name === element?.frameId) { + if (data2 && !data2?.skyflowID) { + const revealRecord: IRevealRecordComposable = { + token: data2?.token ?? '', + redaction: data2?.redaction, + iframeName: data2?.name ?? '', + }; + revealDataInput?.push(revealRecord); + } + } + }); + }); + + this.revealData(revealDataInput, this.containerId, event?.data?.clientConfig?.authToken) + ?.then((revealResponse: any) => { + if (revealResponse?.records?.length > 0) { + const formattedRecord = formatRecordsForClientComposable(revealResponse); + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.containerId, + data: formattedRecord, + }, + this.clientMetaData?.clientDomain, + ); + + revealResponse?.records?.forEach((record: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === record?.frameId) { + revealFrame?.responseUpdate?.(record); + } + }); + }); + } + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + }) + ?.catch((error) => { + const formattedRecord = formatRecordsForClientComposable(error); + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.containerId, + data: formattedRecord, + }, + this.clientMetaData?.clientDomain, + ); + + error?.records?.forEach((record: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === record?.frameId) { + revealFrame?.responseUpdate?.(record); + } + }); + }); + + error?.errors?.forEach((error1: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === error1?.frameId) { + revealFrame?.responseUpdate?.(error1); + } + }); + }); + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + }); + } + }); + + bus?.emit(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId, {}, (data: any) => { + this.#context = data?.context; + if (data?.client?.config) { + data.client.config = { + ...data?.client?.config, + }; + } + this.#client = Client?.fromJSON?.(data?.client); + }); + } + + updateGroupData = () => { + const url = window?.location?.href ?? ''; + const configIndex = url?.indexOf('?') ?? -1; + const encodedString = configIndex !== -1 ? decodeURIComponent(url?.substring(configIndex + 1)) : ''; + const parsedRecord = encodedString ? JSON.parse(atob(encodedString)) : {}; + this.clientMetaData = parsedRecord?.clientJSON?.metaData; + this.group = parsedRecord?.record; + this.containerId = parsedRecord?.containerId ?? ''; + this.#context = parsedRecord?.context; + }; + + static startFrameElement = () => { + RevealComposableFrameElementInit.frameEle = new RevealComposableFrameElementInit(); + }; + + revealData(revealRecords: IRevealRecordComposable[], containerId, authToken) { + return new Promise((resolve, reject) => { + fetchRecordsByTokenIdComposable(revealRecords, this.#client, authToken)?.then( + (resolvedResult) => { + resolve(resolvedResult); + }, + (rejectedResult) => { + reject(rejectedResult); + }, + ); + }); + } + + createContainerDiv = (newGroup) => { + this.group = newGroup; + const { + rows = [], + styles, + errorTextStyles, + } = this.group ?? {}; + + const isComposableContainer = getContainerType(window?.name) === ContainerType?.COMPOSABLE; + this.group.spacing = getValueAndItsUnit(this.group?.spacing)?.join('') ?? ''; + this.rootDiv = document?.createElement('div'); + this.rootDiv.className = 'container'; + + const containerStylesByClassName = getFlexGridStyles({ + 'align-items': this.group?.alignItems ?? 'stretch', + 'justify-content': this.group?.justifyContent ?? 'flex-start', + spacing: this.group?.spacing, + }); + + injectStylesheet?.injectWithAllowlist( + { + [`.${this.rootDiv?.className}`]: containerStylesByClassName, + }, + ALLOWED_MULTIPLE_FIELDS_STYLES, + ); + + let count = 0; + rows?.forEach((row, rowIndex) => { + row.spacing = getValueAndItsUnit(row?.spacing)?.join('') ?? ''; + const rowDiv = document?.createElement('div'); + rowDiv.id = `row-${rowIndex}`; + + const intialRowStyles = { + 'align-items': row?.alignItems ?? 'stretch', + 'justify-content': row?.justifyContent ?? 'flex-start', + spacing: row?.spacing, + padding: this.group?.spacing, + }; + + const rowStylesByClassName = getFlexGridStyles(intialRowStyles); + let errorTextElement; + + if (isComposableContainer) { + rowDiv.className = `${rowDiv?.id} SkyflowElement-${rowDiv?.id}-base`; + const rowStyles = { + [STYLE_TYPE?.BASE]: { + ...(styles?.[STYLE_TYPE?.BASE] ?? {}), + }, + }; + + getCssClassesFromJss?.(rowStyles, `${rowDiv?.id}`); + + errorTextElement = document?.createElement('span'); + errorTextElement.id = `${rowDiv?.id}-error`; + errorTextElement.className = 'SkyflowElement-row-error-base'; + + const errorStyles = { + [STYLE_TYPE?.BASE]: { + ...ERROR_TEXT_STYLES, + ...(errorTextStyles?.[STYLE_TYPE?.BASE] ?? {}), + }, + }; + + getCssClassesFromJss?.(errorStyles, 'row-error'); + if (errorTextStyles?.[STYLE_TYPE?.GLOBAL]) { + generateCssWithoutClass?.(errorTextStyles?.[STYLE_TYPE?.GLOBAL]); + } + } else { + rowDiv.className = `row-${rowIndex}`; + injectStylesheet?.injectWithAllowlist( + { + [`.${rowDiv?.className}`]: rowStylesByClassName, + }, + ALLOWED_MULTIPLE_FIELDS_STYLES, + ); + } + + row?.elements?.forEach((element) => { + const elementDiv = document?.createElement('div'); + elementDiv.className = `element-${count}`; + elementDiv.id = `${rowDiv?.id}:element-${count}`; + count += 1; + + const elementStylesByClassName = { + padding: row?.spacing, + }; + + injectStylesheet?.injectWithAllowlist( + { + [`.${elementDiv?.className}`]: elementStylesByClassName, + }, + ALLOWED_MULTIPLE_FIELDS_STYLES, + ); + + const revealFrame = new RevealFrame(element, this.#context, + this.containerId, elementDiv); + this.revealFrameList?.push(revealFrame); + rowDiv?.append(elementDiv); + }); + + this.rootDiv?.append(rowDiv); + if (isComposableContainer) { + this.rootDiv?.append(errorTextElement); + } + }); + + if (this.#domForm) { + this.#domForm.innerHTML = ''; + document.body.innerHTML = ''; + this.#domForm?.append(this.rootDiv); + document?.body?.append(this.#domForm); + } + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.containerId, + data: { + name: window?.name, + }, + }, + this.clientMetaData.clientDomain, + ); + + bus?.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window?.name, (data, callback) => { + callback?.({ + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }); + }); + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData.clientDomain, + ); + + window?.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window?.name) { + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + } + if (event?.data?.type + === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name) { + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData.clientDomain, + ); + } + }); + }; +} diff --git a/src/core/internal/frame-element-init.ts b/src/core/internal/frame-element-init.ts index d2c35e6b..13c89945 100644 --- a/src/core/internal/frame-element-init.ts +++ b/src/core/internal/frame-element-init.ts @@ -1,17 +1,35 @@ import injectStylesheet from 'inject-stylesheet'; import bus from 'framebus'; +import get from 'lodash/get'; import { getValueAndItsUnit, validateAndSetupGroupOptions } from '../../libs/element-options'; import { getFlexGridStyles } from '../../libs/styles'; import { ContainerType } from '../../skyflow'; -import { Context, Env, LogLevel } from '../../utils/common'; -import { getContainerType } from '../../utils/helpers'; +import { + Context, Env, LogLevel, + MessageType, +} from '../../utils/common'; +import { + fileValidation, generateUploadFileName, getContainerType, vaildateFileName, +} from '../../utils/helpers'; import { ALLOWED_MULTIPLE_FIELDS_STYLES, - ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ERROR_TEXT_STYLES, STYLE_TYPE, + COLLECT_TYPES, + ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, ERROR_TEXT_STYLES, STYLE_TYPE, } from '../constants'; import IFrameFormElement from './iframe-form'; import getCssClassesFromJss, { generateCssWithoutClass } from '../../libs/jss-styles'; import FrameElement from '.'; +import { + checkForElementMatchRule, checkForValueMatch, constructElementsInsertReq, + constructInsertRecordRequest, insertDataInCollect, + updateRecordsBySkyflowIDComposable, +} from '../../core-utils/collect'; +import SkyflowError from '../../libs/skyflow-error'; +import SKYFLOW_ERROR_CODE from '../../utils/constants'; +import Client from '../../client'; +import { printLog } from '../../utils/logs-helper'; + +const set = require('set-value'); export default class FrameElementInit { iframeFormElement: IFrameFormElement | undefined; @@ -30,9 +48,15 @@ export default class FrameElementInit { group: any; + frameList: FrameElement[] = []; + + iframeFormList: IFrameFormElement[] = []; + + #client!: Client; + constructor() { // this.createIframeElement(frameName, label, skyflowID, isRequired); - this.context = { logLevel: LogLevel.ERROR, env: Env.PROD }; // client level + this.context = { logLevel: LogLevel.INFO, env: Env.PROD }; // client level this.containerId = ''; this.#domForm = document.createElement('form'); this.#domForm.action = '#'; @@ -41,8 +65,571 @@ export default class FrameElementInit { }; this.updateGroupData(); this.createContainerDiv(this.group); + bus + // .target(this.clientMetaData.clientDomain) + .emit(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId, {}, (data: any) => { + data.client.config = { + ...data.client.config, + }; + this.#client = Client.fromJSON(data.client) as any; + }); + + window.addEventListener('message', this.handleCollectCall); } + private handleCollectCall = (event: MessageEvent) => { + this.iframeFormList.forEach((inputElement) => { + if (inputElement) { + if (inputElement.fieldType + === ELEMENTS.MULTI_FILE_INPUT.name) { + if (event?.data && event?.data?.name === `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${inputElement.iFrameName}`) { + this.#client = Client.fromJSON(event?.data?.clientConfig); + this.multipleUploadFiles(inputElement, event?.data?.clientConfig, event?.data?.options) + ?.then((response: any) => { + window?.parent.postMessage({ + type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${inputElement.iFrameName}`, + data: response, + }, this.clientMetaData?.clientDomain); + }).catch((error) => { + window?.parent.postMessage({ + type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${inputElement.iFrameName}`, + data: error, + }, this.clientMetaData?.clientDomain); + }); + } + } + } + }); + + // if (event.origin === this.clientMetaData.clientDomain) { + if (event.data && event.data.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + + this.containerId) { + if (event.data.data && event.data.data.type === COLLECT_TYPES.COLLECT) { + this.tokenize(event.data.data, event.data.clientConfig) + .then((response: any) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.containerId, + data: response, + }, this.clientMetaData.clientDomain); + }) + .catch((error) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.containerId, + data: error, + }, this.clientMetaData.clientDomain); + }); + } else if (event.data.data && event.data.data.type === COLLECT_TYPES.FILE_UPLOAD) { + this.parallelUploadFiles(event.data.data, event.data.clientConfig) + .then((response: any) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId, + data: response, + }, this.clientMetaData.clientDomain); + }) + .catch((error) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId, + data: error, + }, this.clientMetaData.clientDomain); + }); + } + } + if (event.data.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId) { + const data = event.data; + data.client.config = { + ...data.client.config, + }; + this.#client = Client.fromJSON(data.client) as any; + } + // } + }; + + private parallelUploadFiles = (options, config) => new Promise((rootResolve, rootReject) => { + const promises: Promise[] = []; + this.iframeFormList.forEach((inputElement) => { + let res: Promise; + if (inputElement) { + if ( + inputElement.fieldType + === ELEMENTS.FILE_INPUT.name + ) { + res = this.uploadFiles(inputElement, config); + promises.push(res); + } + } + }); + Promise.allSettled( + promises, + ).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + resultSet.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value !== undefined && result.value !== null) { + if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { + errorResponse.push(result.value); + } else { + const response = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response); + } + } + } else if (result.status === 'rejected') { + errorResponse.push(result.reason); + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }); + + uploadFiles = (fileElement, clientConfig) => { + this.#client = new Client(clientConfig, {}); + if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); + const fileUploadObject: any = {}; + + const { + state, tableName, skyflowID, onFocusChange, preserveFileName, + } = fileElement; + + if (state.isRequired) { + onFocusChange(false); + } + try { + fileValidation(state.value, state.isRequired, fileElement); + } catch (err) { + return Promise.reject(err); + } + + const validatedFileState = fileValidation(state.value, state.isRequired, fileElement); + + if (!validatedFileState) { + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true)); + } + fileUploadObject[state.name] = state.value; + + const formData = new FormData(); + + const column = Object.keys(fileUploadObject)[0]; + + const value: Blob = Object.values(fileUploadObject)[0] as Blob; + + if (preserveFileName) { + const isValidFileName = vaildateFileName(state.value.name); + if (!isValidFileName) { + return Promise.reject( + new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true), + ); + } + formData.append(column, value); + } else { + const generatedFileName = generateUploadFileName(state.value.name); + formData.append(column, new File([value], generatedFileName, { type: state.value.type })); + } + + const client = this.#client; + const sendRequest = () => new Promise((rootResolve, rootReject) => { + client + .request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}/${skyflowID}/files`, + headers: { + authorization: `Bearer ${clientConfig.authToken}`, + 'content-type': 'multipart/form-data', + }, + }) + .then((response: any) => { + rootResolve(response); + }) + .catch((error) => { + rootReject(error); + }); + }); + + return new Promise((resolve, reject) => { + sendRequest() + .then((res) => resolve(res)) + .catch((err) => { + reject(err); + }); + }); + }; + + private tokenize = (options, clientConfig: any) => { + let errorMessage = ''; + const insertRequestObject: any = {}; + const updateRequestObject: any = {}; + + this.iframeFormList.forEach((inputElement) => { + if (inputElement) { + if (inputElement) { + if ( + inputElement.fieldType + !== ELEMENTS.FILE_INPUT.name && inputElement.fieldType + !== ELEMENTS.MULTI_FILE_INPUT.name + ) { + const { + // eslint-disable-next-line max-len + state, doesClientHasError, clientErrorText, errorText, onFocusChange, validations, + setValue, + } = inputElement; + if (state.isRequired || !state.isValid) { + onFocusChange(false); + } + if (validations + && checkForElementMatchRule(validations) + && checkForValueMatch(validations, inputElement)) { + setValue(state.value); + onFocusChange(false); + } + if (!state.isValid || !state.isComplete) { + if (doesClientHasError) { + errorMessage += `${state.name}:${clientErrorText}`; + } else { errorMessage += `${state.name}:${errorText} `; } + } + } + } + } + }); + + // return for error + if (errorMessage.length > 0) { + // eslint-disable-next-line max-len + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.COMPLETE_AND_VALID_INPUTS, [`${errorMessage}`], true)); + } + // eslint-disable-next-line consistent-return + this.iframeFormList.forEach((inputElement) => { + if (inputElement) { + const { + state, tableName, validations, skyflowID, + } = inputElement; + if (tableName) { + if ( + inputElement.fieldType + !== ELEMENTS.FILE_INPUT.name && inputElement.fieldType + !== ELEMENTS.MULTI_FILE_INPUT.name + ) { + if ( + inputElement.fieldType + === ELEMENTS.checkbox.name + ) { + if (insertRequestObject[state.name]) { + insertRequestObject[state.name] = `${insertRequestObject[state.name]},${state.value + }`; + } else { + insertRequestObject[state.name] = state.value; + } + } else if (insertRequestObject[tableName] && !(skyflowID === '') && skyflowID === undefined) { + if (get(insertRequestObject[tableName], state.name) + && !(validations && checkForElementMatchRule(validations))) { + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT, + [state.name, tableName], true)); + } + set( + insertRequestObject[tableName], + state.name, + inputElement.getUnformattedValue(), + ); + } else if (skyflowID || skyflowID === '') { + if (skyflowID === '' || skyflowID === null) { + return Promise.reject(new SkyflowError( + SKYFLOW_ERROR_CODE.EMPTY_SKYFLOW_ID_IN_ADDITIONAL_FIELDS, + )); + } + if (updateRequestObject[skyflowID]) { + set( + updateRequestObject[skyflowID], + state.name, + inputElement.getUnformattedValue(), + ); + } else { + updateRequestObject[skyflowID] = {}; + set( + updateRequestObject[skyflowID], + state.name, + inputElement.getUnformattedValue(), + ); + set( + updateRequestObject[skyflowID], + 'table', + tableName, + ); + } + } else { + insertRequestObject[tableName] = {}; + set( + insertRequestObject[tableName], + state.name, + inputElement.getUnformattedValue(), + ); + } + } + } + } + }); + let finalInsertRequest; + let finalInsertRecords; + let finalUpdateRecords; + try { + [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq( + insertRequestObject, updateRequestObject, options.options, + ); + finalInsertRequest = constructInsertRecordRequest(finalInsertRecords, options.options); + } catch (error:any) { + return Promise.reject({ + error: error?.message, + }); + } + this.#client = new Client(clientConfig, {}); + const client = this.#client; + const sendRequest = () => new Promise((rootResolve, rootReject) => { + const insertPromiseSet: Promise[] = []; + + // const clientId = client.toJSON()?.metaData?.uuid || ''; + // getAccessToken(clientId).then((authToken) => { + if (finalInsertRequest.length !== 0) { + insertPromiseSet.push( + insertDataInCollect(finalInsertRequest, + client, options, finalInsertRecords, clientConfig.authToken as string), + ); + } + if (finalUpdateRecords.updateRecords.length !== 0) { + insertPromiseSet.push( + updateRecordsBySkyflowIDComposable( + finalUpdateRecords, client, options, clientConfig.authToken as string, + ), + ); + } + if (insertPromiseSet.length !== 0) { + Promise.allSettled(insertPromiseSet).then((resultSet: any) => { + const recordsResponse: any[] = []; + const errorsResponse: any[] = []; + + resultSet.forEach((result: + { status: string; value: any; reason?: any; }) => { + if (result.status === 'fulfilled') { + if (result.value.records !== undefined && Array.isArray(result.value.records)) { + result.value.records.forEach((record) => { + recordsResponse.push(record); + }); + } + if (result.value.errors !== undefined && Array.isArray(result.value.errors)) { + result.value.errors.forEach((error) => { + errorsResponse.push(error); + }); + } + } else { + if (result.reason?.records !== undefined && Array.isArray(result.reason?.records)) { + result.reason.records.forEach((record) => { + recordsResponse.push(record); + }); + } + if (result.reason?.errors !== undefined && Array.isArray(result.reason?.errors)) { + result.reason.errors.forEach((error) => { + errorsResponse.push(error); + }); + } + } + }); + if (errorsResponse.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse.length === 0) rootReject({ errors: errorsResponse }); + else rootReject({ records: recordsResponse, errors: errorsResponse }); + }); + } + // }).catch((err) => { + // rootReject({ + // error: err, + // }); + // }); + }); + + return new Promise((resolve, reject) => { + sendRequest() + .then((res) => resolve(res)) + .catch((err) => reject(err)); + }); + }; + + // eslint-disable-next-line consistent-return + private multipleUploadFiles = + (fileElement: IFrameFormElement, + clientConfig, metaData) => new Promise((rootResolve, rootReject) => { + this.#client = new Client(clientConfig, {}); + if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); + const { + state, tableName, onFocusChange, preserveFileName, + } = fileElement; + if (state.isRequired) { + onFocusChange(false); + } + + if (fileElement.state.value === undefined || fileElement.state.value === null || fileElement.state.value === '') { + rootReject({ error: 'No files selected' }); + return; + } + const files = state.value instanceof FileList + ? Array.from(state.value) + : [state.value]; + + this.validateFiles(files, state, fileElement); + const insertRequest = this.createInsertRequest(files.length, metaData); + this.insertDataCallInMultiFiles( + insertRequest, this.#client, tableName as string, clientConfig.authToken as string, + ).then((response: any) => { + const skyflowIDs = this.extractSkyflowIDs(response); + if (skyflowIDs.length === 0) { + rootReject({ error: 'No skyflow IDs returned from insert data' }); + return; + } + const promises: Promise[] = []; + + files.forEach((file, index) => { + const fileUploadObject: any = {}; + fileUploadObject[state.name] = file; + const formData = new FormData(); + const column = Object.keys(fileUploadObject)[0]; + const value: Blob = Object.values(fileUploadObject)[0] as Blob; + if (preserveFileName) { + formData.append(column, value); + } else { + const generatedFileName = generateUploadFileName(file.name); + formData.append(column, new File([value], generatedFileName, { type: file.type })); + } + const client = this.#client; + const promise1 = new Promise((resolve, reject) => { + client + .request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}/${skyflowIDs[index]}/files`, + headers: { + authorization: `Bearer ${clientConfig.authToken}`, + 'content-type': 'multipart/form-data', + }, + }) + .then((response1) => { + resolve(response1); + }) + .catch((error) => { + reject(error); + }); + }); + promises.push(promise1); + }); + Promise.allSettled( + promises, + ).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + resultSet.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value !== undefined && result.value !== null) { + if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { + errorResponse.push(result.value); + } else { + const response1 = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response1); + } + } + } else if (result.status === 'rejected') { + errorResponse.push({ error: result.reason }); + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }).catch((error) => { + printLog(`${error}`, MessageType.LOG, this.context?.logLevel); + rootReject({ + error: error?.error || error, + }); + }); + }); + + private validateFiles = (files: File[], state: any, fileElement: IFrameFormElement) => { + files.forEach((file) => { + // Check file validation + const validatedFileState = fileValidation(file, state.isRequired, fileElement); + if (!validatedFileState) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true); + } + + // Check filename validation + const isValidFileName = vaildateFileName(file.name); + if (!isValidFileName) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true); + } + }); + return true; + }; + + private createInsertRequest = (numberOfRequests: number, options = {}) => { + // Create basic request structure + const request = { + records: [] as Array<{ fields: Record }>, + tokenization: false, + }; + + // Add empty field objects based on number of requests + for (let i = 0; i < numberOfRequests; i += 1) { + request.records.push({ + fields: options === undefined ? {} : options, + }); + } + + return request; + }; + + private extractSkyflowIDs = (response: { records: Array<{ skyflow_id: string }> }): string[] => { + if (!response?.records || !Array.isArray(response.records)) { + return []; + } + + return response.records + .map((record) => record.skyflow_id) + .filter((id) => id !== undefined && id !== null); + }; + + private insertDataCallInMultiFiles = ( + insertRequest, + client: Client, + tableName: string, + authToken: string, + ) => new Promise((rootResolve, rootReject) => { + client + .request({ + body: { + ...insertRequest, + }, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + .then((response: any) => { + // Extract skyflow IDs from response + const skyflowIDs = this.extractSkyflowIDs(response); + rootResolve({ + ...response, + skyflowIDs, // Add extracted IDs to response + }); + }) + .catch((error) => { + rootReject(error); + }); + }); + updateGroupData = () => { const frameName = window.name; const url = window.location?.href; @@ -56,7 +643,6 @@ export default class FrameElementInit { }; this.group = parsedRecord.record; this.containerId = parsedRecord.containerId; - bus .target(this.clientMetaData.clientDomain) .on(ELEMENT_EVENTS_TO_IFRAME.SET_VALUE + frameName, (data) => { @@ -73,6 +659,7 @@ export default class FrameElementInit { ...this.clientMetaData, isRequired, }, this.context, skyflowID); + this.iframeFormList.push(this.iframeFormElement); return this.iframeFormElement; }; @@ -184,11 +771,21 @@ export default class FrameElementInit { iFrameFormElement, element, elementDiv, + this.clientMetaData.clientDomain, ); + this.frameList.push(this.frameElement); + if (isComposableContainer && errorTextElement) { iFrameFormElement.on(ELEMENT_EVENTS_TO_CLIENT.BLUR, (state) => { errorTextMap[element.elementName] = state.error; this.#updateCombinedErrorText(errorTextElement.id, errorTextMap); + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, + data: { height: rootDiv.scrollHeight, name: window.name }, + }, + this.clientMetaData.clientDomain, + ); }); } @@ -208,6 +805,24 @@ export default class FrameElementInit { bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window.name, (data, callback) => { callback({ height: rootDiv.scrollHeight, name: window.name }); }); + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, + data: { height: rootDiv.scrollHeight, name: window.name }, + }, + this.clientMetaData.clientDomain, + ); + window.addEventListener('message', (event) => { + if (event.data.name === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window.name) { + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, + data: { height: rootDiv.scrollHeight, name: window.name }, + }, + this.clientMetaData.clientDomain, + ); + } + }); }; #updateCombinedErrorText = (elementId, errorMessages) => { diff --git a/src/core/internal/iframe-form/index.ts b/src/core/internal/iframe-form/index.ts index 2ae70aac..de56c27f 100644 --- a/src/core/internal/iframe-form/index.ts +++ b/src/core/internal/iframe-form/index.ts @@ -219,6 +219,35 @@ export default class IFrameFormElement extends EventEmitter { : ELEMENT_EVENTS_TO_CLIENT.BLUR, value: { ...this.getStatus() }, }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: focus + ? ELEMENT_EVENTS_TO_CLIENT.FOCUS + : ELEMENT_EVENTS_TO_CLIENT.BLUR, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: focus + ? ELEMENT_EVENTS_TO_CLIENT.FOCUS + : ELEMENT_EVENTS_TO_CLIENT.BLUR, + name: this.iFrameName, + value: { ...this.getStatus() }, + }, + }, this.metaData.clientDomain); + } + } if (!focus) { bus.emit(ELEMENT_EVENTS_TO_CLIENT.BLUR + this.iFrameName); this._emit(ELEMENT_EVENTS_TO_CLIENT.BLUR, { @@ -282,6 +311,40 @@ export default class IFrameFormElement extends EventEmitter { this.mask = newMask; } + getFileDetails = (value: FileList | File | null): Array<{ + name: string; + size: number; + type: string; + }> => { + // Return empty array if no value + if (!value) return []; + + try { + // Handle FileList + if (value instanceof FileList) { + return Array.from(value).map((file) => ({ + name: file.name, + size: file.size, + type: file.type, + })); + } + + // Handle single File + if (value instanceof File) { + return [{ + name: value.name, + size: value.size, + type: value.type, + }]; + } + + // Return empty array for invalid input + return []; + } catch (error) { + return []; + } + }; + setValidation(validations: IValidationRule[] | undefined) { if (ELEMENTS[this.fieldType].regex) { this.regex = ELEMENTS[this.fieldType].regex; @@ -469,6 +532,21 @@ export default class IFrameFormElement extends EventEmitter { resp = false; } if (this.preserveFileName) vaildateFileNames = vaildateFileName(value.name); + } else if (this.fieldType === ElementType.MULTI_FILE_INPUT) { + const files = this.state.value instanceof FileList + ? Array.from(this.state.value) + : [this.state.value]; + for (let i = 0; i < files.length; i += 1) { + try { + resp = fileValidation(files[i], this.state.isRequired, { + allowedFileType: this.allowedFileType, + blockEmptyFiles: this.blockEmptyFiles, + }); + } catch (err) { + resp = false; + } + if (this.preserveFileName) vaildateFileNames = vaildateFileName(files[i].name); + } } else { // eslint-disable-next-line no-lonely-if if (this.regex && value) { @@ -591,6 +669,31 @@ export default class IFrameFormElement extends EventEmitter { event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } } else if ( data.options !== undefined @@ -673,23 +776,99 @@ export default class IFrameFormElement extends EventEmitter { event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } else if ( this.state.value && (this.fieldType === ELEMENTS.EXPIRATION_DATE.name || this.fieldType === ELEMENTS.EXPIRATION_MONTH.name - || this.fieldType === ELEMENTS.FILE_INPUT.name) + || this.fieldType === ELEMENTS.FILE_INPUT.name + || this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) ) { bus.emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, { name: this.iFrameName, event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } else if (!this.state.isEmpty) { bus.emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, { name: this.iFrameName, event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } this._emit(ELEMENT_EVENTS_TO_CLIENT.CHANGE, { diff --git a/src/core/internal/index.ts b/src/core/internal/index.ts index 8648eb8c..aca89ed3 100644 --- a/src/core/internal/index.ts +++ b/src/core/internal/index.ts @@ -89,15 +89,20 @@ export default class FrameElement { private selectedData?: number = undefined; + private clientDomain: string; + constructor( iFrameFormElement: IFrameFormElement, options: any, htmlDivElement: HTMLDivElement, + clientDomain: string = '', ) { + this.clientDomain = clientDomain; this.iFrameFormElement = iFrameFormElement; this.options = options; this.htmlDivElement = htmlDivElement; this.hasError = false; + this.mount(); this.iFrameFormElement.fieldName = options.column; this.iFrameFormElement.tableName = options.table; @@ -138,7 +143,6 @@ export default class FrameElement { this.inputParent = document.createElement('div'); this.inputParent.style.position = 'relative'; - const inputElement = document.createElement(type); this.domInput = inputElement; this.domInput.iFrameFormElement = this.iFrameFormElement; @@ -217,6 +221,9 @@ export default class FrameElement { if (state.value && this.iFrameFormElement.fieldType === ELEMENTS.FILE_INPUT.name) { this.focusChange(false); } + if (state.value && this.iFrameFormElement.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + this.focusChange(false); + } this.focusChange(false); if (state.error && this.domError) { @@ -454,6 +461,12 @@ export default class FrameElement { .emit(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iFrameFormElement.iFrameName, { name: this.iFrameFormElement.iFrameName, }); + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iFrameFormElement.iFrameName, + data: { + name: this.iFrameFormElement.iFrameName, + }, + }, this.clientDomain); this.updateStyleClasses(this.iFrameFormElement.getStatus()); }; @@ -567,6 +580,10 @@ export default class FrameElement { const target = event.target as HTMLFormElement; this.iFrameFormElement.setValue(target.files[0], target.checkValidity()); this.focusChange(true); + } else if (this.iFrameFormElement.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + const target = event.target as HTMLFormElement; + this.iFrameFormElement.setValue(target.files, target.checkValidity()); + this.focusChange(true); } else { const target = event.target as HTMLInputElement; const { mask } = this.iFrameFormElement; @@ -817,11 +834,21 @@ export default class FrameElement { }; onSubmit = () => { - bus - .emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameFormElement.iFrameName, { - name: this.iFrameFormElement.iFrameName, - event: ELEMENT_EVENTS_TO_CLIENT.SUBMIT, - }); + if (this.iFrameFormElement.containerType === ContainerType.COMPOSABLE) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameFormElement.iFrameName, + data: { + name: this.iFrameFormElement.iFrameName, + event: ELEMENT_EVENTS_TO_CLIENT.SUBMIT, + }, + }, this.clientDomain); + } else { + bus + .emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameFormElement.iFrameName, { + name: this.iFrameFormElement.iFrameName, + event: ELEMENT_EVENTS_TO_CLIENT.SUBMIT, + }); + } }; onArrowKeys = (keyBoardEvent: KeyboardEvent) => { diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index 5611c51b..7fac88e1 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -15,17 +15,25 @@ import { RENDER_ELEMENT_IMAGE_STYLES, DEFAULT_FILE_RENDER_ERROR, ELEMENT_EVENTS_TO_CLIENT, + REVEAL_TYPES, } from '../../constants'; import getCssClassesFromJss, { generateCssWithoutClass } from '../../../libs/jss-styles'; import { printLog, parameterizedString, } from '../../../utils/logs-helper'; import logs from '../../../utils/logs'; -import { Context, MessageType } from '../../../utils/common'; +import { + Context, IRenderResponseType, IRevealRecord, MessageType, +} from '../../../utils/common'; import { constructMaskTranslation, - getAtobValue, getMaskedOutput, getValueFromName, handleCopyIconClick, styleToString, + formatRevealElementOptions, + getAtobValue, + getMaskedOutput, getValueFromName, handleCopyIconClick, styleToString, } from '../../../utils/helpers'; +import { formatForRenderClient, getFileURLFromVaultBySkyflowIDComposable } from '../../../core-utils/reveal'; +import Client from '../../../client'; +import properties from '../../../properties'; const { getType } = require('mime'); @@ -65,6 +73,8 @@ class RevealFrame { #skyflowContainerId: string = ''; + #client!: Client; + static init() { const url = window.location?.href; const configIndex = url.indexOf('?'); @@ -75,9 +85,9 @@ class RevealFrame { parsedRecord.context, skyflowContainerId); } - constructor(record, context, id) { + constructor(record, context, id, rootDiv?) { this.#skyflowContainerId = id; - this.#name = window.name; + this.#name = rootDiv ? record?.name : window.name; this.#containerId = getValueFromName(this.#name, 2); const encodedClientDomain = getValueFromName(this.#name, 4); const clientDomain = getAtobValue(encodedClientDomain); @@ -91,14 +101,14 @@ class RevealFrame { getCssClassesFromJss(REVEAL_ELEMENT_DIV_STYLE, 'div'); this.#labelElement = document.createElement('span'); - this.#labelElement.className = `SkyflowElement-label-${STYLE_TYPE.BASE}`; + this.#labelElement.className = `SkyflowElement-${this.#name}-label-${STYLE_TYPE.BASE}`; this.#dataElememt = document.createElement('span'); - this.#dataElememt.className = `SkyflowElement-content-${STYLE_TYPE.BASE}`; + this.#dataElememt.className = `SkyflowElement-${this.#name}-content-${STYLE_TYPE.BASE}`; this.#dataElememt.id = this.#name; this.#errorElement = document.createElement('span'); - this.#errorElement.className = `SkyflowElement-error-${STYLE_TYPE.BASE}`; + this.#errorElement.className = `SkyflowElement-${this.#name}-error-${STYLE_TYPE.BASE}`; if (this.#record.enableCopy) { this.domCopy = document.createElement('img'); @@ -125,13 +135,14 @@ class RevealFrame { ...REVEAL_ELEMENT_LABEL_DEFAULT_STYLES[STYLE_TYPE.BASE], ...this.#record.labelStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#labelStyles, 'label'); + // getCssClassesFromJss(this.#labelStyles, 'label'); + getCssClassesFromJss(this.#labelStyles, `${this.#name}-label`); if (this.#record.labelStyles[STYLE_TYPE.GLOBAL]) { generateCssWithoutClass(this.#record.labelStyles[STYLE_TYPE.GLOBAL]); } } else { - getCssClassesFromJss(REVEAL_ELEMENT_LABEL_DEFAULT_STYLES, 'label'); + getCssClassesFromJss(REVEAL_ELEMENT_LABEL_DEFAULT_STYLES, `${this.#name}-label`); } } this.updateDataView(); @@ -140,7 +151,7 @@ class RevealFrame { this.#inputStyles[STYLE_TYPE.BASE] = { ...this.#record.inputStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#inputStyles, 'content'); + getCssClassesFromJss(this.#inputStyles, `${this.#name}-content`); if (this.#record.inputStyles[STYLE_TYPE.GLOBAL]) { generateCssWithoutClass(this.#record.inputStyles[STYLE_TYPE.GLOBAL]); } @@ -155,26 +166,68 @@ class RevealFrame { ...REVEAL_ELEMENT_ERROR_TEXT_DEFAULT_STYLES[STYLE_TYPE.BASE], ...this.#record.errorTextStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#errorTextStyles, 'error'); + getCssClassesFromJss(this.#errorTextStyles, `${this.#name}-error`); if (this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]) { generateCssWithoutClass(this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]); } } else { getCssClassesFromJss( REVEAL_ELEMENT_ERROR_TEXT_DEFAULT_STYLES, - 'error', + `${this.#name}-error`, ); } this.#elementContainer.appendChild(this.#dataElememt); - document.body.append(this.#elementContainer); + if (rootDiv) rootDiv.append(this.#elementContainer); + else document.body.append(this.#elementContainer); + // document.body.append(this.#elementContainer); bus.emit(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#name, { name: this.#name }); - + if (rootDiv) { + this.getConfig(); + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#name, + data: { + name: this.#name, + }, + }, this.#clientDomain); + + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + data: { height: this.#elementContainer.scrollHeight, name: this.#name }, + }, this.#clientDomain); + } bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, (_, callback) => { callback({ height: this.#elementContainer.scrollHeight, name: this.#name }); }); + const sub2 = (responseUrl) => { + if (responseUrl.iframeName === this.#name) { + if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { + this.setRevealError(DEFAULT_FILE_RENDER_ERROR); + if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { + this.#dataElememt.innerText = this.#record.altText; + } + bus + .emit( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + { + height: this.#elementContainer.scrollHeight, + }, () => { + }, + ); + } else { + const ext = this.getExtension(responseUrl.url); + this.addFileRender(responseUrl.url, ext); + } + } + }; + bus + .target(window.location.origin) + .on( + ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + this.#name, + sub2, + ); const sub = (data) => { if (Object.prototype.hasOwnProperty.call(data, this.#record.token)) { @@ -189,7 +242,7 @@ class RevealFrame { this.#dataElememt.innerText = formattedOutput; } printLog(parameterizedString(logs.infoLogs.ELEMENT_REVEALED, - CLASS_NAME, this.#record.token), MessageType.LOG, this.#context.logLevel); + CLASS_NAME, this.#record.token), MessageType.LOG, this.#context?.logLevel); // bus // .target(window.location.origin) @@ -220,35 +273,172 @@ class RevealFrame { if (data.isTriggerError) { this.setRevealError(data.clientErrorText as string); } else { this.setRevealError(''); } } }); + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + this.#containerId, + data: { + name: window.name, + }, + }, this.#clientDomain, + ); this.updateRevealElementOptions(); - - const sub2 = (responseUrl) => { - if (responseUrl.iframeName === this.#name) { - if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { - this.setRevealError(DEFAULT_FILE_RENDER_ERROR); - if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { - this.#dataElememt.innerText = this.#record.altText; - } - bus - .emit( - ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, - { - height: this.#elementContainer.scrollHeight, - }, () => { - }, + window.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.#name) { + if (event?.data?.data?.iframeName === this.#name + && event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + this.renderFile(this.#record, event?.data?.clientConfig)?.then((resolvedResult) => { + const result = formatForRenderClient( + resolvedResult as IRenderResponseType, + this.#record?.column, ); - } else { - const ext = this.getExtension(responseUrl.url); - this.addFileRender(responseUrl.url, ext); + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, + data: { + type: REVEAL_TYPES.RENDER_FILE, + result, + }, + }, this.#clientDomain); + + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + })?.catch((error) => { + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, + data: { + type: REVEAL_TYPES.RENDER_FILE, + result: { + errors: error, + }, + }, + }, this.#clientDomain); + + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + }); } } - }; - bus - .target(window.location.origin) - .on( - ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + this.#name, - sub2, + + if (event?.data?.type === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name) { + if (event?.data?.data?.height) { + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + data: { + height: this.#elementContainer?.scrollHeight ?? 0, + name: this.#name, + }, + }, this.#clientDomain); + } + } + }); + } + + responseUpdate = (data) => { + if (data?.frameId === this.#record?.name && data?.error) { + if (!Object.prototype.hasOwnProperty.call(this.#record, 'skyflowID')) { + this.setRevealError(REVEAL_ELEMENT_ERROR_TEXT); + } + } else if (data?.frameId === this.#record?.name + && data?.[0]?.token + && this.#record?.token === data?.[0]?.token) { + const responseValue = data?.[0]?.value as string ?? ''; + this.#revealedValue = responseValue; + this.isRevealCalled = true; + this.#dataElememt.innerText = responseValue; + + if (this.#record?.mask) { + const { formattedOutput } = getMaskedOutput( + this.#dataElememt?.innerText ?? '', + this.#record?.mask?.[0], + constructMaskTranslation(this.#record?.mask), + ); + this.#dataElememt.innerText = formattedOutput ?? ''; + } + + printLog( + parameterizedString( + logs?.infoLogs?.ELEMENT_REVEALED, + CLASS_NAME, + this.#record?.token, + ), + MessageType.LOG, + this.#context?.logLevel, ); + } + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + data: { + height: this.#elementContainer?.scrollHeight ?? 0, + name: this.#name, + }, + }, + this.#clientDomain, + ); + }; + + getConfig = () => { + const url = window.location?.href; + const configIndex = url.indexOf('?'); + const encodedString = configIndex !== -1 ? decodeURIComponent(url.substring(configIndex + 1)) : ''; + const parsedRecord = encodedString ? JSON.parse(atob(encodedString)) : {}; + this.#clientDomain = parsedRecord.clientDomain || ''; + this.#containerId = parsedRecord.containerId; + }; + + getData = () => this.#record; + + private sub2 = (responseUrl) => { + if (responseUrl.iframeName === this.#name) { + if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { + this.setRevealError(DEFAULT_FILE_RENDER_ERROR); + if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { + this.#dataElememt.innerText = this.#record.altText; + } + bus + .emit( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + { + height: this.#elementContainer.scrollHeight, + }, () => { + }, + ); + } else { + const ext = this.getExtension(responseUrl.url); + this.addFileRender(responseUrl.url, ext); + } + } + }; + + private renderFile(data: IRevealRecord, clientConfig) { + this.#client = new Client(clientConfig, {}); + return new Promise((resolve, reject) => { + try { + getFileURLFromVaultBySkyflowIDComposable(data, this.#client, clientConfig.authToken) + .then((resolvedResult) => { + let url = ''; + if (resolvedResult.fields && data.column) { + url = resolvedResult.fields[data.column]; + } + this.sub2({ + url, + iframeName: this.#name, + }); + resolve(resolvedResult); + }, + (rejectedResult) => { + this.sub2({ + error: DEFAULT_FILE_RENDER_ERROR, + iframeName: this.#name, + }); + reject(rejectedResult); + }); + } catch (err) { + reject(err); + } + }); } // eslint-disable-next-line class-methods-use-this @@ -312,6 +502,31 @@ class RevealFrame { } private updateRevealElementOptions() { + window.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + this.#name) { + const data = event?.data; + if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS) { + const updatedValue = data.updatedValue as object; + this.#record = { + ...this.#record, + ...updatedValue, + ...formatRevealElementOptions(updatedValue), + }; + this.updateElementProps(); + if (this.isRevealCalled) { + if (this.#record?.mask) { + const { formattedOutput } = getMaskedOutput( + this.#revealedValue ?? '', + this.#record?.mask?.[0], + constructMaskTranslation(this.#record?.mask), + ); + this.#dataElememt.innerText = formattedOutput ?? ''; + } + } + } + } + }); bus .target(this.#clientDomain) .on(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#name, (data) => { @@ -355,7 +570,7 @@ class RevealFrame { ...this.#inputStyles, ...this.#record.inputStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#inputStyles, 'content'); + getCssClassesFromJss(this.#inputStyles, `${this.#name}-content`); if (this.#record.inputStyles[STYLE_TYPE.GLOBAL]) { const newInputGlobalStyles = { ...this.#inputStyles[STYLE_TYPE.GLOBAL], @@ -370,7 +585,7 @@ class RevealFrame { ...REVEAL_ELEMENT_LABEL_DEFAULT_STYLES[STYLE_TYPE.BASE], ...this.#record.labelStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#labelStyles, 'label'); + getCssClassesFromJss(this.#labelStyles, `${this.#name}-label`); if (this.#record.labelStyles[STYLE_TYPE.GLOBAL]) { const newLabelGlobalStyles = { @@ -386,7 +601,7 @@ class RevealFrame { ...this.#errorTextStyles[STYLE_TYPE.BASE], ...this.#record.errorTextStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#errorTextStyles, 'error'); + getCssClassesFromJss(this.#errorTextStyles, `${this.#name}-error`); if (this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]) { const newErrorTextGlobalStyles = { ...this.#errorTextStyles[STYLE_TYPE.GLOBAL], diff --git a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts index c06964de..146f3888 100644 --- a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts +++ b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts @@ -92,11 +92,11 @@ class SkyflowFrameController { try { window.CoralogixRum.info(SDK_IFRAME_EVENT, data.event); printLog(parameterizedString(logs.infoLogs.METRIC_CAPTURE_EVENT), - MessageType.LOG, this.#context.logLevel); + MessageType.LOG, this.#context?.logLevel); } catch (err: any) { printLog(parameterizedString(logs.infoLogs.UNKNOWN_METRIC_CAPTURE_EVENT, err.toString()), - MessageType.LOG, this.#context.logLevel); + MessageType.LOG, this.#context?.logLevel); } } }, @@ -485,7 +485,8 @@ class SkyflowFrameController { if (inputElement) { if ( inputElement.iFrameFormElement.fieldType - !== ELEMENTS.FILE_INPUT.name + !== ELEMENTS.FILE_INPUT.name && inputElement.iFrameFormElement.fieldType + !== ELEMENTS.MULTI_FILE_INPUT.name ) { const { state, doesClientHasError, clientErrorText, errorText, onFocusChange, validations, @@ -525,6 +526,7 @@ class SkyflowFrameController { if ( inputElement.iFrameFormElement.fieldType !== ELEMENTS.FILE_INPUT.name + && inputElement.iFrameFormElement.fieldType !== ELEMENTS.MULTI_FILE_INPUT.name ) { if ( inputElement.iFrameFormElement.fieldType diff --git a/src/index-internal.ts b/src/index-internal.ts index 31274800..1cf7049f 100644 --- a/src/index-internal.ts +++ b/src/index-internal.ts @@ -4,6 +4,7 @@ Copyright (c) 2022 Skyflow, Inc. import 'core-js/stable'; import RevealFrame from './core/internal/reveal/reveal-frame'; import { + COMPOSABLE_REVEAL, FRAME_ELEMENT, FRAME_REVEAL, SKYFLOW_FRAME_CONTROLLER, @@ -18,6 +19,7 @@ import { } from './utils/logs-helper'; import { getAtobValue, getValueFromName } from './utils/helpers'; import FrameElementInit from './core/internal/frame-element-init'; +import RevealComposableFrameElementInit from './core/internal/composable-frame-element-init'; (function init(root: any) { try { @@ -26,6 +28,9 @@ import FrameElementInit from './core/internal/frame-element-init'; const frameId = getValueFromName(frameName, 1); if (frameType === SKYFLOW_FRAME_CONTROLLER) { SkyflowFrameController.init(frameId); + } else if (frameType === COMPOSABLE_REVEAL) { + root.Skyflow = RevealComposableFrameElementInit; + RevealComposableFrameElementInit.startFrameElement(); } else if (frameType === FRAME_ELEMENT) { const logLevel = getValueFromName(frameName, 4) || LogLevel.ERROR; printLog( diff --git a/src/libs/element-options.ts b/src/libs/element-options.ts index 3ee11b57..85a5364d 100644 --- a/src/libs/element-options.ts +++ b/src/libs/element-options.ts @@ -373,7 +373,7 @@ export const formatOptions = (elementType, options, logLevel) => { break; } - case ELEMENTS.FILE_INPUT.name: { + case ELEMENTS.FILE_INPUT.name || ELEMENTS.MULTI_FILE_INPUT.name: { if (!Object.prototype.hasOwnProperty.call(formattedOptions, 'preserveFileName')) { formattedOptions = { ...formattedOptions, preserveFileName: true }; } @@ -395,7 +395,7 @@ export const formatOptions = (elementType, options, logLevel) => { if (Object.prototype.hasOwnProperty.call(formattedOptions, 'preserveFileName') && !validateBooleanOptions(formattedOptions.preserveFileName)) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_BOOLEAN_OPTIONS, ['preserveFileName'], true); } - if (elementType === ELEMENTS.FILE_INPUT.name) { + if (elementType === ELEMENTS.FILE_INPUT.name || elementType === ELEMENTS.MULTI_FILE_INPUT.name) { if (options.allowedFileType) { if (!Array.isArray(options.allowedFileType)) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_ALLOWED_OPTIONS, [], true); diff --git a/src/skyflow.ts b/src/skyflow.ts index 50333549..ef8dd5f4 100644 --- a/src/skyflow.ts +++ b/src/skyflow.ts @@ -47,11 +47,13 @@ import { formatVaultURL, checkAndSetForCustomUrl } from './utils/helpers'; import ComposableContainer from './core/external/collect/compose-collect-container'; import { validateComposableContainerOptions } from './utils/validators'; import ThreeDS from './core/external/threeds/threeds'; +import ComposableRevealContainer from './core/external/reveal/composable-reveal-container'; export enum ContainerType { COLLECT = 'COLLECT', REVEAL = 'REVEAL', COMPOSABLE = 'COMPOSABLE', + COMPOSE_REVEAL = 'COMPOSABLE_REVEAL', } export interface ISkyflow { vaultID?: string; @@ -164,9 +166,50 @@ class Skyflow { return skyflow; } + #getSkyflowBearerToken = () => new Promise((resolve, reject) => { + if ( + this.#client.config.getBearerToken + && (!this.#bearerToken || !isTokenValid(this.#bearerToken)) + ) { + this.#client.config + .getBearerToken() + .then((bearerToken) => { + if (isTokenValid(bearerToken)) { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#logLevel); + this.#bearerToken = bearerToken; + resolve(this.#bearerToken); + } else { + printLog(parameterizedString( + logs.errorLogs.INVALID_BEARER_TOKEN, + ), MessageType.ERROR, this.#logLevel); + reject({ + error: parameterizedString( + logs.errorLogs.INVALID_BEARER_TOKEN, + ), + }); + } + }) + .catch((err) => { + printLog(parameterizedString(logs.errorLogs.BEARER_TOKEN_REJECTED), MessageType.ERROR, + this.#logLevel); + reject({ error: err }); + }); + } else { + printLog(parameterizedString(logs.infoLogs.REUSE_BEARER_TOKEN, CLASS_NAME), + MessageType.LOG, + this.#logLevel); + resolve(this.#bearerToken); + } + }); + container(type: ContainerType.COLLECT, options?: ContainerOptions): CollectContainer; container(type: ContainerType.COMPOSABLE, options?: ContainerOptions): ComposableContainer; container(type: ContainerType.REVEAL, options?: ContainerOptions): RevealContainer; + container(type: ContainerType.COMPOSE_REVEAL, + options?: ContainerOptions) + : ComposableRevealContainer; container(type: ContainerType, options?: ContainerOptions) { switch (type) { case ContainerType.COLLECT: { @@ -189,6 +232,7 @@ class Skyflow { clientJSON: this.#client.toJSON(), containerType: type, skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, }, this.#skyflowElements, { logLevel: this.#logLevel }, options); @@ -204,6 +248,7 @@ class Skyflow { clientJSON: this.#client.toJSON(), containerType: type, skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, }, this.#skyflowElements, { logLevel: this.#logLevel, env: this.#env }); @@ -213,6 +258,23 @@ class Skyflow { return collectContainer; } + case ContainerType.COMPOSE_REVEAL: { + validateComposableContainerOptions(options); + const revealComposableContainer = new ComposableRevealContainer(options, { + ...this.#metadata, + clientJSON: this.#client.toJSON(), + containerType: type, + skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, + }, + this.#skyflowElements, + { logLevel: this.#logLevel, env: this.#env }); + printLog(parameterizedString(logs.infoLogs.REVEAL_CONTAINER_CREATED, CLASS_NAME), + MessageType.LOG, + this.#logLevel); + return revealComposableContainer; + } + default: if (!type) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_CONTAINER_TYPE, [], true); diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index 5932f888..4077d8f8 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -75,6 +75,15 @@ export interface IRevealRecord { table?: string; } +export interface IRevealRecordComposable { + token?: string; + redaction?: RedactionType; + column?: string; + skyflowID?: string; + table?: string; + iframeName?: string; +} + export interface IInsertResponse { records: IInsertResponseReocrds[]; } @@ -308,6 +317,9 @@ export interface ICollectOptions { additionalFields?: IInsertRecordInput, upsert?: Array, } +export interface MetaData { + [key: string]: any, +} export interface UploadFilesResponse { fileUploadResponse: [{ skyflow_id: string }], diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e80fcf2c..539ae099 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -4,6 +4,12 @@ Copyright (c) 2022 Skyflow, Inc. import logs from './logs'; const SKYFLOW_ERROR_CODE = { + MULTI_FILE_NOT_SUPPORTED: { + code: 400, + description: logs.errorLogs.MULTI_FILE_NOT_SUPPORTED, + }, + INVALID_REVEAL_COMPOSABLE_INPUT: + { code: 400, description: logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT }, INVALID_FILE_NAME: { code: 400, description: logs.errorLogs.INVALID_FILE_NAME }, INVALID_FIELD: { code: 400, description: logs.errorLogs.INVALID_FIELD }, VAULTID_IS_REQUIRED: { code: 400, description: logs.errorLogs.VAULTID_IS_REQUIRED }, diff --git a/src/utils/logs.ts b/src/utils/logs.ts index 367ae2a8..8b5a75c0 100644 --- a/src/utils/logs.ts +++ b/src/utils/logs.ts @@ -10,6 +10,8 @@ const logs = { CREATE_COLLECT_CONTAINER: '%s1 - Creating Collect container.', COLLECT_CONTAINER_CREATED: '%s1 - Created Collect container successfully.', + INITIALIZE_COMPOSABLE_CLIENT: '%s1 - Initializing Composable container.', + CREATE_REVEAL_CONTAINER: '%s1 - Creating Reveal container.', REVEAL_CONTAINER_CREATED: '%s1 - Created Reveal container successfully.', @@ -26,6 +28,8 @@ const logs = { ELEMENT_REVEALED: '%s1 - %s2 Element revealed.', FILE_RENDERED: '%s1 - %s2 File rendered.', COLLECT_SUBMIT_SUCCESS: '%s1 - Data has been collected successfully.', + UPLOAD_FILES_SUCCESS: '%s1 - Files uploaded successfully.', + MULTI_UPLOAD_FILES_SUCCESS: '%s1 - Multiple files uploaded successfully.', REVEAL_SUBMIT_SUCCESS: '%s1 - Data has been revealed successfully.', RENDER_SUBMIT_SUCCESS: '%s1 - File download URL has been fetched successfully.', INSERT_DATA_SUCCESS: '%s1 - Data has been inserted successfully.', @@ -91,6 +95,8 @@ const logs = { VALIDATE_GET_BY_ID_INPUT: '%s1 - Validating getByID input.', }, errorLogs: { + MULTI_FILE_NOT_SUPPORTED: 'Multi file upload is only supported in MULT_FILE_INPUT element in composable container. Please use MULT_FILE_INPUT element for multi file upload.', + INVALID_REVEAL_COMPOSABLE_INPUT: 'Reveal composable input is invalid. Please provide a valid input.', NO_ELEMENTS_IN_COLLECT: 'Validation error. No elements found in collect container', NO_ELEMENTS_IN_COMPOSABLE: 'Validation error. No elements found in composable container', NO_ELEMENTS_IN_REVEAL: 'Validation error. No elements found in reveal container', From f13b04ec476560a9484faaa60455a41546b99f07 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 29 Jul 2025 09:10:59 +0000 Subject: [PATCH 02/47] [AUTOMATED] Release - 2.5.0-beta.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7281b16..3bb4392e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.2-dev.6c87190", + "version": "2.5.0-beta.8", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From de01056bca8b8bbffe89f14b6d2c1ddd497be007 Mon Sep 17 00:00:00 2001 From: skyflow-bharti <118584001+skyflow-bharti@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:50:44 +0530 Subject: [PATCH 03/47] Release/25.7.8.2 (#616) * SK-2177 support composable container in shadow-dom and normal dom * SK-2177 internal release * [AUTOMATED] Release - 2.5.0-beta.5-dev.3d22e02 * SK-2177 fix promises * [AUTOMATED] Release - 2.5.0-beta.5-dev.c97c0f5 * SK-2177 mounted * [AUTOMATED] Release - 2.5.0-beta.5-dev.1a3ba57 * SK-2177 mounted * [AUTOMATED] Release - 2.5.0-beta.5-dev.4f9c03b * SK-2177 mounted * [AUTOMATED] Release - 2.5.0-beta.5-dev.410308a * SK-2177 comment mount * [AUTOMATED] Release - 2.5.0-beta.5-dev.06519d4 * SK-2177 comment mounted * [AUTOMATED] Release - 2.5.0-beta.5-dev.9d975d3 * SK-2177 client initialise * [AUTOMATED] Release - 2.5.0-beta.5-dev.1c981ce * SK-2177 add target * [AUTOMATED] Release - 2.5.0-beta.5-dev.deb34bf * SK-2177 add target * [AUTOMATED] Release - 2.5.0-beta.5-dev.7dda8b1 * SK-2177 add target * [AUTOMATED] Release - 2.5.0-beta.5-dev.a9e2143 * SK-2177 client initialise * [AUTOMATED] Release - 2.5.0-beta.5-dev.d5616a2 * SK-2177 client initialise * [AUTOMATED] Release - 2.5.0-beta.5-dev.a0c77c9 * SK-2177 client initialise * [AUTOMATED] Release - 2.5.0-beta.5-dev.69a8f50 * SK-2177 client fix * [AUTOMATED] Release - 2.5.0-beta.5-dev.9812099 * [AUTOMATED] Release - 2.5.0-beta.5-dev.0ee2970 * [AUTOMATED] Release - 2.5.0-beta.5-dev.ef404d4 * SK-2177 target test * [AUTOMATED] Release - 2.5.0-beta.5-dev.5f64ad2 * SK-2177 target tests * [AUTOMATED] Release - 2.5.0-beta.5-dev.25da8a5 * SK-2177 target tests * SK-2177 fix height * [AUTOMATED] Release - 2.5.0-beta.5-dev.36a43c5 * SK-2177 file render * [AUTOMATED] Release - 2.5.0-beta.5-dev.fc93609 * SKS-2117 WIP reveal composable changes. * SKS-2177: WIP added reveal composble element styles. * SK-2177 added event for reveal composable * SK-2177 added event for reveal composable * SK-2177 add reveal element input validations. * SK-2177 file render changes * SK-2177 file render changes * [AUTOMATED] Release - 2.5.0-beta.5-dev.50ad902 * SK-2177 fix height * SK-2177 fix render changes * [AUTOMATED] Release - 2.5.0-beta.5-dev.8fd34b7 * SK-2177 added error handling * SK-2177 trigger release internal * [AUTOMATED] Release - 2.5.0-beta.5-dev.de7a4f1 * SK-2177 add optional checks * [AUTOMATED] Release - 2.5.0-beta.5-dev.f8a1528 * SK-2191 add support for multiple file upload * [AUTOMATED] Release - 2.5.0-beta.6-dev.97db0e3 * SK-2191 fix drag and drop * [AUTOMATED] Release - 2.5.0-beta.6-dev.e034fae * SK-2191 fix drag and drop * [AUTOMATED] Release - 2.5.0-beta.6-dev.9003510 * SK-2191 fix drag and drop * [AUTOMATED] Release - 2.5.0-beta.6-dev.b79aa08 * [AUTOMATED] Release - 2.5.0-beta.6-dev.5974a3a * SK-2191 fix drag and drop * SK-2197 removed commented code * [AUTOMATED] Release - 2.5.0-beta.6-dev.19a5b44 * SK-2191 fix height * [AUTOMATED] Release - 2.5.0-beta.6-dev.7c41e89 * [AUTOMATED] Release - 2.5.0-beta.6-dev.a29ea92 * SK-2202 fix the listeners * [AUTOMATED] Release - 2.4.2-dev.eb6dbb3 * SK-2202 fix the submit * [AUTOMATED] Release - 2.4.2-dev.47ea8b2 * SK-2202 add optional check * [AUTOMATED] Release - 2.4.2-dev.141ab05 * [AUTOMATED] Release - 2.4.2-dev.6c87190 * SK-2202 event listener * [AUTOMATED] Release - 2.5.0-beta.8-dev.d79138c * [AUTOMATED] Release - 2.5.0-beta.8-dev.cc9dc5a --------- Co-authored-by: skyflow-bharti Co-authored-by: yaswanth-pula-skyflow --- package.json | 2 +- src/core/external/collect/collect-element.ts | 3 ++- src/core/internal/iframe-form/index.ts | 20 +++++++++++++------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 3bb4392e..4ce21b38 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.8", + "version": "2.5.0-beta.8-dev.cc9dc5a", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index d2aa0ac7..967f5b40 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -512,7 +512,8 @@ class CollectElement extends SkyflowElement { this.#states[index].isFocused = data.value.isFocused; this.#states[index].isRequired = data.value.isRequired; this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; - if (element.elementType === ElementType.MULTI_FILE_INPUT) { + if (element.elementType === ElementType.MULTI_FILE_INPUT + || element.elementType === ElementType.FILE_INPUT) { this.#states[index].metaData = data?.value?.metaData || []; } if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; diff --git a/src/core/internal/iframe-form/index.ts b/src/core/internal/iframe-form/index.ts index de56c27f..6da0eebb 100644 --- a/src/core/internal/iframe-form/index.ts +++ b/src/core/internal/iframe-form/index.ts @@ -220,7 +220,8 @@ export default class IFrameFormElement extends EventEmitter { value: { ...this.getStatus() }, }); if (this.containerType === ContainerType.COMPOSABLE) { - if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { window.parent.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, data: { @@ -670,7 +671,8 @@ export default class IFrameFormElement extends EventEmitter { value: this.getStatus(), }); if (this.containerType === ContainerType.COMPOSABLE) { - if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { window.parent.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, data: { @@ -777,7 +779,8 @@ export default class IFrameFormElement extends EventEmitter { value: this.getStatus(), }); if (this.containerType === ContainerType.COMPOSABLE) { - if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { window.parent.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, data: { @@ -806,7 +809,8 @@ export default class IFrameFormElement extends EventEmitter { && (this.fieldType === ELEMENTS.EXPIRATION_DATE.name || this.fieldType === ELEMENTS.EXPIRATION_MONTH.name || this.fieldType === ELEMENTS.FILE_INPUT.name - || this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) + || this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + ) ) { bus.emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, { name: this.iFrameName, @@ -814,7 +818,8 @@ export default class IFrameFormElement extends EventEmitter { value: this.getStatus(), }); if (this.containerType === ContainerType.COMPOSABLE) { - if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { window.parent.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, data: { @@ -845,7 +850,8 @@ export default class IFrameFormElement extends EventEmitter { value: this.getStatus(), }); if (this.containerType === ContainerType.COMPOSABLE) { - if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { window.parent.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, data: { @@ -905,4 +911,4 @@ export default class IFrameFormElement extends EventEmitter { this.resetData(); this.resetEvents(); } -} +} \ No newline at end of file From c482a14055878207ece771b8c5f9542a2267682e Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 14 Aug 2025 16:57:33 +0530 Subject: [PATCH 04/47] SK-2239: add file metadata in file render and wcag keyboard actions --- package.json | 2 +- src/core-utils/reveal.ts | 3 +- src/core/external/collect/collect-element.ts | 9 +++--- src/core/external/common/iframe.ts | 1 + .../reveal/composable-reveal-container.ts | 4 +-- .../reveal/composable-reveal-internal.ts | 20 ++++++------- src/core/internal/iframe-form/index.ts | 20 ++++++------- src/core/internal/index.ts | 30 ++++++++++++++++++- src/core/internal/reveal/reveal-frame.ts | 4 ++- src/utils/common/index.ts | 1 + tests/utils/jwt-utils.test.js | 4 +-- 11 files changed, 65 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 4ce21b38..3c308b17 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.8-dev.cc9dc5a", + "version": "2.4.3-dev.b8e2236", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", diff --git a/src/core-utils/reveal.ts b/src/core-utils/reveal.ts index 79361634..145708d0 100644 --- a/src/core-utils/reveal.ts +++ b/src/core-utils/reveal.ts @@ -141,7 +141,7 @@ export const getFileURLForRender = ( paramList += `${skyflowIdRecord.skyflowID}?`; - paramList += `fields=${skyflowIdRecord.column}&${FILE_DOWNLOAD_URL_PARAM}`; + paramList += `fields=${skyflowIdRecord.column}&${FILE_DOWNLOAD_URL_PARAM}&returnFileMetadata=true`; const vaultEndPointurl: string = `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${skyflowIdRecord.table}/${paramList}`; return client.request({ @@ -340,6 +340,7 @@ export const formatForRenderClient = (response: IRenderResponseType, column: str const successRecord = { skyflow_id: response.fields.skyflow_id, column, + fileMetadata: response.fileMetadata, }; formattedResponse.success = successRecord; } else if (response.errors) { diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index 967f5b40..a423b3b5 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -468,7 +468,7 @@ class CollectElement extends SkyflowElement { } }); this.#elements.forEach((element1) => { - const isComposableContainer = this.#elements.length > 1; + const isComposableContainer = this.#metaData?.containerType === 'COMPOSABLE'; if (isComposableContainer) { window.addEventListener('message', (event) => { if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT @@ -482,7 +482,6 @@ class CollectElement extends SkyflowElement { ) { this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); } else { - const isComposable = this.#elements.length > 1; this.#elements.forEach((element, index) => { if (data.name === element.elementName) { let emitEvent = ''; @@ -519,7 +518,7 @@ class CollectElement extends SkyflowElement { if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; else this.#states[index].value = undefined; - emitEvent = isComposable ? `${emitEvent}:${data.name}` : emitEvent; + emitEvent = isComposableContainer ? `${emitEvent}:${data.name}` : emitEvent; this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, {}, (payload:any) => { @@ -531,12 +530,12 @@ class CollectElement extends SkyflowElement { ...this.#states[index], elementType: element.elementType, }; - if (isComposable) { + if (isComposableContainer) { this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, { iframeName: this.#iframe.name, }); } - if (isComposable && this.#groupEmitter) { + if (isComposableContainer && this.#groupEmitter) { this.#groupEmitter._emit(emitEvent, emitData); } else { this.#eventEmitter._emit(emitEvent, emitData); diff --git a/src/core/external/common/iframe.ts b/src/core/external/common/iframe.ts index 8f7f8a17..c85b8e22 100644 --- a/src/core/external/common/iframe.ts +++ b/src/core/external/common/iframe.ts @@ -26,6 +26,7 @@ export default class IFrame { this.iframe = iframer({ name: this.name, referrer: clientDomain, + title: name.match(/^element:([^:]+):/)?.[1] ?? name, }); } diff --git a/src/core/external/reveal/composable-reveal-container.ts b/src/core/external/reveal/composable-reveal-container.ts index 2d829848..bf635096 100644 --- a/src/core/external/reveal/composable-reveal-container.ts +++ b/src/core/external/reveal/composable-reveal-container.ts @@ -187,9 +187,9 @@ class ComposableRevealContainer extends Container { let element = this.#elements[this.#tempElements.elementName]; if (element) { if (isSingleElementAPI) { - element.update(elements[0]); + // element.update(elements[0]); } else { - element.update(this.#tempElements); + // element.update(this.#tempElements); } } else { const elementId = uuid(); diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts index a5584755..618f753e 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -108,12 +108,6 @@ class ComposableRevealInternalElement extends SkyflowElement { this.#iframe?.setIframeHeight(data?.height); }); - window?.addEventListener('message', (event) => { - if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED - + this.#containerId) { - this.#isComposableFrameReady = true; - } - }); window?.addEventListener('message', (event) => { if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + this.#iframe?.name) { @@ -136,6 +130,12 @@ class ComposableRevealInternalElement extends SkyflowElement { rows?.forEach((row, rowIndex) => { row?.elements?.forEach((element: any, elementIndex: number) => { if (!element?.name) return; + window?.addEventListener('message', (event) => { + if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + element?.name) { + this.#isComposableFrameReady = true; + } + }); this.#eventEmitter?.on( `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST}:${element?.name}`, (data, callback) => { @@ -351,9 +351,9 @@ class ComposableRevealInternalElement extends SkyflowElement { MessageType.LOG, loglevel); validateRenderElementRecord(recordData); - window.addEventListener('message', (event) => { + window.addEventListener('message', (event) => { if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED - + this.#containerId) { + + recordData?.name) { this.#isMounted = true; this.#getSkyflowBearerToken()?.then((authToken) => { printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), @@ -377,7 +377,7 @@ class ComposableRevealInternalElement extends SkyflowElement { window.addEventListener('message', (event1) => { if (event1?.data && event1?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE - + this.#iframe.name) { + + recordData.name) { if (event1?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { const revealData = event1?.data?.data?.result; if (revealData?.error) { @@ -592,7 +592,7 @@ class ComposableRevealInternalElement extends SkyflowElement { } else { window.addEventListener('message', (event) => { if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED - + this.#containerId) { + + record?.name) { this.#emitEvent( ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, { diff --git a/src/core/internal/iframe-form/index.ts b/src/core/internal/iframe-form/index.ts index 6da0eebb..32d29010 100644 --- a/src/core/internal/iframe-form/index.ts +++ b/src/core/internal/iframe-form/index.ts @@ -313,9 +313,9 @@ export default class IFrameFormElement extends EventEmitter { } getFileDetails = (value: FileList | File | null): Array<{ - name: string; - size: number; - type: string; + fileName: string; + fileSizeKB: number; + fileType: string; }> => { // Return empty array if no value if (!value) return []; @@ -324,18 +324,18 @@ export default class IFrameFormElement extends EventEmitter { // Handle FileList if (value instanceof FileList) { return Array.from(value).map((file) => ({ - name: file.name, - size: file.size, - type: file.type, + fileName: file.name, + fileSizeKB: Math.ceil(file.size / 1024), + fileType: file.type, })); } // Handle single File if (value instanceof File) { return [{ - name: value.name, - size: value.size, - type: value.type, + fileName: value.name, + fileSizeKB: Math.ceil(value.size / 1024), + fileType: value.type, }]; } @@ -911,4 +911,4 @@ export default class IFrameFormElement extends EventEmitter { this.resetData(); this.resetEvents(); } -} \ No newline at end of file +} diff --git a/src/core/internal/index.ts b/src/core/internal/index.ts index aca89ed3..01878a53 100644 --- a/src/core/internal/index.ts +++ b/src/core/internal/index.ts @@ -145,6 +145,14 @@ export default class FrameElement { this.inputParent.style.position = 'relative'; const inputElement = document.createElement(type); this.domInput = inputElement; + inputElement.addEventListener('keydown', (event) => { + const keyboardEvent = event as KeyboardEvent; + if ((keyboardEvent.ctrlKey || keyboardEvent.metaKey) && keyboardEvent.key.toLowerCase() === 'z') { + keyboardEvent.preventDefault(); + this.setValue(''); + this.iFrameFormElement.setValue('', true); + } + }); this.domInput.iFrameFormElement = this.iFrameFormElement; inputElement.setAttribute(CUSTOM_ROW_ID_ATTRIBUTE, this.htmlDivElement?.id?.split(':')[0] || ''); this.inputParent.append(inputElement); @@ -164,6 +172,15 @@ export default class FrameElement { this.dropdownSelect = document.createElement('select'); this.dropdownSelect.setAttribute('style', this.options?.inputStyles?.dropdown ? (DROPDOWN_STYLES + styleToString(this.options.inputStyles.dropdown)) : DROPDOWN_STYLES); + this.dropdownSelect.addEventListener('focus', () => { + if (this.options?.inputStyles?.dropdownIcon?.focus) { + this.setDropdownIconStyle(this.options?.inputStyles?.dropdownIcon?.focus); + } + }); + + this.dropdownSelect.addEventListener('blur', () => { + this.setDropdownIconStyle(this.options?.inputStyles?.dropdownIcon); + }); this.dropdownSelect.addEventListener('change', (event:any) => { event.preventDefault(); @@ -888,7 +905,6 @@ export default class FrameElement { case INPUT_KEYBOARD_EVENTS.ENTER: this.onSubmit(); - keyBoardEvent.preventDefault(); break; default: break; @@ -1165,4 +1181,16 @@ export default class FrameElement { } } } + + private setDropdownIconStyle(styleObj?: any) { + if (this.dropdownIcon?.style.display === 'block') { + this.dropdownIcon.setAttribute( + 'style', + styleObj + ? DROPDOWN_ICON_STYLES + styleToString(styleObj) + : DROPDOWN_ICON_STYLES, + ); + this.dropdownIcon.style.display = 'block'; + } + } } diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index 7fac88e1..71a268d3 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -275,7 +275,7 @@ class RevealFrame { }); window.parent.postMessage( { - type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + this.#containerId, + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + this.#name, data: { name: window.name, }, @@ -291,6 +291,8 @@ class RevealFrame { resolvedResult as IRenderResponseType, this.#record?.column, ); + console.log("event name : ",ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name) + console.log("resultt : ", result); window?.parent?.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, data: { diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index 4077d8f8..678043d8 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -99,6 +99,7 @@ export interface IRevealResponseType { export interface IRenderResponseType { fields?: Record errors?: Record + fileMetadata?: Record } export interface IDetokenizeInput { diff --git a/tests/utils/jwt-utils.test.js b/tests/utils/jwt-utils.test.js index e6c4f52e..332e11c5 100644 --- a/tests/utils/jwt-utils.test.js +++ b/tests/utils/jwt-utils.test.js @@ -5,7 +5,7 @@ import isTokenValid from "../../src/utils/jwt-utils"; jest.mock('jwt-decode', () => () => ({exp: 123})) describe('Validation token', () => { - + test('empty token', () => { const res = isTokenValid("") expect(res).toBe(false) @@ -20,4 +20,4 @@ describe('Validation token', () => { const res = isTokenValid("token") expect(res).toBe(false) }) -}); \ No newline at end of file +}); From 1ea087379cc789c749aab4301f561a2134032db6 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 14 Aug 2025 11:28:22 +0000 Subject: [PATCH 05/47] [AUTOMATED] Release - 2.4.3-dev.c482a14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c308b17..991e2df5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.3-dev.b8e2236", + "version": "2.4.3-dev.c482a14", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 15b241930f1fb87e33cdfdcaf4e2c87953014502 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 14 Aug 2025 17:22:25 +0530 Subject: [PATCH 06/47] SK-2239: remove console log --- src/core/internal/reveal/reveal-frame.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index 71a268d3..4476eded 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -291,8 +291,6 @@ class RevealFrame { resolvedResult as IRenderResponseType, this.#record?.column, ); - console.log("event name : ",ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name) - console.log("resultt : ", result); window?.parent?.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, data: { From 6e78ac2ba029c96204c085d462aa3ae2c674dbaa Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 14 Aug 2025 11:53:08 +0000 Subject: [PATCH 07/47] [AUTOMATED] Release - 2.4.3-dev.15b2419 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 991e2df5..ee1cee60 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.3-dev.c482a14", + "version": "2.4.3-dev.15b2419", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From a4369f10c1328493b95be6ff3e652950362e2c64 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 14 Aug 2025 12:14:13 +0000 Subject: [PATCH 08/47] [AUTOMATED] Release - 2.5.0-beta.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee1cee60..55bd50d4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.3-dev.15b2419", + "version": "2.5.0-beta.9", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 99fa815fe130079108a692e9bab473917c399643 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 13:15:45 +0530 Subject: [PATCH 09/47] SK-2254: fix 0px height in composable container --- src/core/external/collect/collect-element.ts | 20 ++- src/core/internal/frame-element-init.ts | 141 +++++++++++-------- src/utils/validators/index.ts | 4 - 3 files changed, 98 insertions(+), 67 deletions(-) diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index a423b3b5..c4cb3e25 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -41,6 +41,7 @@ import { pushElementEventWithTimeout, updateMetricObjectValue, } from '../../../metrics'; +import properties from '../../../properties'; const CLASS_NAME = 'Element'; class CollectElement extends SkyflowElement { @@ -202,12 +203,27 @@ class CollectElement extends SkyflowElement { if (!domElement) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['CollectElement'], true); } - this.resizeObserver = new ResizeObserver(() => { + + if(domElement instanceof HTMLElement){ + this.resizeObserver = new ResizeObserver(() => { + if (domElement.getElementsByTagName('iframe')[0]?.contentWindow) { + const iframeElement = domElement.getElementsByTagName('iframe')[0] + if(iframeElement.name === this.#iframe.name){ + iframeElement?.contentWindow?.postMessage({ + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }); + } else if (typeof domElement === 'string'){ + this.resizeObserver = new ResizeObserver(() => { this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, {}, (payload:any) => { this.#iframe.setIframeHeight(payload.height); }); - }); + }); + } + updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElement); if ( this.#metaData?.clientJSON?.config?.options?.trackMetrics diff --git a/src/core/internal/frame-element-init.ts b/src/core/internal/frame-element-init.ts index 13c89945..a3b033ca 100644 --- a/src/core/internal/frame-element-init.ts +++ b/src/core/internal/frame-element-init.ts @@ -217,6 +217,9 @@ export default class FrameElementInit { const value: Blob = Object.values(fileUploadObject)[0] as Blob; + formData.append('columnName', column); + formData.append('tableName', tableName); + if (preserveFileName) { const isValidFileName = vaildateFileName(state.value.name); if (!isValidFileName) { @@ -224,10 +227,14 @@ export default class FrameElementInit { new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true), ); } - formData.append(column, value); + formData.append('file', value); } else { const generatedFileName = generateUploadFileName(state.value.name); - formData.append(column, new File([value], generatedFileName, { type: state.value.type })); + formData.append('file', new File([value], generatedFileName, { type: state.value.type })); + } + + if (skyflowID) { + formData.append('skyflowID', skyflowID); } const client = this.#client; @@ -236,7 +243,7 @@ export default class FrameElementInit { .request({ body: formData, requestMethod: 'POST', - url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}/${skyflowID}/files`, + url: `${client.config.vaultURL}/v2/vaults/${client.config.vaultID}/files/upload`, headers: { authorization: `Bearer ${clientConfig.authToken}`, 'content-type': 'multipart/form-data', @@ -457,73 +464,90 @@ export default class FrameElementInit { // eslint-disable-next-line consistent-return private multipleUploadFiles = - (fileElement: IFrameFormElement, - clientConfig, metaData) => new Promise((rootResolve, rootReject) => { + (fileElement: IFrameFormElement, clientConfig, metaData) => new Promise((rootResolve, rootReject) => { this.#client = new Client(clientConfig, {}); if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); - const { - state, tableName, onFocusChange, preserveFileName, - } = fileElement; + + const { state, tableName, onFocusChange, preserveFileName } = fileElement; if (state.isRequired) { onFocusChange(false); } - if (fileElement.state.value === undefined || fileElement.state.value === null || fileElement.state.value === '') { + if (state.value === undefined || state.value === null || state.value === '') { rootReject({ error: 'No files selected' }); return; } - const files = state.value instanceof FileList - ? Array.from(state.value) - : [state.value]; + const files = state.value instanceof FileList ? Array.from(state.value) : [state.value]; this.validateFiles(files, state, fileElement); - const insertRequest = this.createInsertRequest(files.length, metaData); - this.insertDataCallInMultiFiles( - insertRequest, this.#client, tableName as string, clientConfig.authToken as string, - ).then((response: any) => { - const skyflowIDs = this.extractSkyflowIDs(response); - if (skyflowIDs.length === 0) { - rootReject({ error: 'No skyflow IDs returned from insert data' }); - return; + + const uploadFile = (file: File, skyflowID?: string) => { + const formData = new FormData(); + formData.append('columnName', state.name); + if (tableName) formData.append('tableName', tableName); + if (preserveFileName) { + formData.append('file', file); + } else { + const generatedFileName = generateUploadFileName(file.name); + formData.append('file', new File([file], generatedFileName, { type: file.type })); } - const promises: Promise[] = []; - - files.forEach((file, index) => { - const fileUploadObject: any = {}; - fileUploadObject[state.name] = file; - const formData = new FormData(); - const column = Object.keys(fileUploadObject)[0]; - const value: Blob = Object.values(fileUploadObject)[0] as Blob; - if (preserveFileName) { - formData.append(column, value); - } else { - const generatedFileName = generateUploadFileName(file.name); - formData.append(column, new File([value], generatedFileName, { type: file.type })); + if (skyflowID) formData.append('skyflowID', skyflowID); + const client = this.#client; + return this.#client.request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v2/vaults/${this.#client.config.vaultID}/files/upload`, + headers: { + authorization: `Bearer ${clientConfig.authToken}`, + 'content-type': 'multipart/form-data', + }, + }); + }; + + if (metaData && Object.keys(metaData).length > 0) { + const insertRequest = this.createInsertRequest(files.length, metaData); + this.insertDataCallInMultiFiles( + insertRequest, this.#client, tableName as string, clientConfig.authToken as string, + ).then((response: any) => { + const skyflowIDs = this.extractSkyflowIDs(response); + if (skyflowIDs.length === 0) { + rootReject({ error: 'No skyflow IDs returned from insert data' }); + return; } - const client = this.#client; - const promise1 = new Promise((resolve, reject) => { - client - .request({ - body: formData, - requestMethod: 'POST', - url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}/${skyflowIDs[index]}/files`, - headers: { - authorization: `Bearer ${clientConfig.authToken}`, - 'content-type': 'multipart/form-data', - }, - }) - .then((response1) => { - resolve(response1); - }) - .catch((error) => { - reject(error); - }); + const promises = files.map((file, idx) => uploadFile(file, skyflowIDs[idx])); + Promise.allSettled(promises).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + resultSet.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value !== undefined && result.value !== null) { + if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { + errorResponse.push(result.value); + } else { + const response1 = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response1); + } + } + } else if (result.status === 'rejected') { + errorResponse.push({ error: result.reason }); + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }).catch((error) => { + printLog(`${error}`, MessageType.LOG, this.context?.logLevel); + rootReject({ + error: error?.error || error, }); - promises.push(promise1); }); - Promise.allSettled( - promises, - ).then((resultSet) => { + } else { + const promises = files.map((file) => uploadFile(file)); + Promise.allSettled(promises).then((resultSet) => { const fileUploadResponse: any[] = []; const errorResponse: any[] = []; resultSet.forEach((result) => { @@ -547,12 +571,7 @@ export default class FrameElementInit { } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); else rootReject({ fileUploadResponse, errorResponse }); }); - }).catch((error) => { - printLog(`${error}`, MessageType.LOG, this.context?.logLevel); - rootReject({ - error: error?.error || error, - }); - }); + } }); private validateFiles = (files: File[], state: any, fileElement: IFrameFormElement) => { diff --git a/src/utils/validators/index.ts b/src/utils/validators/index.ts index c3d26997..1cf03de7 100644 --- a/src/utils/validators/index.ts +++ b/src/utils/validators/index.ts @@ -560,10 +560,6 @@ export const validateCollectElementInput = (input: CollectElementInput, logLevel if (Object.prototype.hasOwnProperty.call(input, 'skyflowID') && !(typeof input.skyflowID === 'string')) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_SKYFLOWID_IN_COLLECT, [], true); } - if (input.type === ElementType.FILE_INPUT - && !Object.keys(input).includes('skyflowID')) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_SKYFLOWID_IN_COLLECT, [], true); - } }; export const validateUpsertOptions = (upsertOptions) => { From afa427716201b3b6f3d4adf00f3641eb21e26b1d Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 07:46:37 +0000 Subject: [PATCH 10/47] [AUTOMATED] Release - 2.5.0-beta.9-dev.99fa815 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55bd50d4..c086c343 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.9", + "version": "2.5.0-beta.9-dev.99fa815", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From fcfbd99c49bb50b87e11b15e55f47db8aacbf448 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 14:33:13 +0530 Subject: [PATCH 11/47] SK-2254: fix 0px height in composable reveal --- .../reveal/composable-reveal-internal.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts index 618f753e..2e5a2cbf 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -54,6 +54,8 @@ class ComposableRevealInternalElement extends SkyflowElement { #elementId: string; + resizeObserver: ResizeObserver | null; + #readyToMount: boolean = false; #eventEmitter: EventEmitter; @@ -83,6 +85,7 @@ class ComposableRevealInternalElement extends SkyflowElement { super(); this.#elementId = elementId; this.#metaData = metaData; + this.resizeObserver = null; this.#clientId = this.#metaData?.uuid; this.#isSingleElementAPI = isSingleElementAPI; this.#recordData = recordGroup; @@ -185,6 +188,20 @@ class ComposableRevealInternalElement extends SkyflowElement { if (!domElementSelector) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['RevealElement'], true); } + + if(domElementSelector instanceof HTMLElement){ + this.resizeObserver = new ResizeObserver(() => { + if (domElementSelector.getElementsByTagName('iframe')[0]?.contentWindow) { + const iframeElement = domElementSelector.getElementsByTagName('iframe')[0] + if(iframeElement.name === this.#iframe.name){ + iframeElement?.contentWindow?.postMessage({ + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }); + } + updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElementSelector); if ( this.#metaData?.clientJSON?.config?.options?.trackMetrics @@ -193,6 +210,15 @@ class ComposableRevealInternalElement extends SkyflowElement { pushElementEventWithTimeout(this.#elementId); } + if (typeof domElementSelector === 'string') { + const targetElement = document.querySelector(domElementSelector); + if (targetElement) { + this.resizeObserver?.observe(targetElement); + } + } else if (domElementSelector instanceof HTMLElement) { + this.resizeObserver?.observe(domElementSelector); + } + this.#readyToMount = true; if (this.#readyToMount) { this.#iframe.mount(domElementSelector, undefined, { From 3e89042a4c8a6185a31188fbee161d0765161916 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 09:04:49 +0000 Subject: [PATCH 12/47] [AUTOMATED] Release - 2.5.0-beta.9-dev.fcfbd99 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c086c343..325b02da 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.9-dev.99fa815", + "version": "2.5.0-beta.9-dev.fcfbd99", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From ff71850aaea094c49fc14ffe227b4b6d85ef3942 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 16:13:36 +0530 Subject: [PATCH 13/47] SK-2254: add resize observer for multiple iframes --- src/core/external/collect/collect-element.ts | 19 +++++++++++++------ .../reveal/composable-reveal-internal.ts | 19 +++++++++++++------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index c4cb3e25..dee013bc 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -206,12 +206,19 @@ class CollectElement extends SkyflowElement { if(domElement instanceof HTMLElement){ this.resizeObserver = new ResizeObserver(() => { - if (domElement.getElementsByTagName('iframe')[0]?.contentWindow) { - const iframeElement = domElement.getElementsByTagName('iframe')[0] - if(iframeElement.name === this.#iframe.name){ - iframeElement?.contentWindow?.postMessage({ - name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - }, properties.IFRAME_SECURE_ORIGIN); + const iframeElements = domElement.getElementsByTagName('iframe'); + for (let i = 0; i < iframeElements.length; i++) { + const iframeElement = iframeElements[i]; + if ( + iframeElement.name === this.#iframe.name && + iframeElement.contentWindow + ) { + iframeElement?.contentWindow?.postMessage( + { + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, + properties.IFRAME_SECURE_ORIGIN + ); } } }); diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts index 2e5a2cbf..f9b26bc6 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -191,12 +191,19 @@ class ComposableRevealInternalElement extends SkyflowElement { if(domElementSelector instanceof HTMLElement){ this.resizeObserver = new ResizeObserver(() => { - if (domElementSelector.getElementsByTagName('iframe')[0]?.contentWindow) { - const iframeElement = domElementSelector.getElementsByTagName('iframe')[0] - if(iframeElement.name === this.#iframe.name){ - iframeElement?.contentWindow?.postMessage({ - name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - }, properties.IFRAME_SECURE_ORIGIN); + const iframeElements = domElementSelector.getElementsByTagName('iframe'); + for (let i = 0; i < iframeElements.length; i++) { + const iframeElement = iframeElements[i]; + if ( + iframeElement.name === this.#iframe.name && + iframeElement.contentWindow + ) { + iframeElement?.contentWindow?.postMessage( + { + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, + properties.IFRAME_SECURE_ORIGIN + ); } } }); From f295fe0c87966a50a6252ae3103422b712ed820a Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 10:44:46 +0000 Subject: [PATCH 14/47] [AUTOMATED] Release - 2.5.0-beta.9-dev.ff71850 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 325b02da..a818b825 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.9-dev.fcfbd99", + "version": "2.5.0-beta.9-dev.ff71850", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 3d1ed74f3421e3dc2a1e5c13e0d87fe4752ab1d9 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 16:57:19 +0530 Subject: [PATCH 15/47] SK-2254: add undefined check for resize observer for multiple iframes --- src/core/external/collect/collect-element.ts | 26 ++++++++++--------- .../reveal/composable-reveal-internal.ts | 26 ++++++++++--------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index dee013bc..93a5bb8c 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -207,18 +207,20 @@ class CollectElement extends SkyflowElement { if(domElement instanceof HTMLElement){ this.resizeObserver = new ResizeObserver(() => { const iframeElements = domElement.getElementsByTagName('iframe'); - for (let i = 0; i < iframeElements.length; i++) { - const iframeElement = iframeElements[i]; - if ( - iframeElement.name === this.#iframe.name && - iframeElement.contentWindow - ) { - iframeElement?.contentWindow?.postMessage( - { - name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - }, - properties.IFRAME_SECURE_ORIGIN - ); + if (iframeElements && iframeElements.length > 0) { + for (let i = 0; i < iframeElements.length; i++) { + const iframeElement = iframeElements[i]; + if ( + iframeElement.name === this.#iframe.name && + iframeElement.contentWindow + ) { + iframeElement?.contentWindow?.postMessage( + { + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, + properties.IFRAME_SECURE_ORIGIN + ); + } } } }); diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts index f9b26bc6..9453a06a 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -192,18 +192,20 @@ class ComposableRevealInternalElement extends SkyflowElement { if(domElementSelector instanceof HTMLElement){ this.resizeObserver = new ResizeObserver(() => { const iframeElements = domElementSelector.getElementsByTagName('iframe'); - for (let i = 0; i < iframeElements.length; i++) { - const iframeElement = iframeElements[i]; - if ( - iframeElement.name === this.#iframe.name && - iframeElement.contentWindow - ) { - iframeElement?.contentWindow?.postMessage( - { - name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - }, - properties.IFRAME_SECURE_ORIGIN - ); + if (iframeElements && iframeElements.length > 0) { + for (let i = 0; i < iframeElements.length; i++) { + const iframeElement = iframeElements[i]; + if ( + iframeElement.name === this.#iframe.name && + iframeElement.contentWindow + ) { + iframeElement?.contentWindow?.postMessage( + { + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, + properties.IFRAME_SECURE_ORIGIN + ); + } } } }); From fcc6e87460cdaafe075406b38356be4f82403a46 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 11:28:25 +0000 Subject: [PATCH 16/47] [AUTOMATED] Release - 2.5.0-beta.9-dev.3d1ed74 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a818b825..d677e338 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.9-dev.ff71850", + "version": "2.5.0-beta.9-dev.3d1ed74", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From dd803c135a5de151b973850061da2a92242b6ce8 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 18:25:59 +0000 Subject: [PATCH 17/47] [AUTOMATED] Release - 2.5.0-beta.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d677e338..6d849e7a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.9-dev.3d1ed74", + "version": "2.5.0-beta.10", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 08ce5476eeb7052a5958c8676a37d976ff1e9901 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 16 Oct 2025 18:39:26 +0530 Subject: [PATCH 18/47] SK-2330 fix the lint error --- src/core/internal/frame-element-init.ts | 7 +++++-- tests/core/external/collect/composable-container.test.js | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/internal/frame-element-init.ts b/src/core/internal/frame-element-init.ts index a3b033ca..c615d1e9 100644 --- a/src/core/internal/frame-element-init.ts +++ b/src/core/internal/frame-element-init.ts @@ -464,11 +464,14 @@ export default class FrameElementInit { // eslint-disable-next-line consistent-return private multipleUploadFiles = - (fileElement: IFrameFormElement, clientConfig, metaData) => new Promise((rootResolve, rootReject) => { + (fileElement: IFrameFormElement, + clientConfig, metaData) => new Promise((rootResolve, rootReject) => { this.#client = new Client(clientConfig, {}); if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); - const { state, tableName, onFocusChange, preserveFileName } = fileElement; + const { + state, tableName, onFocusChange, preserveFileName, + } = fileElement; if (state.isRequired) { onFocusChange(false); } diff --git a/tests/core/external/collect/composable-container.test.js b/tests/core/external/collect/composable-container.test.js index 4dd7be88..7ca7037e 100644 --- a/tests/core/external/collect/composable-container.test.js +++ b/tests/core/external/collect/composable-container.test.js @@ -18,7 +18,7 @@ const bus = require('framebus'); iframerUtils.getIframeSrc = jest.fn(() => ('https://google.com')); -const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve('token')); const mockUuid = '1234'; jest.mock('../../../../src/libs/uuid', () => ({ @@ -57,6 +57,7 @@ EventEmitter.mockImplementation(()=>({ const metaData = { + getSkyflowBearerToken: getBearerToken, skyflowContainer:{ isControllerFrameReady: true }, @@ -78,6 +79,7 @@ const metaData = { }, }; const metaData2 = { + getSkyflowBearerToken: getBearerToken, skyflowContainer:{ isControllerFrameReady: false }, From 6d64db036338e83bd58482d6833120839d8e0dc4 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 16 Oct 2025 13:10:13 +0000 Subject: [PATCH 19/47] [AUTOMATED] Release - 2.5.0-beta.10-dev.08ce547 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d849e7a..5f39e47d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.10", + "version": "2.5.0-beta.10-dev.08ce547", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From b1f692066583e45ba5c76a763c860cc1416d634e Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow <156889717+saileshwar-skyflow@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:41:26 +0530 Subject: [PATCH 20/47] SK-2219: Public release with TS gaps fix (#631) * SK-2219 Fix typescript gaps in JS SDK --- package.json | 2 +- .../skyflow-elements-update/src/index.ts | 343 +-- src/client/index.ts | 25 +- src/core-utils/collect.ts | 12 +- src/core-utils/delete.ts | 8 +- src/core-utils/reveal.ts | 56 +- .../external/collect/collect-container.ts | 82 +- src/core/external/collect/collect-element.ts | 15 +- .../collect/compose-collect-container.ts | 39 +- .../collect/compose-collect-element.ts | 2 +- src/core/external/common/iframe.ts | 8 +- src/core/external/common/skyflow-element.ts | 14 +- src/core/external/reveal/reveal-container.ts | 15 +- src/core/external/reveal/reveal-element.ts | 17 +- src/core/external/skyflow-container.ts | 2 +- src/core/internal/internal-types/index.ts | 83 + .../skyflow-frame/skyflow-frame-controller.ts | 127 +- src/libs/element-options.ts | 17 +- src/libs/jss-styles.ts | 2 +- src/libs/styles.ts | 8 +- src/skyflow.ts | 48 +- src/utils/bus-events/index.ts | 2 +- src/utils/common/index.ts | 19 +- src/utils/helpers/index.ts | 10 +- src/utils/validators/index.ts | 12 +- tests/client.test.ts | 411 ++++ tests/core-utils/collect.test.ts | 168 ++ .../collect/collect-container.test.js | 153 +- .../collect/collect-container.test.ts | 266 ++- .../external/collect/collect-element.test.ts | 1359 ++++++++++++ .../collect/composable-container.test.js | 41 +- .../collect/composable-container.test.ts | 453 ++++ .../collect/composable-element.test.ts | 111 + .../external/reveal/reveal-container.test.ts | 437 ++++ .../external/reveal/reveal-element.test.ts | 1019 +++++++++ ...w-frame-controller-upload-tokenize.test.js | 121 +- ...w-frame-controller-upload-tokenize.test.ts | 1873 +++++++++++++++++ .../skyflow-frame-controller.test.ts | 1697 +++++++++++++++ 38 files changed, 8529 insertions(+), 548 deletions(-) create mode 100644 src/core/internal/internal-types/index.ts create mode 100644 tests/client.test.ts create mode 100644 tests/core-utils/collect.test.ts create mode 100644 tests/core/external/collect/collect-element.test.ts create mode 100644 tests/core/external/collect/composable-container.test.ts create mode 100644 tests/core/external/collect/composable-element.test.ts create mode 100644 tests/core/external/reveal/reveal-container.test.ts create mode 100644 tests/core/external/reveal/reveal-element.test.ts create mode 100644 tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts create mode 100644 tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts diff --git a/package.json b/package.json index 552645f6..d03b0994 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.3", + "version": "2.5.0-beta.10-dev.8bf4fef", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", diff --git a/samples/using-typescript/skyflow-elements-update/src/index.ts b/samples/using-typescript/skyflow-elements-update/src/index.ts index 9168aeb9..f50d4460 100644 --- a/samples/using-typescript/skyflow-elements-update/src/index.ts +++ b/samples/using-typescript/skyflow-elements-update/src/index.ts @@ -2,7 +2,7 @@ Copyright (c) 2025 Skyflow, Inc. */ import Skyflow, { - CollectContainer, + CollectContainer, CollectElement, CollectElementInput, CollectElementOptions, @@ -48,11 +48,13 @@ try { logLevel: Skyflow.LogLevel.ERROR, env: Skyflow.Env.PROD, }, - } + }; const skyflowClient: Skyflow = Skyflow.init(config); // Create collect Container. - const collectContainer = skyflowClient.container(Skyflow.ContainerType.COLLECT) as CollectContainer; + const collectContainer = skyflowClient.container( + Skyflow.ContainerType.COLLECT + ) as CollectContainer; // Custom styles for collect elements. const collectStylesOptions = { @@ -128,7 +130,7 @@ try { label: "Cvv", placeholder: "cvv", type: Skyflow.ElementType.CVV, - } + }; const cvvElement: CollectElement = collectContainer.create(cvvInput); const expiryDateInput: CollectElementInput = { @@ -138,8 +140,9 @@ try { label: "Expiry Date", placeholder: "MM/YYYY", type: Skyflow.ElementType.EXPIRATION_DATE, - } - const expiryDateElement: CollectElement = collectContainer.create(expiryDateInput); + }; + const expiryDateElement: CollectElement = + collectContainer.create(expiryDateInput); const cardholderNameInput: CollectElementInput = { table: "pii_fields", @@ -148,8 +151,9 @@ try { label: "Card Holder Name", placeholder: "cardholder name", type: Skyflow.ElementType.CARDHOLDER_NAME, - } - const cardHolderNameElement: CollectElement = collectContainer.create(cardholderNameInput); + }; + const cardHolderNameElement: CollectElement = + collectContainer.create(cardholderNameInput); // Mount the elements. cardNumberElement.mount("#collectCardNumber"); @@ -184,11 +188,15 @@ try { cardNumberElement.on(Skyflow.EventName.CHANGE, (state: ElementState) => { if (state.isValid) { // update cvv element validation rule. - if (findCvvLength((state.value) as string) === 3) { - const updateOptions: CollectElementUpdateOptions = { validations: [length3Rule] }; + if (findCvvLength(state.value as string) === 3) { + const updateOptions: CollectElementUpdateOptions = { + validations: [length3Rule], + }; cvvElement.update(updateOptions); } else { - const updateOptions: CollectElementUpdateOptions = { validations: [length4Rule] }; + const updateOptions: CollectElementUpdateOptions = { + validations: [length4Rule], + }; cvvElement.update(updateOptions); } } @@ -225,175 +233,194 @@ try { } // Collect all elements data. - const collectButton = document.getElementById("collectPCIData") as HTMLButtonElement; + const collectButton = document.getElementById( + "collectPCIData" + ) as HTMLButtonElement; if (collectButton) { collectButton.addEventListener("click", () => { - const collectResponse: Promise = collectContainer.collect(); + const collectResponse: Promise = + collectContainer.collect(); collectResponse .then((response: CollectResponse) => { console.log(response); collectResponseData = response; - const responseElement = document.getElementById('collectResponse') as HTMLElement; + const responseElement = document.getElementById( + "collectResponse" + ) as HTMLElement; if (responseElement) { responseElement.innerHTML = JSON.stringify(response, null, 2); } - }) - .catch((err: CollectResponse) => { - const errorElement = document.getElementById('collectResponse') as HTMLElement; - if (errorElement){ - errorElement.innerHTML = JSON.stringify(err, null, 2); - } - console.log(err); - }); - }); - } - revealView!.style.visibility = "visible"; + revealView.style.visibility = "visible"; - const revealStyleOptions = { - inputStyles: { - base: { - border: "1px solid #eae8ee", - padding: "10px 16px", - borderRadius: "4px", - color: "#1d1d1d", - marginTop: "4px", - fontFamily: '"Roboto", sans-serif', - }, - global: { - "@import": - 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - }, - } as InputStyles, - labelStyles: { - base: { - fontSize: "16px", - fontWeight: "bold", - fontFamily: '"Roboto", sans-serif', - }, - global: { - "@import": - 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - }, - } as LabelStyles, - errorTextStyles: { - base: { - color: "#f44336", - paddingLeft: "20px", - fontFamily: '"Roboto", sans-serif', - }, - global: { - "@import": - 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', - }, - } as ErrorTextStyles, - }; + const revealStyleOptions = { + inputStyles: { + base: { + border: "1px solid #eae8ee", + padding: "10px 16px", + borderRadius: "4px", + color: "#1d1d1d", + marginTop: "4px", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as InputStyles, + labelStyles: { + base: { + fontSize: "16px", + fontWeight: "bold", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as LabelStyles, + errorTextStyles: { + base: { + color: "#f44336", + paddingLeft: "20px", + fontFamily: '"Roboto", sans-serif', + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + } as ErrorTextStyles, + }; - // Create Reveal Elements With Tokens. - const fieldsTokenData = collectResponseData.records![0].fields; - const revealContainer = skyflowClient.container(Skyflow.ContainerType.REVEAL) as RevealContainer; - - const revealCardNumberInput: RevealElementInput = { - token: fieldsTokenData.card_number, - label: "Card Number", - ...revealStyleOptions, - }; - const revealCardNumberElement: RevealElement = revealContainer.create(revealCardNumberInput); - revealCardNumberElement.mount("#revealCardNumber"); + // Create Reveal Elements With Tokens. + const fieldsTokenData = collectResponseData.records![0].fields; + const revealContainer = skyflowClient.container( + Skyflow.ContainerType.REVEAL + ) as RevealContainer; - const revealCardCvvInput: RevealElementInput = { - token: fieldsTokenData.cvv, - label: "CVV", - ...revealStyleOptions, - altText: "###", - }; - const revealCardCvvElement: RevealElement = revealContainer.create(revealCardCvvInput); - revealCardCvvElement.mount("#revealCvv"); + const revealCardNumberInput: RevealElementInput = { + token: fieldsTokenData.card_number, + label: "Card Number", + ...revealStyleOptions, + }; + const revealCardNumberElement: RevealElement = revealContainer.create( + revealCardNumberInput + ); + revealCardNumberElement.mount("#revealCardNumber"); - const revealCardExpiryInput: RevealElementInput = { - token: fieldsTokenData.expiration_date, - label: "Card Expiry Date", - ...revealStyleOptions, - }; - const revealCardExpiryElement: RevealElement = revealContainer.create(revealCardExpiryInput); - revealCardExpiryElement.mount("#revealExpiryDate"); + const revealCardCvvInput: RevealElementInput = { + token: fieldsTokenData.cvv, + label: "CVV", + ...revealStyleOptions, + altText: "###", + }; + const revealCardCvvElement: RevealElement = + revealContainer.create(revealCardCvvInput); + revealCardCvvElement.mount("#revealCvv"); - const revealCardholderNameInput: RevealElementInput = { - token: fieldsTokenData.name, - label: "Card Holder Name", - ...revealStyleOptions, - }; - const revealCardholderNameElement: RevealElement = revealContainer.create(revealCardholderNameInput); - revealCardholderNameElement.mount("#revealCardholderName"); + const revealCardExpiryInput: RevealElementInput = { + token: fieldsTokenData.expiration_date, + label: "Card Expiry Date", + ...revealStyleOptions, + }; + const revealCardExpiryElement: RevealElement = revealContainer.create( + revealCardExpiryInput + ); + revealCardExpiryElement.mount("#revealExpiryDate"); - const revealButton = document.getElementById("revealPCIData") as HTMLButtonElement; + const revealCardholderNameInput: RevealElementInput = { + token: fieldsTokenData.name, + label: "Card Holder Name", + ...revealStyleOptions, + }; + const revealCardholderNameElement: RevealElement = + revealContainer.create(revealCardholderNameInput); + revealCardholderNameElement.mount("#revealCardholderName"); - // update Reveal elements' properties - const updateRevealElementsButton = document.getElementById( - "updateRevealElements" - ) as HTMLButtonElement; - if (updateRevealElementsButton) { - updateRevealElementsButton.addEventListener("click", () => { - // update label,inputStyles on cardholderName, - revealCardholderNameElement.update({ - label: "CARDHOLDER NAME", - inputStyles: { - base: { - color: "#aa11aa", - }, - }, - } as RevealElementInput); + const revealButton = document.getElementById( + "revealPCIData" + ) as HTMLButtonElement; - // update label,labelSyles on card number - revealCardNumberElement.update({ - label: "CARD NUMBER", - labelStyles: { - base: { - borderWidth: "5px", - }, - }, - } as RevealElementInput); + // update Reveal elements' properties + const updateRevealElementsButton = document.getElementById( + "updateRevealElements" + ) as HTMLButtonElement; + if (updateRevealElementsButton) { + updateRevealElementsButton.addEventListener("click", () => { + // update label,inputStyles on cardholderName, + revealCardholderNameElement.update({ + label: "CARDHOLDER NAME", + inputStyles: { + base: { + color: "#aa11aa", + }, + }, + } as RevealElementInput); - // update redaction,inputStyles on expiry date - revealCardExpiryElement.update({ - redaction: Skyflow.RedactionType.REDACTED, - inputStyles: { - base: { - backgroundColor: "#000", - color: "#fff", - }, - }, - } as RevealElementInput); + // update label,labelSyles on card number + revealCardNumberElement.update({ + label: "CARD NUMBER", + labelStyles: { + base: { + borderWidth: "5px", + }, + }, + } as RevealElementInput); - // update altText,token,inputStyles,errorTextStyles on cvv - revealCardCvvElement.update({ - altText: "XXXX-XX", - token: "new-random-roken", - inputStyles: { - base: { - color: "#fff", - backgroundColor: "#000", - borderColor: "#f00", - borderWidth: "5px", - }, - }, - errorTextStyles: { - base: { - backgroundColor: "#000", - border: "1px #f00 solid", - }, - }, - } as RevealElementInput); - }); - } + // update redaction,inputStyles on expiry date + revealCardExpiryElement.update({ + redaction: Skyflow.RedactionType.REDACTED, + inputStyles: { + base: { + backgroundColor: "#000", + color: "#fff", + }, + }, + } as RevealElementInput); - if (revealButton) { - revealButton.addEventListener("click", () => { - const revealResponse: Promise = revealContainer.reveal(); - revealResponse.then((res: RevealResponse) => { - console.log(res); + // update altText,token,inputStyles,errorTextStyles on cvv + revealCardCvvElement.update({ + altText: "XXXX-XX", + token: "new-random-roken", + inputStyles: { + base: { + color: "#fff", + backgroundColor: "#000", + borderColor: "#f00", + borderWidth: "5px", + }, + }, + errorTextStyles: { + base: { + backgroundColor: "#000", + border: "1px #f00 solid", + }, + }, + } as RevealElementInput); + }); + } + + if (revealButton) { + revealButton.addEventListener("click", () => { + const revealResponse: Promise = + revealContainer.reveal(); + revealResponse + .then((res: RevealResponse) => { + console.log(res); + }) + .catch((err: RevealResponse) => { + console.log(err); + }); + }); + } }) - .catch((err: RevealResponse) => { + .catch((err: CollectResponse) => { + const errorElement = document.getElementById( + "collectResponse" + ) as HTMLElement; + if (errorElement) { + errorElement.innerHTML = JSON.stringify(err, null, 2); + } console.log(err); }); }); diff --git a/src/client/index.ts b/src/client/index.ts index 3f51f25c..c13e5a32 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -10,9 +10,10 @@ import sdkDetails from '../../package.json'; import { getMetaObject, } from '../utils/helpers'; +import { ClientMetadata } from '../core/internal/internal-types'; export interface IClientRequest { - body?: any; + body?: Document | XMLHttpRequestBodyInit | null; headers?: Record; requestMethod: | 'GET' @@ -30,24 +31,30 @@ export interface SdkInfo { sdkName: string; sdkVersion: string; } + +export interface ClientToJSON { + config: ISkyflow; + metaData: ClientMetadata; +} + class Client { config: ISkyflow; - #metaData: any; + #metaData: ClientMetadata; - constructor(config: ISkyflow, metadata) { + constructor(config: ISkyflow, metadata: ClientMetadata) { this.config = config; this.#metaData = metadata; } - toJSON() { + toJSON(): ClientToJSON { return { config: this.config, metaData: this.#metaData, }; } - static fromJSON(json) { + static fromJSON(json: ClientToJSON) { return new Client(json.config, json.metaData); } @@ -63,7 +70,7 @@ class Client { if (request.headers) { const metaDataObject = getMetaObject(sdkDetails, this.#metaData, navigator); request.headers[SKY_METADATA_HEADER] = JSON.stringify(metaDataObject); - const { headers } = request; + const headers = request.headers; Object.keys(request.headers).forEach((key) => { if (!(key === 'content-type' && headers[key] && headers[key].includes(ContentType.FORMDATA))) { httpRequest.setRequestHeader(key, headers[key]); @@ -75,7 +82,11 @@ class Client { || request.headers?.['content-type']?.includes(ContentType.FORMDATA)) { httpRequest.send(request.body); } else { - httpRequest.send(JSON.stringify({ ...request.body })); + /* Earlier we were stringifying here, but due to TS, we're stringifying + at the point where we are creating the request. Since the body parameter + doesn't accept JSON object. + */ + httpRequest.send(request.body); } httpRequest.onload = () => { diff --git a/src/core-utils/collect.ts b/src/core-utils/collect.ts index e4672d8a..93181075 100644 --- a/src/core-utils/collect.ts +++ b/src/core-utils/collect.ts @@ -10,10 +10,12 @@ import { getAccessToken } from '../utils/bus-events'; import { IInsertRecordInput, IInsertRecord, IValidationRule, ValidationRuleType, MessageType, LogLevel, + InsertResponse, } from '../utils/common'; import SKYFLOW_ERROR_CODE from '../utils/constants'; import { printLog } from '../utils/logs-helper'; import IFrameFormElement from '../core/internal/iframe-form'; +import { BatchInsertRequestBody } from '../core/internal/internal-types'; export interface IUpsertOptions{ table: string, @@ -35,8 +37,8 @@ export const getUpsertColumn = (tableName: string, options:Array export const constructInsertRecordRequest = ( records: IInsertRecordInput, options: Record = { tokens: true }, -) => { - const requestBody: any = []; +): Array => { + const requestBody: Array = []; if (options?.tokens || options === null) { records.records.forEach((record, index) => { const upsertColumn = getUpsertColumn(record.table, options.upsert); @@ -74,7 +76,7 @@ export const constructInsertRecordResponse = ( responseBody: any, tokens: boolean, records: IInsertRecord[], -) => { +): InsertResponse => { if (tokens) { return { records: responseBody.responses @@ -207,12 +209,12 @@ const updateRecordsInVault = ( skyflowIdRecord.fields = omit(skyflowIdRecord.fields, 'table'); skyflowIdRecord.fields = omit(skyflowIdRecord.fields, 'skyflowID'); return client.request({ - body: { + body: JSON.stringify({ record: { fields: { ...skyflowIdRecord.fields }, }, tokenization: options?.tokens !== undefined ? options.tokens : true, - }, + }), requestMethod: 'PUT', url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${table}/${skyflowID}`, headers: { diff --git a/src/core-utils/delete.ts b/src/core-utils/delete.ts index 01f1c946..0b6a6a0f 100644 --- a/src/core-utils/delete.ts +++ b/src/core-utils/delete.ts @@ -2,7 +2,9 @@ import Client from '../client'; import SkyflowError from '../libs/skyflow-error'; import { getAccessToken } from '../utils/bus-events'; import { + IDeleteOptions, IDeleteRecord, + IDeleteRecordInput, IDeleteResponseType, LogLevel, MessageType, @@ -40,9 +42,9 @@ export const deleteRecordsFromVault = async ( }; export const deleteData = async ( - records, - options, - client, + records: IDeleteRecordInput, + options: IDeleteOptions, + client: Client, ): Promise => { const clientId = client.toJSON()?.metaData?.uuid || ''; diff --git a/src/core-utils/reveal.ts b/src/core-utils/reveal.ts index 7285716d..7e11fc37 100644 --- a/src/core-utils/reveal.ts +++ b/src/core-utils/reveal.ts @@ -11,6 +11,11 @@ import { IRenderResponseType, IGetOptions, RenderFileResponse, + RevealResponse, + GetResponse, + GetResponseRecord, + GetByIdResponse, + GetByIdResponseRecord, } from '../utils/common'; import { printLog } from '../utils/logs-helper'; import { FILE_DOWNLOAD_URL_PARAM } from '../core/constants'; @@ -52,7 +57,7 @@ const getRecordsFromVault = ( client: Client, authToken:string, options?: IGetOptions, -) => { +): Promise => { let paramList: string = ''; skyflowIdRecord.ids?.forEach((skyflowId) => { @@ -81,13 +86,13 @@ const getRecordsFromVault = ( authorization: `Bearer ${authToken}`, 'content-type': 'application/json', }, - }); + }) as Promise; }; const getSkyflowIdRecordsFromVault = ( skyflowIdRecord: ISkyflowIdRecord, client: Client, authToken:string, -) => { +): Promise => { let paramList: string = ''; skyflowIdRecord.ids.forEach((skyflowId) => { @@ -103,7 +108,7 @@ const getSkyflowIdRecordsFromVault = ( authorization: `Bearer ${authToken}`, 'content-type': 'application/json', }, - }); + }) as Promise; }; const getTokenRecordsFromVault = ( token:string, @@ -120,14 +125,14 @@ const getTokenRecordsFromVault = ( 'content-type': 'application/json', }, body: - { + JSON.stringify({ detokenizationParameters: [ { token, redaction, }, ], - }, + }), }); }; @@ -271,36 +276,43 @@ export const formatForRenderClient = (response: IRenderResponseType, column: str return formattedResponse; }; -export const formatRecordsForClient = (response: IRevealResponseType) => { +export const formatRecordsForClient = (response: IRevealResponseType): RevealResponse => { + const revealResponse: RevealResponse = {}; if (response.records) { const successRecords = response.records.map((record) => ({ token: record.token, valueType: record.valueType, })); - if (response.errors) return { success: successRecords, errors: response.errors }; - return { success: successRecords }; + revealResponse.success = successRecords; } - return { errors: response.errors }; + if (response.errors) { + const errorRecords = response.errors.map((errorRecord) => ({ + token: errorRecord.token, + error: errorRecord.error, + })); + revealResponse.errors = errorRecords; + } + return revealResponse; }; export const fetchRecordsGET = async ( skyflowIdRecords: IGetRecord[], client: Client, options?: IGetOptions, -) => new Promise((rootResolve, rootReject) => { +): Promise => new Promise((rootResolve, rootReject) => { let vaultResponseSet: Promise[]; const clientId = client.toJSON()?.metaData?.uuid || ''; getAccessToken(clientId).then((authToken) => { vaultResponseSet = skyflowIdRecords.map( - (skyflowIdRecord) => new Promise((resolve, reject) => { + (skyflowIdRecord: IGetRecord) => new Promise((resolve, reject) => { getRecordsFromVault(skyflowIdRecord, client, authToken as string, options) .then( - (resolvedResult: any) => { - const response: any[] = []; - const recordsData: any[] = resolvedResult.records; + (resolvedResult: GetResponse) => { + const response: GetResponseRecord[] = []; + const recordsData: GetResponseRecord[] = resolvedResult.records || []; recordsData.forEach((fieldData) => { const id = fieldData.fields.skyflow_id; - const currentRecord = { + const currentRecord: GetResponseRecord = { fields: { id, ...fieldData.fields, @@ -329,7 +341,7 @@ export const fetchRecordsGET = async ( reject(errorResponse); }, ) - .catch((error) => { + .catch((error: unknown) => { reject(error); }); }), @@ -358,20 +370,20 @@ export const fetchRecordsGET = async ( export const fetchRecordsBySkyflowID = async ( skyflowIdRecords: ISkyflowIdRecord[], client: Client, -) => new Promise((rootResolve, rootReject) => { +): Promise => new Promise((rootResolve, rootReject) => { let vaultResponseSet: Promise[]; const clientId = client.toJSON()?.metaData?.uuid || ''; getAccessToken(clientId).then((authToken) => { vaultResponseSet = skyflowIdRecords.map( - (skyflowIdRecord) => new Promise((resolve, reject) => { + (skyflowIdRecord: ISkyflowIdRecord) => new Promise((resolve, reject) => { getSkyflowIdRecordsFromVault(skyflowIdRecord, client, authToken as string) .then( - (resolvedResult: any) => { + (resolvedResult: GetByIdResponse) => { const response: any[] = []; - const recordsData: any[] = resolvedResult.records; + const recordsData: GetByIdResponseRecord[] = resolvedResult.records || []; recordsData.forEach((fieldData) => { const id = fieldData.fields.skyflow_id; - const currentRecord = { + const currentRecord: GetByIdResponseRecord = { fields: { id, ...fieldData.fields, diff --git a/src/core/external/collect/collect-container.ts b/src/core/external/collect/collect-container.ts index 285f4bc6..2642d236 100644 --- a/src/core/external/collect/collect-container.ts +++ b/src/core/external/collect/collect-container.ts @@ -17,6 +17,7 @@ import { CollectResponse, ICollectOptions, UploadFilesResponse, + ContainerOptions, } from '../../../utils/common'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import logs from '../../../utils/logs'; @@ -32,11 +33,40 @@ import { CONTROLLER_STYLES, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, FRAME_ELEMENT, COLLECT_TYPES, + ElementType, } from '../../constants'; import Container from '../common/container'; import CollectElement from './collect-element'; import EventEmitter from '../../../event-emitter'; import properties from '../../../properties'; +import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; + +export interface ICollectElement { + elementType: ElementType; + elementName: string; + name: string; + table?: string; + column?: string; + sensitive?: boolean; + replacePattern?: RegExp; + mask?: string[]; + value?: string; + isMounted: boolean; + [key: string]: unknown; +} + +export interface ElementGroupItem extends CollectElementInput, CollectElementOptions { + elementType: ElementType; + name?: string; + accept?: string[]; + elementName?: string; +} + +export interface ElementGroup { + rows: Array<{ + elements: Array; + }>; +} const CLASS_NAME = 'CollectContainer'; class CollectContainer extends Container { @@ -44,11 +74,11 @@ class CollectContainer extends Container { #elements: Record = {}; - #metaData: any; + #metaData: Metadata; - #context:Context; + #context: Context; - #skyflowElements:any; + #skyflowElements: Array; type:string = ContainerType.COLLECT; @@ -58,7 +88,12 @@ class CollectContainer extends Container { #isSkyflowFrameReady: boolean = false; - constructor(options, metaData, skyflowElements, context) { + constructor( + metaData: Metadata, + skyflowElements: Array, + context: Context, + options?: ContainerOptions, + ) { super(); this.#isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; this.#containerId = uuid(); @@ -97,35 +132,34 @@ class CollectContainer extends Container { create = (input: CollectElementInput, options: CollectElementOptions = { required: false, - }) => { + }): CollectElement => { validateCollectElementInput(input, this.#context.logLevel); const validations = formatValidations(input.validations); const formattedOptions = formatOptions(input.type, options, this.#context.logLevel); - const elementGroup = { - rows: [ - { - elements: [ - { - elementType: input.type, - name: input.column, - accept: options.allowedFileType, - ...input, - ...formattedOptions, - validations, - }, - ], - }, - ], + + const elementGroup: ElementGroup = { + rows: [{ + elements: [{ + elementType: input.type, + name: input.column, + accept: options.allowedFileType, + ...input, + ...formattedOptions, + validations, + }], + }], }; + return this.#createMultipleElement(elementGroup, true); }; #createMultipleElement = ( - multipleElements: any, + multipleElements: ElementGroup, isSingleElementAPI: boolean = false, - ) => { + ): CollectElement => { const elements: any[] = []; const tempElements = deepClone(multipleElements); + tempElements.rows.forEach((row) => { row.elements.forEach((element) => { const options = element; @@ -362,7 +396,7 @@ class CollectContainer extends Container { }); }; - uploadFiles = (options: ICollectOptions) :Promise => { + uploadFiles = (options?: ICollectOptions): Promise => { this.#isSkyflowFrameReady = this.#metaData.skyflowContainer.isControllerFrameReady; if (this.#isSkyflowFrameReady) { return new Promise((resolve, reject) => { @@ -472,7 +506,7 @@ class CollectContainer extends Container { if (!mountedIframeIds.length) return; this.#removeUnmountedElements(mountedIframeIds); - } catch (error) { + } catch (error: unknown) { printLog(`${error}`, MessageType.LOG, this.#context.logLevel); } }; diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index 298aa5d2..8dfc05de 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -41,6 +41,7 @@ import { pushElementEventWithTimeout, updateMetricObjectValue, } from '../../../metrics'; +import { Metadata, ContainerProps, InternalState } from '../../internal/internal-types'; const CLASS_NAME = 'Element'; class CollectElement extends SkyflowElement { @@ -54,7 +55,7 @@ class CollectElement extends SkyflowElement { #isSingleElementAPI: boolean = false; - #states: any[]; + #states: InternalState[]; #elements: any[]; @@ -72,7 +73,7 @@ class CollectElement extends SkyflowElement { #group: any; - #metaData: any; + #metaData: Metadata; #eventEmitter: EventEmitter = new EventEmitter(); @@ -97,8 +98,8 @@ class CollectElement extends SkyflowElement { constructor( elementId: string, elementGroup: any, - metaData: any, - container: any, + metaData: Metadata, + container: ContainerProps, isSingleElementAPI: boolean = false, destroyCallback: Function, updateCallback: Function, @@ -318,7 +319,7 @@ class CollectElement extends SkyflowElement { } }; - #onUpdate = (callback) => { + #onUpdate = (callback: Function) => { // todo: us bus if else there will be an infinite loop if (!this.#isSingleElementAPI) { this.#eventEmitter.on( @@ -331,7 +332,7 @@ class CollectElement extends SkyflowElement { } }; - updateElement = (elementOptions) => { + updateElement = (elementOptions: { elementName: string } & CollectElementUpdateOptions) => { this.#bus.emit(ELEMENT_EVENTS_TO_IFRAME.SET_VALUE + elementOptions.elementName, { name: elementOptions.elementName, options: elementOptions, @@ -446,7 +447,7 @@ class CollectElement extends SkyflowElement { } } - #onDestroy = (callback) => { + #onDestroy = (callback: Function) => { this.#eventEmitter.on( ELEMENT_EVENTS_TO_IFRAME.DESTROY_FRAME, () => { diff --git a/src/core/external/collect/compose-collect-container.ts b/src/core/external/collect/compose-collect-container.ts index 4a9f0480..c3f7d0ef 100644 --- a/src/core/external/collect/compose-collect-container.ts +++ b/src/core/external/collect/compose-collect-container.ts @@ -21,6 +21,9 @@ import { CollectElementOptions, ICollectOptions, CollectResponse, + InputStyles, + ErrorTextStyles, + ContainerOptions, } from '../../../utils/common'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import logs from '../../../utils/logs'; @@ -38,6 +41,13 @@ import { import Container from '../common/container'; import CollectElement from './collect-element'; import ComposableElement from './compose-collect-element'; +import { ElementGroup, ElementGroupItem } from './collect-container'; +import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; + +export interface ComposableElementGroup extends ElementGroup { + styles: InputStyles; + errorTextStyles: ErrorTextStyles; +} const CLASS_NAME = 'CollectContainer'; class ComposableContainer extends Container { @@ -45,21 +55,21 @@ class ComposableContainer extends Container { #elements: Record = {}; - #metaData: any; + #metaData: Metadata; - #elementGroup: any = { rows: [] }; + #elementGroup: ComposableElementGroup = { rows: [], styles: {}, errorTextStyles: {} }; - #elementsList:any = []; + #elementsList: Array = []; #context:Context; - #skyflowElements:any; + #skyflowElements: Array; #eventEmitter: EventEmitter; #isMounted: boolean = false; - #options: any; + #options: ContainerOptions; #containerElement:any; @@ -73,7 +83,12 @@ class ComposableContainer extends Container { #isSkyflowFrameReady: boolean = false; - constructor(options, metaData, skyflowElements, context) { + constructor( + metaData: Metadata, + skyflowElements: Array, + context: Context, + options: ContainerOptions, + ) { super(); this.#containerId = uuid(); this.#metaData = { @@ -114,7 +129,7 @@ class ComposableContainer extends Container { create = (input: CollectElementInput, options: CollectElementOptions = { required: false, - }) => { + }): ComposableElement => { validateCollectElementInput(input, this.#context.logLevel); const validations = formatValidations(input.validations); const formattedOptions = formatOptions(input.type, options, this.#context.logLevel); @@ -139,9 +154,9 @@ class ComposableContainer extends Container { }; #createMultipleElement = ( - multipleElements: any, + multipleElements: ComposableElementGroup, isSingleElementAPI: boolean = false, - ) => { + ): ComposableContainer => { const elements: any[] = []; this.#tempElements = deepClone(multipleElements); this.#tempElements.rows.forEach((row) => { @@ -307,7 +322,7 @@ class ComposableContainer extends Container { this.#containerElement.unmount(); }; - collect = (options: ICollectOptions = { tokens: true }) :Promise => { + collect = (options: ICollectOptions = { tokens: true }): Promise => { this.#isSkyflowFrameReady = this.#metaData.skyflowContainer.isControllerFrameReady; if (this.#isSkyflowFrameReady) { return new Promise((resolve, reject) => { @@ -342,7 +357,7 @@ class ComposableContainer extends Container { this.#elementsList.forEach((element) => { elementIds.push({ frameId: this.#tempElements.elementName, - elementId: element.elementName, + elementId: element.elementName || '', }); }); bus @@ -412,7 +427,7 @@ class ComposableContainer extends Container { this.#elementsList.forEach((element) => { elementIds.push({ frameId: this.#tempElements.elementName, - elementId: element.elementName, + elementId: element.elementName || '', }); }); bus diff --git a/src/core/external/collect/compose-collect-element.ts b/src/core/external/collect/compose-collect-element.ts index 44a63a22..b3e7b7c3 100644 --- a/src/core/external/collect/compose-collect-element.ts +++ b/src/core/external/collect/compose-collect-element.ts @@ -19,7 +19,7 @@ class ComposableElement { #isUpdateCalled = false; - constructor(name, eventEmitter, iframeName) { + constructor(name: string, eventEmitter: EventEmitter, iframeName: string) { this.#elementName = name; this.#iframeName = iframeName; this.#eventEmitter = eventEmitter; diff --git a/src/core/external/common/iframe.ts b/src/core/external/common/iframe.ts index b59e8bc6..defcf630 100644 --- a/src/core/external/common/iframe.ts +++ b/src/core/external/common/iframe.ts @@ -9,17 +9,19 @@ import SkyflowError from '../../../libs/skyflow-error'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import { updateMetricObjectValue } from '../../../metrics/index'; import { METRIC_TYPES } from '../../constants'; +import { LogLevel } from '../../../index-node'; +import { Metadata } from '../../internal/internal-types'; export default class IFrame { name: string; - metadata: any; + metadata: Metadata; iframe: HTMLIFrameElement; container?: Element; - constructor(name, metadata, containerId, logLevel) { + constructor(name: string, metadata: Metadata, containerId: string, logLevel: LogLevel) { const clientDomain = metadata.clientDomain || ''; this.name = `${name}:${containerId}:${logLevel}:${btoa(clientDomain)}`; this.metadata = metadata; @@ -29,7 +31,7 @@ export default class IFrame { }); } - mount = (domElement, elementId?: string, data?: any) => { + mount = (domElement: HTMLElement | string, elementId?: string, data?: any) => { this.unmount(); try { if (typeof domElement === 'string') { diff --git a/src/core/external/common/skyflow-element.ts b/src/core/external/common/skyflow-element.ts index 7ab6161e..791077e1 100644 --- a/src/core/external/common/skyflow-element.ts +++ b/src/core/external/common/skyflow-element.ts @@ -2,19 +2,19 @@ Copyright (c) 2022 Skyflow, Inc. */ abstract class SkyflowElement { - abstract mount(domElementSelector); + abstract mount(domElementSelector: HTMLElement | string): void; - abstract unmount(); + abstract unmount(): void; - abstract setError(clientErrorText:string); + abstract setError(clientErrorText: string): void; - abstract resetError(); + abstract resetError(): void; - abstract setErrorOverride(customErrorText:string); + abstract setErrorOverride(customErrorText: string): void; - abstract iframeName(); + abstract iframeName(): string; - abstract getID(); + abstract getID(): string; } export default SkyflowElement; diff --git a/src/core/external/reveal/reveal-container.ts b/src/core/external/reveal/reveal-container.ts index 0961910f..fac8027a 100644 --- a/src/core/external/reveal/reveal-container.ts +++ b/src/core/external/reveal/reveal-container.ts @@ -8,6 +8,7 @@ import SkyflowError from '../../../libs/skyflow-error'; import uuid from '../../../libs/uuid'; import { ContainerType } from '../../../skyflow'; import { + ContainerOptions, Context, MessageType, RedactionType, RevealResponse, } from '../../../utils/common'; @@ -22,6 +23,7 @@ import { import Container from '../common/container'; import RevealElement from './reveal-element'; import properties from '../../../properties'; +import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; export interface IRevealElementInput { token?: string; @@ -50,7 +52,7 @@ class RevealContainer extends Container { #mountedRecords: { id: string }[] = []; - #metaData: any; + #metaData: Metadata; #containerId: string; @@ -62,15 +64,20 @@ class RevealContainer extends Container { #context: Context; - #skyflowElements: any; + #skyflowElements: Array; - #isMounted:any; + #isMounted: boolean = false; type:string = ContainerType.REVEAL; #isSkyflowFrameReady: boolean = false; - constructor(metaData, skyflowElements, context, options = {}) { + constructor( + metaData: Metadata, + skyflowElements: Array, + context: Context, + options?: ContainerOptions, + ) { super(); this.#isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; this.#metaData = { diff --git a/src/core/external/reveal/reveal-element.ts b/src/core/external/reveal/reveal-element.ts index 259f1a90..a0264f8c 100644 --- a/src/core/external/reveal/reveal-element.ts +++ b/src/core/external/reveal/reveal-element.ts @@ -32,17 +32,19 @@ import { parameterizedString, printLog } from '../../../utils/logs-helper'; import { formatForRenderClient } from '../../../core-utils/reveal'; import properties from '../../../properties'; import { validateInitConfig, validateRenderElementRecord } from '../../../utils/validators'; +import { Metadata, RevealContainerProps } from '../../internal/internal-types'; +import EventEmitter from '../../../event-emitter'; const CLASS_NAME = 'RevealElement'; class RevealElement extends SkyflowElement { #iframe: IFrame; - #metaData: any; + #metaData: Metadata; #recordData: any; - #containerId: any; + #containerId: string; #isMounted:boolean = false; @@ -54,7 +56,7 @@ class RevealElement extends SkyflowElement { #readyToMount: boolean = false; - #eventEmitter:any; + #eventEmitter: EventEmitter; #isFrameReady: boolean; @@ -64,9 +66,14 @@ class RevealElement extends SkyflowElement { #isSkyflowFrameReady: boolean = false; - constructor(record: IRevealElementInput, + constructor( + record: IRevealElementInput, options: IRevealElementOptions = {}, - metaData: any, container: any, elementId: string, context: Context) { + metaData: Metadata, + container: RevealContainerProps, + elementId: string, + context: Context, + ) { super(); this.#elementId = elementId; this.#metaData = metaData; diff --git a/src/core/external/skyflow-container.ts b/src/core/external/skyflow-container.ts index e512de16..a5c9dfe2 100644 --- a/src/core/external/skyflow-container.ts +++ b/src/core/external/skyflow-container.ts @@ -57,7 +57,7 @@ class SkyflowContainer { #context: Context; - constructor(client, context) { + constructor(client: Client, context: Context) { this.#client = client; this.#containerId = this.#client.toJSON()?.metaData?.uuid || ''; this.#context = context; diff --git a/src/core/internal/internal-types/index.ts b/src/core/internal/internal-types/index.ts new file mode 100644 index 00000000..bb236b3f --- /dev/null +++ b/src/core/internal/internal-types/index.ts @@ -0,0 +1,83 @@ +import { ClientToJSON } from '../../../client'; +import EventEmitter from '../../../event-emitter'; +import { CollectContainer, ComposableContainer, RevealContainer } from '../../../index-node'; +import { ContainerType } from '../../../skyflow'; +import { CollectElementOptions, ICollectOptions } from '../../../utils/common'; +import { ElementType } from '../../constants'; +import SkyflowContainer from '../../external/skyflow-container'; + +export interface ElementInfo { + frameId: string; + elementId: string; +} + +export interface TokenizeDataInput extends ICollectOptions{ + type: string; + elementIds: Array; + containerId: string; +} + +export interface UploadFileDataInput extends ICollectOptions { + type: string; + elementIds: Array; + containerId: string; +} + +export interface BatchInsertRequestBody { + method: string; + quorum?: boolean; + tableName: string; + fields?: Record; + upsert?: string; + ID?: string; + tokenization?: boolean; + [key: string]: any; +} + +export interface ContainerProps { + containerId: string; + isMounted: boolean; + type: string; +} + +export interface RevealContainerProps { + containerId: string; + isMounted: boolean; + eventEmitter: EventEmitter; +} + +export interface InternalState { + isEmpty: boolean, + isValid: boolean, + isFocused: boolean, + isRequired: boolean, + name: string; + elementType: ElementType; + isComplete: boolean; + value: string | Blob | undefined; + selectedCardScheme: string; +} + +export interface FormattedCollectElementOptions extends CollectElementOptions { + [key: string]: any; +} + +export interface SkyflowElementProps { + id: string; + type: ElementType; + element: HTMLElement; + container: CollectContainer | RevealContainer | ComposableContainer; +} + +export interface ClientMetadata { + uuid: string, + clientDomain: string, + sdkVersion?: string; + sessionId?: string; +} + +export interface Metadata extends ClientMetadata { + clientJSON: ClientToJSON; + containerType: ContainerType; + skyflowContainer: SkyflowContainer; +} diff --git a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts index c06964de..7b44335a 100644 --- a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts +++ b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts @@ -36,9 +36,19 @@ import { MessageType, Context, ISkyflowIdRecord, - IDeleteRecord, IGetOptions, - IInsertResponse, + IInsertRecordInput, + IInsertOptions, + UploadFilesResponse, + RevealResponse, + InsertResponse, + CollectResponse, + IRevealResponseType, + GetResponse, + GetByIdResponse, + IDeleteResponseType, + IDeleteRecordInput, + IRenderResponseType, } from '../../../utils/common'; import { deleteData } from '../../../core-utils/delete'; import properties from '../../../properties'; @@ -48,6 +58,10 @@ import { } from '../../../utils/helpers'; import SkyflowError from '../../../libs/skyflow-error'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; +import { + BatchInsertRequestBody, ElementInfo, TokenizeDataInput, UploadFileDataInput, +} from '../internal-types'; +import IFrameFormElement from '../iframe-form'; const set = require('set-value'); @@ -121,7 +135,7 @@ class SkyflowFrameController { data.records as IRevealRecord[], this.#client, ).then( - (resolvedResult) => { + (resolvedResult: IRevealResponseType) => { printLog( parameterizedString( logs.infoLogs.FETCH_RECORDS_RESOLVED, @@ -132,7 +146,7 @@ class SkyflowFrameController { ); callback(resolvedResult); }, - (rejectedResult) => { + (rejectedResult: IRevealResponseType) => { printLog( parameterizedString(logs.errorLogs.FETCH_RECORDS_REJECTED), MessageType.ERROR, @@ -143,8 +157,8 @@ class SkyflowFrameController { }, ); } else if (data.type === PUREJS_TYPES.INSERT) { - this.insertData(data.records, data.options) - .then((result) => { + this.insertData(data.records as IInsertRecordInput, data.options as IInsertOptions) + .then((result: InsertResponse) => { printLog( parameterizedString( logs.infoLogs.INSERT_RECORDS_RESOLVED, @@ -153,10 +167,9 @@ class SkyflowFrameController { MessageType.LOG, this.#context.logLevel, ); - callback(result); }) - .catch((error) => { + .catch((error: InsertResponse) => { printLog( parameterizedString(logs.errorLogs.INSERT_RECORDS_REJECTED), MessageType.ERROR, @@ -168,7 +181,7 @@ class SkyflowFrameController { fetchRecordsGET( data.records as IGetRecord[], this.#client, data.options as IGetOptions, ).then( - (resolvedResult) => { + (resolvedResult: GetResponse) => { printLog( parameterizedString(logs.infoLogs.GET_RESOLVED, CLASS_NAME), MessageType.LOG, @@ -177,7 +190,7 @@ class SkyflowFrameController { callback(resolvedResult); }, - (rejectedResult) => { + (rejectedResult: GetResponse) => { printLog(parameterizedString( logs.errorLogs.GET_REJECTED, ), @@ -192,7 +205,7 @@ class SkyflowFrameController { data.records as ISkyflowIdRecord[], this.#client, ).then( - (resolvedResult) => { + (resolvedResult: GetByIdResponse) => { printLog( parameterizedString( logs.infoLogs.GET_BY_SKYFLOWID_RESOLVED, @@ -204,7 +217,7 @@ class SkyflowFrameController { callback(resolvedResult); }, - (rejectedResult) => { + (rejectedResult: GetByIdResponse) => { printLog( parameterizedString(logs.errorLogs.GET_BY_SKYFLOWID_REJECTED), MessageType.ERROR, @@ -216,11 +229,11 @@ class SkyflowFrameController { ); } else if (data.type === PUREJS_TYPES.DELETE) { deleteData( - data.records as IDeleteRecord[], - data.options, + data.records as IDeleteRecordInput, + data.options || {}, this.#client, ).then( - (resolvedResult) => { + (resolvedResult: IDeleteResponseType) => { printLog( parameterizedString( logs.infoLogs.DELETE_RESOLVED, @@ -232,7 +245,7 @@ class SkyflowFrameController { callback(resolvedResult); }, - ).catch((rejectedResult) => { + ).catch((rejectedResult: IDeleteResponseType) => { printLog( parameterizedString( logs.errorLogs.DELETE_RECORDS_REJECTED, @@ -278,22 +291,34 @@ class SkyflowFrameController { MessageType.LOG, this.#context.logLevel, ); - this.tokenize(data) - .then((response) => { + const tokenizeDataInput: TokenizeDataInput = { + ...data, + type: data.type, + elementIds: data.elementIds as Array, + containerId: data.containerId as string, + }; + this.tokenize(tokenizeDataInput) + .then((response: CollectResponse) => { callback(response); }) - .catch((error) => { + .catch((error: CollectResponse) => { callback({ error }); }); } else if (data.type === COLLECT_TYPES.FILE_UPLOAD) { printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.FILE_UPLOAD), MessageType.LOG, this.#context.logLevel); - this.parallelUploadFiles(data) - .then((response) => { + const uploadFilesDataInput = { + ...data, + type: data.type, + elementIds: data.elementIds as string[], + containerId: data.containerId as string, + }; + this.parallelUploadFiles(uploadFilesDataInput) + .then((response: UploadFilesResponse) => { callback(response); }) - .catch((error) => { + .catch((error: UploadFilesResponse) => { callback({ error }); }); } @@ -331,7 +356,7 @@ class SkyflowFrameController { printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.REVEAL_REQUEST), MessageType.LOG, this.#context.logLevel); - this.revealData(data.records as any, data.containerId).then( + this.revealData(data.records as IRevealRecord[], data.containerId as string).then( (resolvedResult) => { callback(resolvedResult); }, @@ -343,7 +368,7 @@ class SkyflowFrameController { printLog(parameterizedString(logs.infoLogs.CAPTURE_EVENT, CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), MessageType.LOG, this.#context.logLevel); - this.renderFile(data.records, data.iframeName).then( + this.renderFile(data.records as IRevealRecord, data.iframeName as string).then( (resolvedResult) => { callback( resolvedResult, @@ -357,7 +382,7 @@ class SkyflowFrameController { }); } - static init(clientId) { + static init(clientId: string): SkyflowFrameController { const trackingStatus = getValueFromName(window.name, 3) === 'true'; if (trackingStatus) { const scriptTag = document.createElement('script'); @@ -367,7 +392,7 @@ class SkyflowFrameController { return new SkyflowFrameController(clientId); } - revealData(revealRecords: IRevealRecord[], containerId) { + revealData(revealRecords: IRevealRecord[], containerId: string): Promise { const id = containerId; return new Promise((resolve, reject) => { fetchRecordsByTokenId(revealRecords, this.#client).then( @@ -397,13 +422,15 @@ class SkyflowFrameController { }); } - insertData(records, options) { - const requestBody = constructInsertRecordRequest(records, options); + insertData(records: IInsertRecordInput, options: IInsertOptions): Promise { + const requestBody: Array = constructInsertRecordRequest( + records, options, + ); return new Promise((rootResolve, rootReject) => { getAccessToken(this.#clientId).then((authToken) => { this.#client .request({ - body: { records: requestBody }, + body: JSON.stringify({ records: requestBody }), requestMethod: 'POST', url: `${this.#client.config.vaultURL}/v1/vaults/${ @@ -417,7 +444,7 @@ class SkyflowFrameController { rootResolve( constructInsertRecordResponse( response, - options?.tokens, + options?.tokens ?? true, records?.records, ), ); @@ -431,7 +458,7 @@ class SkyflowFrameController { }); } - renderFile(data, iframeName) { + renderFile(data: IRevealRecord, iframeName: string): Promise { return new Promise((resolve, reject) => { try { getFileURLFromVaultBySkyflowID(data, this.#client) @@ -472,8 +499,8 @@ class SkyflowFrameController { }); } - tokenize = (options) => { - const id = options.containerId; + tokenize = (options: TokenizeDataInput): Promise => { + const id: string = options.containerId; if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); const insertResponseObject: any = {}; const updateResponseObject: any = {}; @@ -584,11 +611,11 @@ class SkyflowFrameController { } } } - let finalInsertRequest; + let finalInsertRequest: Array; let finalInsertRecords; let finalUpdateRecords; - let insertResponse: IInsertResponse; - let updateResponse: IInsertResponse; + let insertResponse: InsertResponse; + let updateResponse: InsertResponse; let insertErrorResponse: any; let updateErrorResponse; let insertDone = false; @@ -604,15 +631,13 @@ class SkyflowFrameController { }); } const client = this.#client; - const sendRequest = () => new Promise((rootResolve, rootReject) => { + const sendRequest = (): Promise => new Promise((rootResolve, rootReject) => { const clientId = client.toJSON()?.metaData?.uuid || ''; getAccessToken(clientId).then((authToken) => { if (finalInsertRequest.length !== 0) { client .request({ - body: { - records: finalInsertRequest, - }, + body: JSON.stringify({ records: finalInsertRequest }), requestMethod: 'POST', url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, headers: { @@ -623,7 +648,7 @@ class SkyflowFrameController { .then((response: any) => { insertResponse = constructInsertRecordResponse( response, - options.tokens, + options.tokens ?? true, finalInsertRecords.records, ); insertDone = true; @@ -634,12 +659,14 @@ class SkyflowFrameController { if (updateErrorResponse.records === undefined) { updateErrorResponse.records = insertResponse.records; } else { - updateErrorResponse.records = insertResponse.records + updateErrorResponse.records = (insertResponse.records || []) .concat(updateErrorResponse.records); } rootReject(updateErrorResponse); } else if (updateDone && updateResponse !== undefined) { - rootResolve({ records: insertResponse.records.concat(updateResponse.records) }); + rootResolve( + { records: (insertResponse.records || []).concat(updateResponse.records || []) }, + ); } }) .catch((error) => { @@ -680,7 +707,9 @@ class SkyflowFrameController { rootResolve(updateResponse); } if (insertDone && insertResponse !== undefined) { - rootResolve({ records: insertResponse.records.concat(updateResponse.records) }); + rootResolve( + { records: (insertResponse.records || []).concat(updateResponse.records || []) }, + ); } else if (insertDone && insertErrorResponse !== undefined) { const errors = insertErrorResponse.errors; const records = updateResponse.records; @@ -696,7 +725,7 @@ class SkyflowFrameController { if (updateErrorResponse.records === undefined) { updateErrorResponse.records = insertResponse.records; } else { - updateErrorResponse.records = insertResponse.records + updateErrorResponse.records = (insertResponse.records || []) .concat(updateErrorResponse.records); } rootReject(updateErrorResponse); @@ -719,7 +748,8 @@ class SkyflowFrameController { }); }; - parallelUploadFiles = (options) => new Promise((rootResolve, rootReject) => { + parallelUploadFiles = (options: UploadFileDataInput): + Promise => new Promise((rootResolve, rootReject) => { const id = options.containerId; const promises: Promise[] = []; for (let i = 0; i < options.elementIds.length; i += 1) { @@ -762,7 +792,7 @@ class SkyflowFrameController { }); }); - uploadFiles = (fileElement) => { + uploadFiles = (fileElement: IFrameFormElement) => { if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); const fileUploadObject: any = {}; @@ -806,7 +836,8 @@ class SkyflowFrameController { } const client = this.#client; - const sendRequest = () => new Promise((rootResolve, rootReject) => { + const sendRequest = (): + Promise => new Promise((rootResolve, rootReject) => { const clientId = client.toJSON()?.metaData?.uuid || ''; getAccessToken(clientId).then((authToken) => { client diff --git a/src/libs/element-options.ts b/src/libs/element-options.ts index 3ee11b57..360c6258 100644 --- a/src/libs/element-options.ts +++ b/src/libs/element-options.ts @@ -11,13 +11,16 @@ import { DEFAULT_EXPIRATION_YEAR_FORMAT, DEFAULT_INPUT_FORMAT_TRANSLATION, ELEMENTS, + ElementType, INPUT_FORMATTING_NOT_SUPPORTED_ELEMENT_TYPES, INPUT_STYLES, } from '../core/constants'; import CollectElement from '../core/external/collect/collect-element'; import ComposableElement from '../core/external/collect/compose-collect-element'; +import { FormattedCollectElementOptions } from '../core/internal/internal-types'; import { - IValidationRule, MessageType, ValidationRuleType, + CollectElementOptions, + IValidationRule, LogLevel, MessageType, ValidationRuleType, } from '../utils/common'; import SKYFLOW_ERROR_CODE from '../utils/constants'; import logs from '../utils/logs'; @@ -277,8 +280,12 @@ IValidationRule[] | undefined => { }); }; -export const formatOptions = (elementType, options, logLevel) => { - let formattedOptions = { +export const formatOptions = ( + elementType: ElementType, + options: CollectElementOptions, + logLevel: LogLevel, +) => { + let formattedOptions: FormattedCollectElementOptions = { required: false, ...options, }; @@ -349,7 +356,7 @@ export const formatOptions = (elementType, options, logLevel) => { } formattedOptions = { ...formattedOptions, - format: isvalidFormat ? formattedOptions.format.toUpperCase() + format: (isvalidFormat && formattedOptions.format) ? formattedOptions.format.toUpperCase() : DEFAULT_EXPIRATION_DATE_FORMAT, }; delete formattedOptions?.translation; @@ -366,7 +373,7 @@ export const formatOptions = (elementType, options, logLevel) => { } formattedOptions = { ...formattedOptions, - format: isvalidFormat ? formattedOptions.format.toUpperCase() + format: (isvalidFormat && formattedOptions.format) ? formattedOptions.format.toUpperCase() : DEFAULT_EXPIRATION_YEAR_FORMAT, }; delete formattedOptions?.translation; diff --git a/src/libs/jss-styles.ts b/src/libs/jss-styles.ts index 81d0749c..daced7d7 100644 --- a/src/libs/jss-styles.ts +++ b/src/libs/jss-styles.ts @@ -6,7 +6,7 @@ import preset from 'jss-preset-default'; jss.setup(preset()); -export default function getCssClassesFromJss(styles, name) { +export default function getCssClassesFromJss(styles, name: string) { const createGenerateId = () => (rule) => `SkyflowElement-${name}-${rule.key}`; jss.setup({ createGenerateId }); const cssStyle = jss.createStyleSheet(styles); diff --git a/src/libs/styles.ts b/src/libs/styles.ts index 0f2fa8e0..886100d0 100644 --- a/src/libs/styles.ts +++ b/src/libs/styles.ts @@ -58,11 +58,9 @@ export const getFlexGridStyles = (obj: any) => { const styles = { 'align-items': obj['align-items'] || 'stretch', 'justify-content': obj['justify-content'] || 'flex-start', - height: - 'auto' - || `calc(100% + ${ - Number.parseInt(spacingValueAndUnit[0], 10) * 2 + spacingValueAndUnit[1] - })`, + height: obj.height === 'auto' ? 'auto' : `calc(100% + ${ + Number.parseInt(spacingValueAndUnit[0], 10) * 2 + spacingValueAndUnit[1] + })`, width: `calc(100% + ${ Number.parseInt(spacingValueAndUnit[0], 10) * 2 + spacingValueAndUnit[1] }))`, diff --git a/src/skyflow.ts b/src/skyflow.ts index 50333549..447dff8c 100644 --- a/src/skyflow.ts +++ b/src/skyflow.ts @@ -47,25 +47,34 @@ import { formatVaultURL, checkAndSetForCustomUrl } from './utils/helpers'; import ComposableContainer from './core/external/collect/compose-collect-container'; import { validateComposableContainerOptions } from './utils/validators'; import ThreeDS from './core/external/threeds/threeds'; +import { ClientMetadata, SkyflowElementProps } from './core/internal/internal-types'; export enum ContainerType { COLLECT = 'COLLECT', REVEAL = 'REVEAL', COMPOSABLE = 'COMPOSABLE', } +export interface SkyflowConfigOptions { + logLevel?: LogLevel; + env?: Env; + trackingKey?: string; + trackMetrics?: boolean; + customElementsURL?: string; +} export interface ISkyflow { vaultID?: string; vaultURL?: string; getBearerToken: () => Promise; - options?: Record; + options?: SkyflowConfigOptions; } + const CLASS_NAME = 'Skyflow'; class Skyflow { #client: Client; #uuid: string = uuid(); - #metadata = { + #metadata: ClientMetadata = { uuid: this.#uuid, clientDomain: window.location.origin, }; @@ -80,7 +89,7 @@ class Skyflow { #env:Env; - #skyflowElements: any; + #skyflowElements: Array; constructor(config: ISkyflow) { const localSDKversion = localStorage.getItem('sdk_version') || ''; @@ -94,11 +103,11 @@ class Skyflow { ); this.#logLevel = config?.options?.logLevel || LogLevel.ERROR; this.#env = config?.options?.env || Env.PROD; - this.#skyflowElements = {}; + this.#skyflowElements = []; this.#skyflowContainer = new SkyflowContainer(this.#client, { logLevel: this.#logLevel, env: this.#env }); - const cb = (data, callback) => { + const cb = (data, callback: Function) => { printLog(parameterizedString(logs.infoLogs.CAPTURED_BEARER_TOKEN_EVENT, CLASS_NAME), MessageType.LOG, this.#logLevel); @@ -170,14 +179,14 @@ class Skyflow { container(type: ContainerType, options?: ContainerOptions) { switch (type) { case ContainerType.COLLECT: { - const collectContainer = new CollectContainer(options, { + const collectContainer = new CollectContainer({ ...this.#metadata, clientJSON: this.#client.toJSON(), containerType: type, skyflowContainer: this.#skyflowContainer, }, this.#skyflowElements, - { logLevel: this.#logLevel, env: this.#env }); + { logLevel: this.#logLevel, env: this.#env }, options); printLog(parameterizedString(logs.infoLogs.COLLECT_CONTAINER_CREATED, CLASS_NAME), MessageType.LOG, this.#logLevel); @@ -191,26 +200,29 @@ class Skyflow { skyflowContainer: this.#skyflowContainer, }, this.#skyflowElements, - { logLevel: this.#logLevel }, options); + { logLevel: this.#logLevel, env: this.#env }, options); printLog(parameterizedString(logs.infoLogs.REVEAL_CONTAINER_CREATED, CLASS_NAME), MessageType.LOG, this.#logLevel); return revealContainer; } case ContainerType.COMPOSABLE: { - validateComposableContainerOptions(options); - const collectContainer = new ComposableContainer(options, { - ...this.#metadata, - clientJSON: this.#client.toJSON(), - containerType: type, - skyflowContainer: this.#skyflowContainer, - }, - this.#skyflowElements, - { logLevel: this.#logLevel, env: this.#env }); + validateComposableContainerOptions(options!); + const composableContainer = new ComposableContainer( + { + ...this.#metadata, + clientJSON: this.#client.toJSON(), + containerType: type, + skyflowContainer: this.#skyflowContainer, + }, + this.#skyflowElements, + { logLevel: this.#logLevel, env: this.#env }, + options!, + ); printLog(parameterizedString(logs.infoLogs.COLLECT_CONTAINER_CREATED, CLASS_NAME), MessageType.LOG, this.#logLevel); - return collectContainer; + return composableContainer; } default: diff --git a/src/utils/bus-events/index.ts b/src/utils/bus-events/index.ts index 80c3d329..4cfe1952 100644 --- a/src/utils/bus-events/index.ts +++ b/src/utils/bus-events/index.ts @@ -5,7 +5,7 @@ import bus from 'framebus'; import { ELEMENT_EVENTS_TO_IFRAME, FRAME_ELEMENT } from '../../core/constants'; import properties from '../../properties'; -export function getAccessToken(clientId) { +export function getAccessToken(clientId: string) { return new Promise((resolve, reject) => { bus.emit(ELEMENT_EVENTS_TO_IFRAME.GET_BEARER_TOKEN + clientId, {}, (data:any) => { diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index 5932f888..6736d4a7 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -1,9 +1,9 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ import { IUpsertOptions } from '../../core-utils/collect'; import { CardType, ElementType } from '../../core/constants'; -/* -Copyright (c) 2022 Skyflow, Inc. -*/ declare global { interface Window { CoralogixRum: any; @@ -198,6 +198,7 @@ export interface DetokenizeResponse extends IRevealResponseType {} export interface InsertResponseRecords { fields: Record, table: string, + skyflow_id?: string, } export interface ErrorRecord { @@ -310,19 +311,19 @@ export interface ICollectOptions { } export interface UploadFilesResponse { - fileUploadResponse: [{ skyflow_id: string }], - errorResponse: [{ error: ErrorRecord }], + fileUploadResponse?: Record, + errorResponse?: Record, } export interface RevealResponse { - success?: { + success?: Array<{ token: string, valueType: string, - }, - errors?: { + }>, + errors?: Array<{ error: ErrorRecord, token: string, - } + }> } export interface RenderFileResponse { diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 13ad38c5..6f86f350 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -33,7 +33,7 @@ export function removeSpaces(inputString:string) { return inputString.trim().replace(/[\s-]/g, ''); } -export function formatVaultURL(vaultURL) { +export function formatVaultURL(vaultURL?: string) { if (typeof vaultURL !== 'string') return vaultURL; return (vaultURL?.trim().slice(-1) === '/') ? vaultURL.slice(0, -1) : vaultURL.trim(); } @@ -42,7 +42,7 @@ export function checkIfDuplicateExists(arr) { return new Set(arr).size !== arr.length; } -export const appendZeroToOne = (value) => { +export const appendZeroToOne = (value: string) => { if (value.length === 1 && Number(value) === 1) { return { isAppended: true, @@ -52,14 +52,14 @@ export const appendZeroToOne = (value) => { return { isAppended: false, value }; }; -export const appendMonthFourDigitYears = (value) => { +export const appendMonthFourDigitYears = (value: string) => { if (value.length === 6 && Number(value.charAt(5)) === 1) { return { isAppended: true, value: `${value.substring(0, 5)}0${value.charAt(5)}` }; } return { isAppended: false, value }; }; -export const appendMonthTwoDigitYears = (value) => { +export const appendMonthTwoDigitYears = (value: string) => { const lastChar = (value.length > 0 && value.charAt(value.length - 1)) || ''; if (value.length === 4 && Number(lastChar) === 1) { return { isAppended: true, value: `${value.substring(0, 3)}0${lastChar}` }; @@ -223,7 +223,7 @@ export const fileValidation = (value, required: Boolean = false, fileElement) => return true; }; -export const vaildateFileName = (name) => ALLOWED_NAME_FOR_FILE.test(name); +export const vaildateFileName = (name: string) => ALLOWED_NAME_FOR_FILE.test(name); export const styleToString = (style) => Object.keys(style).reduce((acc, key) => ( `${acc + key.split(/(?=[A-Z])/).join('-').toLowerCase()}:${style[key]};` diff --git a/src/utils/validators/index.ts b/src/utils/validators/index.ts index c3d26997..5630f9d7 100644 --- a/src/utils/validators/index.ts +++ b/src/utils/validators/index.ts @@ -22,6 +22,8 @@ import { IDeleteRecordInput, IGetOptions, CollectElementInput, + LogLevel, + ContainerOptions, } from '../common'; import SKYFLOW_ERROR_CODE from '../constants'; import { appendZeroToOne } from '../helpers'; @@ -59,7 +61,7 @@ export const detectCardType = (cardNumber: string = '') => { return detectedType; }; -const getYearAndMonthBasedOnFormat = (cardDate, format: string) => { +const getYearAndMonthBasedOnFormat = (cardDate: string, format: string) => { const [part1, part2] = cardDate.split('/'); switch (format) { case 'MM/YY': return { month: appendZeroToOne(part1).value, year: 2000 + Number(part2) }; @@ -74,8 +76,8 @@ export const validateExpiryDate = (date: string, format: string) => { if (date.trim().length === 0) return true; if (!date.includes('/')) return false; const { month, year } = getYearAndMonthBasedOnFormat(date, format); - if (format.endsWith('YYYY') && year.length !== 4) { return false; } - const expiryDate = new Date(year, month, 0); + if (format.endsWith('YYYY') && year.toString().length !== 4) { return false; } + const expiryDate = new Date(Number(year), Number(month), 0); expiryDate.setHours(23, 59, 59, 999); const today = new Date(); @@ -547,7 +549,7 @@ export const validateInitConfig = (initConfig: ISkyflow) => { } }; -export const validateCollectElementInput = (input: CollectElementInput, logLevel) => { +export const validateCollectElementInput = (input: CollectElementInput, logLevel: LogLevel) => { if (!Object.prototype.hasOwnProperty.call(input, 'type')) { throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_ELEMENT_TYPE, [], true); } @@ -637,7 +639,7 @@ export const validateUpsertOptions = (upsertOptions) => { }); }; -export const validateComposableContainerOptions = (options) => { +export const validateComposableContainerOptions = (options: ContainerOptions) => { if (!options) { throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_COMPOSABLE_CONTAINER_OPTIONS, [], true); } diff --git a/tests/client.test.ts b/tests/client.test.ts new file mode 100644 index 00000000..59b58cfe --- /dev/null +++ b/tests/client.test.ts @@ -0,0 +1,411 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import assert from "assert"; +import Client, { IClientRequest } from "../src/client"; +import { ClientMetadata } from "../src/core/internal/internal-types"; +import { ISkyflow } from "../src/skyflow"; + +const skyflowConfig: ISkyflow = { + vaultID: "e20afc3ae1b54f0199f24130e51e0c11", + vaultURL: "https://testurl.com", + getBearerToken: jest.fn(), + options: { trackMetrics: true, trackingKey: "key" }, +}; + +const metaData: ClientMetadata = { + uuid: "1234", + clientDomain: "http://abc.com", +}; + +describe("Client Class", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + test("Client fromJson method", () => { + const testClientObject = Client.fromJSON({ + config: skyflowConfig, + metaData: metaData, + }); + expect(testClientObject).toBeInstanceOf(Client); + }); + + test("client toJSON", () => { + const testClient = new Client(skyflowConfig, metaData); + const testClient2 = testClient.toJSON(); + expect(testClient2.metaData).toBeDefined(); + }); + + test("Client Request Method without errors", () => { + try { + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 200, + response: JSON.stringify({ message: "Hello World!" }), + getAllResponseHeaders: jest + .fn() + .mockImplementation(() => "content-type: application/json"), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient.request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + "content-type": "application/json", + Auth: "eyde.ed.ewe", + }, + body: JSON.stringify({ + key: "value", + }), + }); + expect(xhrMock.open).toBeCalledWith("GET", "https://example-test.com"); + expect(xhrMock.setRequestHeader).toBeCalledWith("Auth", "eyde.ed.ewe"); + expect(xhrMock.send).toBeCalledWith( + JSON.stringify({ + key: "value", + }) + ); + } catch (err) { + console.log(err); + } + }); + + test("Client Request Method with url-formencoded content-type", () => { + try { + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 200, + response: JSON.stringify({ message: "Hello World!" }), + getAllResponseHeaders: jest.fn().mockImplementation( + () => `content-type: application/json + x-request-id: req_123` + ), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient.request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + body: JSON.stringify({ + key: "value", + }), + }); + expect(xhrMock.setRequestHeader).toBeCalledWith( + "content-type", + "application/x-www-form-urlencoded" + ); + } catch (err) { + console.log(err); + } + }); + + test("Client Request Method with form-data content-type", () => { + try { + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 200, + response: JSON.stringify({ message: "Hello World!" }), + getAllResponseHeaders: jest.fn().mockImplementation( + () => `content-type: application/json + x-request-id: req_123` + ), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient.request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + Auth: "eyde.ed.ewe", + "content-type": "multipart/form-data", + }, + body: JSON.stringify({ + key: "value", + }), + }); + expect(xhrMock.setRequestHeader).toBeCalledWith("Auth", "eyde.ed.ewe"); + } catch (err) { + console.log(err); + } + }); + + test("Client Request Method with url-formencoded content-type and navigator as null", () => { + try { + const mockNavigator = { + userAgent: "", // Set userAgent to null or any desired value + // Add other properties or methods as needed for your test case + }; + + // Replace the original navigator object with the mock navigator + Object.defineProperty(window, "navigator", { + value: mockNavigator, + configurable: true, + enumerable: true, + writable: false, + }); + + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 200, + response: JSON.stringify({ message: "Hello World!" }), + getAllResponseHeaders: jest.fn().mockImplementation( + () => `content-type: application/json + x-request-id: req_123` + ), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient.request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + body: JSON.stringify({ + key: "value", + }), + }); + expect(xhrMock.setRequestHeader).toBeCalledWith( + "content-type", + "application/x-www-form-urlencoded" + ); + } catch (err) { + console.log(err); + } + }); + + test("Client Request Method with error 1", () => { + try { + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 401, + response: JSON.stringify({ message: "Hello World!" }), + getAllResponseHeaders: jest.fn().mockImplementation( + () => `content-type: text/plain + x-request-id: req_123` + ), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient + .request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + Auth: "eyde.ed.ewe", + }, + body: JSON.stringify({ + key: "value", + }), + }) + .catch((err) => { + expect(err).toBeDefined(); + }); + expect(xhrMock.open).toBeCalledWith("GET", "https://example-test.com"); + expect(xhrMock.setRequestHeader).toBeCalledWith("Auth", "eyde.ed.ewe"); + expect(xhrMock.send).toBeCalledWith( + JSON.stringify({ + key: "value", + }) + ); + } catch (err) { + console.log(err); + } + }); + + test("Client Request Method with error 2", () => { + try { + const xhrMock = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + onload: jest.fn(), + readyState: 4, + status: 401, + response: JSON.stringify({ + error: { message: "Something went wrong" }, + }), + getAllResponseHeaders: jest + .fn() + .mockImplementation(() => "content-type: application/json"), + } as Partial; + + jest.spyOn(window, "XMLHttpRequest").mockImplementation(() => { + const xhrInstance = { ...xhrMock } as XMLHttpRequest; + + // Add a simple onreadystatechange handler that fires onload/onerror + // This is crucial for making the mock behave like a real XHR + xhrInstance.onreadystatechange = function (this: XMLHttpRequest) { + if (this.readyState === 4) { + if (this.status >= 200 && this.status < 300) { + if (this.onload) { + (this.onload as () => void)(); // Call the onload handler + } + } else { + if (this.onerror) { + (this.onerror as () => void)(); // Call the onerror handler + } + } + } + }; + + return xhrInstance; + }); + const testClient = new Client(skyflowConfig, metaData); + const resp = testClient + .request({ + requestMethod: "GET", + url: "https://example-test.com", + headers: { + Auth: "eyde.ed.ewe", + }, + body: JSON.stringify({ + key: "value", + }), + }) + .catch((err) => { + expect(err).toBeDefined(); + }); + expect(xhrMock.open).toBeCalledWith("GET", "https://example-test.com"); + expect(xhrMock.setRequestHeader).toBeCalledWith("Auth", "eyde.ed.ewe"); + expect(xhrMock.send).toBeCalledWith( + JSON.stringify({ + key: "value", + }) + ); + } catch (err) { + console.log(err); + } + }); +}); diff --git a/tests/core-utils/collect.test.ts b/tests/core-utils/collect.test.ts new file mode 100644 index 00000000..ddb4bf0c --- /dev/null +++ b/tests/core-utils/collect.test.ts @@ -0,0 +1,168 @@ +import { + getUpsertColumn, + constructElementsInsertReq, + checkForValueMatch, +} from "../../src/core-utils/collect"; +import IFrameFormElement from "../../src/core/internal/iframe-form"; +import { + ICollectOptions, + IUpsertOption, + IValidationRule, + ValidationRuleType, +} from "../../src/utils/common"; +import SKYFLOW_ERROR_CODE from "../../src/utils/constants"; +import { parameterizedString } from "../../src/utils/logs-helper"; + +describe("Testing getUpsertColumn method", () => { + const options: ICollectOptions = { + upsert: [ + { + table: "test", + column: "column", + } as IUpsertOption, + ], + }; + + test("return unique column", () => { + const fnResponse = getUpsertColumn("test", options.upsert); + expect(fnResponse).toStrictEqual("column"); + }); + + test("return empty column", () => { + const fnResponse = getUpsertColumn("testTwo", options.upsert); + expect(fnResponse).toStrictEqual(""); + }); + + test("upsert options as undefined", () => { + const fnResponse = getUpsertColumn("test", undefined); + expect(fnResponse).toStrictEqual(""); + }); +}); + +let req = { + table1: { + fields: { + cvv: "122", + }, + }, +}; + +let update = { + table1: { + fields: { + cvv: "122", + }, + }, +}; + +let update2 = { + table1: { + fields: { + column: "122", + }, + }, +}; + +const options: ICollectOptions = { + tokens: true, + additionalFields: { + records: [ + { + table: "table1", + fields: { + name: "name", + }, + }, + ], + }, +}; + +const options2: ICollectOptions = { + tokens: true, + additionalFields: { + records: [ + { + table: "table1", + fields: { + column: "122", + skyflowID: "table1", + }, + }, + ], + }, +}; + +describe("Testing constructElementsInsertReq method", () => { + test("constructElementsInsertReq error 1", () => { + try { + constructElementsInsertReq(req, update, options); + } catch (err) { + expect(err.error.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT_ADDITIONAL_FIELDS.description + ) + ); + } + }); + + test("constructElementsInsertReq error 2", () => { + try { + constructElementsInsertReq(req, update2, options); + } catch (err) { + expect(err.error.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT.description, + "name", + "table1" + ) + ); + } + }); + + test("constructElementsInsertReq error 2", () => { + try { + constructElementsInsertReq(req, update2, options2); + } catch (err) { + expect(err.error.description).toEqual( + parameterizedString(SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT.description) + ); + } + }); +}); + +class MockIFrameFormElement { + state = { value: "testValue" }; + + isMatchEqual(index: number, value: string, rule: IValidationRule) { + return index % 2 === 0; + } +} + +describe("Testing checkForValueMatch method", () => { + let element: MockIFrameFormElement; + + beforeEach(() => { + element = new MockIFrameFormElement(); + }); + + it("should return true when an ELEMENT_VALUE_MATCH_RULE type is found and isMatchEqual returns false", () => { + const validations = [ + { type: ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, params: {} }, + ]; + jest.spyOn(element, "isMatchEqual").mockReturnValue(false); + expect(checkForValueMatch(validations, element as IFrameFormElement)).toBe( + true + ); + }); + + it("should return false when an ELEMENT_VALUE_MATCH_RULE type is found but isMatchEqual returns true", () => { + const validations = [ + { type: ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, params: {} }, + ]; + jest.spyOn(element, "isMatchEqual").mockReturnValue(true); + + expect(checkForValueMatch(validations, element as IFrameFormElement)).toBe( + false + ); + }); +}); diff --git a/tests/core/external/collect/collect-container.test.js b/tests/core/external/collect/collect-container.test.js index 26b58ae2..cbb0d666 100644 --- a/tests/core/external/collect/collect-container.test.js +++ b/tests/core/external/collect/collect-container.test.js @@ -222,7 +222,7 @@ describe('Collect container', () => { document.body.innerHTML = ''; }); it('should throw error when collect call made with no elements ', () => { - const collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.collect().then().catch(err => { expect(err).toBeDefined(); @@ -230,7 +230,7 @@ describe('Collect container', () => { }) }); it('should throw error when collect call made with no elements case2 ', () => { - const collectContainer = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const collectContainer = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.collect().then().catch(err => { expect(err).toBeDefined(); @@ -238,7 +238,7 @@ describe('Collect container', () => { }) }); it('should throw error when uploadfiles call made with no elements ', () => { - const collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.uploadFiles().then().catch(err => { expect(err).toBeDefined(); @@ -246,7 +246,7 @@ describe('Collect container', () => { }) }); it('should throw error when uploadfiles call made with no elements ', () => { - const collectContainer = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const collectContainer = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); expect(collectContainer).toBeDefined(); collectContainer.uploadFiles().then().catch(err => { expect(err).toBeDefined(); @@ -255,7 +255,7 @@ describe('Collect container', () => { }); it("container collect success", () => { - let collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -299,7 +299,7 @@ describe('Collect container', () => { }); it("container collect case when tokens are invalid", () => { - let collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -326,7 +326,7 @@ describe('Collect container', () => { }); it("container collect case when additional fields are invalid", () => { - let collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -353,7 +353,7 @@ describe('Collect container', () => { }); it("container collect case when upsert are invalid", () => { - let collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -379,7 +379,7 @@ describe('Collect container', () => { }) }); it("container collect case when elements are invalid", () => { - let collectContainer = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let collectContainer = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -444,7 +444,7 @@ describe('Collect container', () => { }); it('should resolve successfully when collect is called and isSkyflowFrameReady is false', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -512,7 +512,7 @@ describe('Collect container', () => { }); }); it('should throw error when collect is called and isSkyflowFrameReady is false', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -581,7 +581,7 @@ describe('Collect container', () => { }); }); it('should throw error when collect is called and isSkyflowFrameReady is false and tokens is invalid', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -630,7 +630,7 @@ describe('Collect container', () => { }); it('should throw error when collect is called and isSkyflowFrameReady is false and upsert is invalid', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -679,7 +679,7 @@ describe('Collect container', () => { }); it('should throw error when collect is called and isSkyflowFrameReady is false and additionalFields is invalid', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -728,7 +728,7 @@ describe('Collect container', () => { }); it('should throw error when collect is called and isSkyflowFrameReady is false and additionalFields is invalid', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -780,7 +780,7 @@ describe('Collect container', () => { }); it('element type radio or checkox created', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div1 = document.createElement('div'); const div2 = document.createElement('div'); @@ -829,7 +829,7 @@ describe('Collect container', () => { }); it('should successfully upload files when elements are mounted', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); @@ -872,7 +872,7 @@ describe('Collect container', () => { }); }); it('should throw error when elements are not created', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const uploadPromise = container.uploadFiles(); @@ -883,7 +883,7 @@ describe('Collect container', () => { }); }); it('should throw error when elements are not created and skyflow frame controller not ready', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const uploadPromise = container.uploadFiles(); @@ -894,7 +894,7 @@ describe('Collect container', () => { }); }); it('should throw error when elements are created but not mounted', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -911,7 +911,7 @@ describe('Collect container', () => { }); it('should successfully upload files when elements are mounted', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); @@ -935,14 +935,14 @@ describe('Collect container', () => { }); it('should throw an error if elements are not mounted', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); await expect(container.uploadFiles()).rejects.toThrow(SkyflowError); }); it('should throw an error if elements are not mounted and skyflow frame not ready', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -954,7 +954,7 @@ describe('Collect container', () => { expect(response).rejects.toThrow(SkyflowError); }); it('should throw an error if elements are not mounted when skyflow frame controller is not ready', () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); Object.defineProperty(container, '#isSkyflowFrameReady', { @@ -974,7 +974,7 @@ describe('Collect container', () => { }); it('should handle errors during file upload', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const div = document.createElement('div'); const fileElement = container.create(FileElement); @@ -997,7 +997,7 @@ describe('Collect container', () => { }); it('should not emit events when isSkyflowFrameReady is false', async () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -1019,7 +1019,7 @@ describe('Collect container', () => { }); it('should resolve successfully when file upload is successful', async () => { - const container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); Object.defineProperty(container, '#isSkyflowFrameReady', { value: false, @@ -1071,7 +1071,7 @@ describe('Collect container', () => { }); it('Invalid element type', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, type: 'abc' }); } catch (err) { @@ -1080,7 +1080,7 @@ describe('Collect container', () => { }); it('Invalid table', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1092,7 +1092,7 @@ describe('Collect container', () => { }); it('Invalid column', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1104,7 +1104,7 @@ describe('Collect container', () => { }); it('Invalid validation params, missing element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1120,7 +1120,7 @@ describe('Collect container', () => { }); it('Invalid validation params, invalid collect element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1137,7 +1137,7 @@ describe('Collect container', () => { } }); it('Invalid validation params, invalid collect element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1154,7 +1154,7 @@ describe('Collect container', () => { } }); it('valid validation params, regex match rule', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1174,7 +1174,7 @@ describe('Collect container', () => { it('create valid Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let cvv; try { cvv = container.create(cvvElement); @@ -1186,7 +1186,7 @@ describe('Collect container', () => { }); it('test default options for card_number', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let card_number; try { card_number = container.create(cardNumberElement); @@ -1199,7 +1199,7 @@ describe('Collect container', () => { it('test invalid option for EXPIRATION_DATE', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { format: 'invalid' }); @@ -1211,7 +1211,7 @@ describe('Collect container', () => { it('test valid option for EXPIRATION_DATE', () => { const validFormat = 'YYYY/MM' - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { format: validFormat }); @@ -1222,7 +1222,7 @@ describe('Collect container', () => { }); it('test enableCardIcon option is enabled for elements', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCardIcon: true }); @@ -1234,7 +1234,7 @@ describe('Collect container', () => { }); it('test enableCopy option is enabled for elements', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCopy: true }); @@ -1246,7 +1246,7 @@ describe('Collect container', () => { }); it('test enableCardIcon option is disabled for elements', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCardIcon: false }); @@ -1257,7 +1257,7 @@ describe('Collect container', () => { }); it('test enableCopy option is disabled for elements', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationDateElement, { enableCopy: false }); @@ -1270,7 +1270,7 @@ describe('Collect container', () => { it('test invalid option for EXPIRATION_YEAR', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationYearElement, { format: 'invalid' }); @@ -1282,7 +1282,7 @@ describe('Collect container', () => { it('test valid option for EXPIRATION_YEAR', () => { const validFormat = 'YYYY' - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryElement; try { expiryElement = container.create(ExpirationYearElement, { format: validFormat }); @@ -1293,13 +1293,13 @@ describe('Collect container', () => { }); it("container collect", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); container.collect().then().catch(err => { expect(err).toBeDefined(); }) }); it("container create options", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryDate = container.create({ table: 'pii_fields', column: 'primary_card.cvv', @@ -1316,7 +1316,7 @@ describe('Collect container', () => { }); }); it("container create options 2", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let expiryDate = container.create({ table: 'pii_fields', column: 'primary_card.cvv', @@ -1334,7 +1334,7 @@ describe('Collect container', () => { }); it('create valid file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); let file; try { file = container.create(FileElement); @@ -1346,7 +1346,7 @@ describe('Collect container', () => { }); it('skyflowID undefined for file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1358,7 +1358,7 @@ describe('Collect container', () => { } }); it('empty table for Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ column: 'col', @@ -1370,7 +1370,7 @@ describe('Collect container', () => { } }); it('invalid table for Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ column: 'col', @@ -1383,7 +1383,7 @@ describe('Collect container', () => { } }); it('invalid table for Element case 2', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ column: 'col', @@ -1396,7 +1396,7 @@ describe('Collect container', () => { } }); it('missing column for Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1408,7 +1408,7 @@ describe('Collect container', () => { } }); it('invalid column for Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1421,7 +1421,7 @@ describe('Collect container', () => { } }); it('invalid column for Element case 2', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1434,7 +1434,7 @@ describe('Collect container', () => { } }); it('invalid column for Element case 2', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ type: 'CARD_NUMBER', @@ -1446,7 +1446,7 @@ describe('Collect container', () => { } }); it('skyflowID is missing for file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1456,7 +1456,7 @@ describe('Collect container', () => { } }); it('skyflowID empty for file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1468,7 +1468,7 @@ describe('Collect container', () => { } }); it('skyflowID null for file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1480,7 +1480,7 @@ describe('Collect container', () => { } }); it('skyflowID of invalid type for file Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1492,7 +1492,7 @@ describe('Collect container', () => { } }); it('skyflowID of invalid type for file Element another case', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const file = container.create({ ...cvvFileElementElement, @@ -1504,7 +1504,7 @@ describe('Collect container', () => { } }); it('skyflowID undefined for collect Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1516,7 +1516,7 @@ describe('Collect container', () => { } }); it('skyflowID empty for collect Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1528,7 +1528,7 @@ describe('Collect container', () => { } }); it('skyflowID null for collect Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1540,7 +1540,7 @@ describe('Collect container', () => { } }); it('skyflowID of invalid type for collect Element', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1552,7 +1552,7 @@ describe('Collect container', () => { } }); it('skyflowID null for collect Element another case', () => { - const container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + const container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); try { const cvv = container.create({ ...cvvElement, @@ -1565,7 +1565,7 @@ describe('Collect container', () => { }); it("container collect options", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const options = { tokens: true, additionalFields: { @@ -1594,7 +1594,7 @@ describe('Collect container', () => { }) }); it("container collect options error case 2", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const element1 = container.create(cvvElement2); const options = { tokens: true, @@ -1622,7 +1622,7 @@ describe('Collect container', () => { const div1 = document.createElement('div'); const div2 = document.createElement('div'); - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); element1.mount(div1); @@ -1642,7 +1642,7 @@ describe('Collect container', () => { }); it("container collect options error", () => { - let container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + let container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }); const options = { tokens: true, additionalFields: { @@ -1671,12 +1671,12 @@ describe('iframe cleanup logic', () => { let container; let div1; let div2; - let emitSpy; + let emitSpy; let targetSpy; let onSpy; beforeEach(() => { - emitSpy = null; + emitSpy = null; targetSpy = null; onSpy = null; emitSpy = jest.spyOn(bus, 'emit'); @@ -1698,7 +1698,7 @@ describe('iframe cleanup logic', () => { it('should remove unmounted iframe elements', () => { // Create and mount elements - container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); @@ -1729,7 +1729,7 @@ describe('iframe cleanup logic', () => { }); it('should handle empty document.body', () => { - container = new CollectContainer({}, metaData, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + container = new CollectContainer(metaData, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); const element1 = container.create(cvvElement); element1.mount(div1); @@ -1759,8 +1759,9 @@ describe('iframe cleanup logic', () => { expect(error).not.toBeDefined(); }); }); - it('should remove unmounted iframe elements', () => { - container = new CollectContainer({}, metaData2, {}, { logLevel: LogLevel.ERROR, env: Env.PROD }); + + it('should remove unmounted iframe elements', () => { + container = new CollectContainer(metaData2, [], { logLevel: LogLevel.ERROR, env: Env.PROD }, {}); // Create and mount elements const element1 = container.create(cvvElement); diff --git a/tests/core/external/collect/collect-container.test.ts b/tests/core/external/collect/collect-container.test.ts index d17ae55c..fd778506 100644 --- a/tests/core/external/collect/collect-container.test.ts +++ b/tests/core/external/collect/collect-container.test.ts @@ -1,10 +1,16 @@ /* Copyright (c) 2025 Skyflow, Inc. */ -import { ElementType } from "../../../../src/core/constants"; +import { + ELEMENT_EVENTS_TO_IFRAME, + ElementType, +} from "../../../../src/core/constants"; import CollectContainer from "../../../../src/core/external/collect/collect-container"; import CollectElement from "../../../../src/core/external/collect/collect-element"; +import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import { Metadata } from "../../../../src/core/internal/internal-types"; import * as iframerUtils from "../../../../src/iframe-libs/iframer"; +import { ContainerType } from "../../../../src/skyflow"; import { LogLevel, Env, @@ -36,47 +42,33 @@ jest.mock("../../../../src/libs/uuid", () => ({ default: jest.fn(() => mockUuid), })); -const metaData = { +const metaData: Metadata = { uuid: "123", - skyflowContainer: { - isControllerFrameReady: true, - }, - config: { - vaultID: "vault123", - vaultURL: "https://sb.vault.dev", - getBearerToken, - }, - metaData: { - clientDomain: "http://abc.com", - }, + sdkVersion: "", + sessionId: "1234", + clientDomain: "http://abc.com", + containerType: ContainerType.COLLECT, clientJSON: { config: { vaultID: "vault123", vaultURL: "https://sb.vault.dev", getBearerToken, }, + metaData: { + uuid: "123", + clientDomain: "http://abc.com", + }, }, + skyflowContainer: { + isControllerFrameReady: true, + } as unknown as SkyflowContainer, }; -const metaData2 = { - uuid: "123", + +const metaData2: Metadata = { + ...metaData, skyflowContainer: { isControllerFrameReady: false, - }, - config: { - vaultID: "vault123", - vaultURL: "https://sb.vault.dev", - getBearerToken, - }, - metaData: { - clientDomain: "http://abc.com", - }, - clientJSON: { - config: { - vaultID: "vault123", - vaultURL: "https://sb.vault.dev", - getBearerToken, - }, - }, + } as unknown as SkyflowContainer, }; const collectStylesOptions = { @@ -89,7 +81,7 @@ const collectStylesOptions = { } as InputStyles, }; -const cvvElement: CollectElementInput = { +const cvvInput: CollectElementInput = { table: "pii_fields", column: "primary_card.cvv", placeholder: "cvv", @@ -98,20 +90,20 @@ const cvvElement: CollectElementInput = { ...collectStylesOptions, }; -const cardNumberElement: CollectElementInput = { +const cardNumberInput: CollectElementInput = { table: "pii_fields", column: "primary_card.card_number", type: ElementType.CARD_NUMBER, ...collectStylesOptions, }; -const ExpirationDateElement: CollectElementInput = { +const ExpirationDateInput: CollectElementInput = { table: "pii_fields", column: "primary_card.expiry", type: ElementType.EXPIRATION_DATE, }; -const FileElement: CollectElementInput = { +const fileInput: CollectElementInput = { table: "pii_fields", column: "primary_card.file", type: ElementType.FILE_INPUT, @@ -154,31 +146,28 @@ describe("Collect container", () => { document.body.innerHTML = ""; }); - it("container collect success", () => { - let collectContainer = new CollectContainer( - {}, - metaData, - {}, - { logLevel: LogLevel.ERROR, env: Env.PROD } - ); - const div1 = document.createElement("div"); - const div2 = document.createElement("div"); + it("should successfully collect data from elements", () => { + const collectContainer = new CollectContainer(metaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const div1 = document.createElement("div1"); + const div2 = document.createElement("div2"); - const element1: CollectElement = collectContainer.create(cvvElement); - const element2: CollectElement = collectContainer.create(cardNumberElement); + const element1: CollectElement = collectContainer.create(cvvInput); + const element2: CollectElement = collectContainer.create(cardNumberInput); element1.mount(div1); element2.mount(div2); const mountCvvCb = onSpy.mock.calls[2][1]; - mountCvvCb({ - name: `element:${cvvElement.type}:${btoa(element1.getID())}`, + name: `element:${cvvInput.type}:${btoa(element1.getID())}`, }); const mountCardNumberCb = onSpy.mock.calls[5][1]; mountCardNumberCb({ - name: `element:${cardNumberElement.type}:${btoa(element2.getID())}`, + name: `element:${cardNumberInput.type}:${btoa(element2.getID())}`, }); collectContainer @@ -199,53 +188,188 @@ describe("Collect container", () => { }); it("should successfully upload files when elements are mounted", async () => { - const container = new CollectContainer( - {}, - metaData, - {}, - { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + const collectContainer = new CollectContainer(metaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); const div = document.createElement("div"); - const fileElement = container.create(FileElement); + const fileElement = collectContainer.create(fileInput); fileElement.mount(div); const mountCb = onSpy.mock.calls[2][1]; mountCb({ - name: `element:${FileElement.type}:${btoa(fileElement.getID())}`, + name: `element:${fileInput.type}:${btoa(fileElement.getID())}`, }); - const uploadPromise: Promise = container.uploadFiles({ - tokens: true, - }); + const uploadPromise: Promise = + collectContainer.uploadFiles(); - const uploadRequestCb = emitSpy.mock.calls[1][2]; + const uploadFileCallRequestEvent = emitSpy.mock.calls.find((call) => { + return ( + call[0] && + call[0].includes(ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS) + ); + }); + expect(uploadFileCallRequestEvent).toBeDefined(); + const uploadRequestCb = uploadFileCallRequestEvent[2]; uploadRequestCb({ - fileUploadResonse: [{ skyflow_id: "1234" }], + fileUploadResonse: [{ skyflow_id: "abc-def" }], }); const expectedResponse = await uploadPromise; - console.log(JSON.stringify(expectedResponse, null, 2)) - + console.log(JSON.stringify(expectedResponse, null, 2)); expect(expectedResponse).toBeDefined(); }); it("tests different collect element options for elements", () => { - const container = new CollectContainer( - {}, - metaData, - {}, - { logLevel: LogLevel.ERROR, env: Env.PROD } - ); + const collectContainer = new CollectContainer(metaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); let expiryElement: CollectElement; let elementOptions: CollectElementOptions = { required: true, enableCardIcon: true, enableCopy: true, }; - expiryElement = container.create(ExpirationDateElement, elementOptions); + expiryElement = collectContainer.create( + ExpirationDateInput, + elementOptions + ); const options = expiryElement.getOptions(); expect(options.enableCardIcon).toBe(true); expect(options.enableCopy).toBe(true); }); }); + +describe("iframe cleanup logic", () => { + let collectContainer: CollectContainer; + let div1: HTMLElement; + let div2: HTMLElement; + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let onSpy: jest.SpyInstance; + + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + onSpy = jest.spyOn(bus, "on"); + targetSpy.mockReturnValue({ + on, + off: jest.fn(), + }); + div1 = document.createElement("div"); + div2 = document.createElement("div"); + document.body.innerHTML = ""; + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + document.body.innerHTML = ""; + }); + + it("should remove unmounted iframe elements", () => { + // Create and mount elements + collectContainer = new CollectContainer(metaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + + const element1 = collectContainer.create(cvvInput); + const element2 = collectContainer.create(cardNumberInput); + + element1.mount(div1); + element2.mount(div2); + + const mountCvvCb = onSpy.mock.calls[2][1]; + mountCvvCb({ + name: `element:${cvvInput.type}:${btoa(element1.getID())}`, + }); + + const mountCardNumberCb = onSpy.mock.calls[5][1]; + mountCardNumberCb({ + name: `element:${cardNumberInput.type}:${btoa(element2.getID())}`, + }); + + // Mock iframe elements in document + const iframe1 = document.createElement("iframe"); + iframe1.id = element1.iframeName(); + document.body.appendChild(iframe1); + + // Trigger cleanup by calling collect + collectContainer.collect().catch((error) => { + expect(error).not.toBeDefined(); + }); + }); + + it("should handle empty document.body", () => { + collectContainer = new CollectContainer(metaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + + const element1 = collectContainer.create(cvvInput); + element1.mount(div1); + + const mountCvvCb = onSpy.mock.calls[2][1]; + mountCvvCb({ + name: `element:${cvvInput.type}:${btoa(element1.getID())}`, + }); + + // Mock document.body as null + const originalBody = document.body; + Object.defineProperty(document, "body", { + value: null, + writable: true, + }); + + collectContainer.collect().catch(() => {}); + + // Restore document.body + Object.defineProperty(document, "body", { + value: originalBody, + writable: true, + }); + + // Elements should remain unchanged + collectContainer.collect().catch((error) => { + expect(error).not.toBeDefined(); + }); + }); + + it("should remove unmounted iframe elements", () => { + collectContainer = new CollectContainer(metaData2, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + + // Create and mount elements + const element1 = collectContainer.create(cvvInput); + const element2 = collectContainer.create(cardNumberInput); + + element1.mount(div1); + element2.mount(div2); + + const mountCvvCb = onSpy.mock.calls[2][1]; + mountCvvCb({ + name: `element:${cvvInput.type}:${btoa(element1.getID())}`, + }); + + const mountCardNumberCb = onSpy.mock.calls[5][1]; + mountCardNumberCb({ + name: `element:${cardNumberInput.type}:${btoa(element2.getID())}`, + }); + + // Mock iframe elements in document + const iframe1 = document.createElement("iframe"); + iframe1.id = element1.iframeName(); + document.body.appendChild(iframe1); + + // Trigger cleanup by calling collect + collectContainer.collect().catch((error) => { + expect(error).not.toBeDefined(); + }); + }); +}); diff --git a/tests/core/external/collect/collect-element.test.ts b/tests/core/external/collect/collect-element.test.ts new file mode 100644 index 00000000..a070cf91 --- /dev/null +++ b/tests/core/external/collect/collect-element.test.ts @@ -0,0 +1,1359 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import bus from "framebus"; +import CollectElement from "../../../../src/core/external/collect/collect-element"; +import SkyflowError from "../../../../src/libs/skyflow-error"; +import { + LogLevel, + Env, + ValidationRuleType, + CollectElementInput, + LabelStyles, + ErrorTextStyles, + ElementState, +} from "../../../../src/utils/common"; +import { + ELEMENT_EVENTS_TO_CLIENT, + ELEMENT_EVENTS_TO_IFRAME, + ElementType, +} from "../../../../src/core/constants"; +import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; +import { ContainerType } from "../../../../src/skyflow"; +import EventEmitter from "../../../../src/event-emitter"; +import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import { Metadata } from "../../../../src/core/internal/internal-types"; + +global.ResizeObserver = jest.fn(() => ({ + observe: jest.fn(), + disconnect: jest.fn(), + unobserve: jest.fn(), +})); + +const elementName = "element:CVV:cGlpX2ZpZWxkcy5wcmltYXJ5X2NhcmQuY3Z2"; +const id = "id"; +const input: CollectElementInput = { + table: "pii_fields", + column: "primary_card.cvv", + inputStyles: { + base: { + color: "#1d1d1d", + }, + }, + placeholder: "cvv", + label: "cvv", + type: ElementType.CVV, +}; + +const composableElementName = + "element:group:YXJ5X2NhcmQuY3Z2cGlpX2ZpZWxkcy5wcmlt"; + +const composableInput: CollectElementInput = { + table: "pii_fields", + column: "primary_card.card_numner", + inputStyles: { + base: { + color: "#1d1d1d", + }, + }, + placeholder: "XXXX XXXX XXXX XXXX", + label: "card number", + type: ElementType.CARD_NUMBER, +}; + +const labelStyles: LabelStyles = { + base: { + fontSize: "16px", + fontWeight: "bold", + }, +}; + +const errorTextStyles: ErrorTextStyles = { + base: { + color: "#f44336", + }, +}; + +const rows = [ + { + elements: [ + { + elementName, + elementType: input.type, + name: input.column, + labelStyles, + errorTextStyles, + ...input, + }, + ], + }, +]; + +const composableRows = [ + { + elements: [ + { + composableElementName, + elementType: input.type, + elementName, + name: input.column, + labelStyles, + errorTextStyles, + ...input, + }, + { + composableElementName, + elementType: composableInput.type, + name: composableInput.column, + labelStyles, + errorTextStyles, + ...composableInput, + }, + ], + }, +]; + +const updateElementInput = { + elementType: ElementType.CVV, + name: input.column, + ...input, +}; + +const destroyCallback = jest.fn(); +const updateCallback = jest.fn(); +const groupEmittFn = jest.fn(); +let groupOnCb: Function; +const groupEmiitter: EventEmitter = { + _emit: groupEmittFn, + on: jest.fn().mockImplementation((_, cb) => { + groupOnCb = cb; + }), + events: {}, + off: jest.fn(), + hasListener: jest.fn(), + resetEvents: jest.fn(), +}; + +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); +const metaData: Metadata = { + uuid: "123", + sdkVersion: "", + sessionId: "1234", + clientDomain: "http://abc.com", + containerType: ContainerType.COLLECT, + clientJSON: { + config: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev", + getBearerToken, + }, + metaData: { + uuid: "123", + clientDomain: "http://abc.com", + }, + }, + skyflowContainer: { + isFrameControllerReady: true, + } as unknown as SkyflowContainer, +}; + +jest.mock("../../../../src/event-emitter"); +let emitterSpy: Function; + +(EventEmitter as unknown as jest.Mock).mockImplementation(() => ({ + on: jest.fn().mockImplementation((_, cb) => { + emitterSpy = cb; + }), + _emit: jest.fn(), +})); + +const on = jest.fn(); +describe("testing collect element under various scenarios", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + off: jest.fn(), + }); + }); + + it("tests constructor for collect element", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { elementName, rows }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const inputEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName + ); + const inputCb = inputEvent[0][1]; + const cb2 = jest.fn(); + + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.FOCUS, + value: {}, + }, + cb2 + ); + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.BLUR, + value: {}, + }, + cb2 + ); + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + value: {}, + }, + cb2 + ); + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(() => { + inputCb( + { + name: elementName, + event: "Invalid event", + }, + cb2 + ); + }).toThrow(SkyflowError); + + element.updateElement({ table: "table", elementName: "element" }); + + expect(element.elementType).toBe(input.type); + expect(element.isMounted()).toBe(false); + expect(element.isValidElement()).toBe(true); + + const heightCb = emitSpy.mock.calls[1][2]; + heightCb({ + height: "123", + }); + }); + + it("tests constructor for collect element with element mounted", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const inputEvent = onSpy.mock.calls[1][0]; + expect(inputEvent).toBe(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName); + const inputCb = onSpy.mock.calls[1][1]; + const cb2 = jest.fn(); + + const mountEvent = onSpy.mock.calls[2][0]; + expect(mountEvent).toBe(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + elementName); + const mountCb = onSpy.mock.calls[2][1]; + const cb3 = jest.fn(); + + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.isMounted()).toBe(false); + + mountCb( + { + name: elementName, + }, + cb3 + ); + + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + cb3(); + const heightCb = emitSpy.mock.calls[0][2]; + heightCb({ + name: elementName, + height: "123", + }); + }); + + it("tests constructor with element mounted for different element", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const inputCb = onSpy.mock.calls[1][1]; + const inputEvent = onSpy.mock.calls[1][0]; + expect(inputEvent).toBe(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName); + const cb2 = jest.fn(); + + const mountedEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + elementName + ); + const mountCb = mountedEvent[0][1]; + const cb3 = jest.fn(); + + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.isMounted()).toBe(false); + + mountCb( + { + name: elementName, + }, + cb3 + ); + + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + cb3(); + const heightCb = emitSpy.mock.calls[0][2]; + heightCb({ + height: "123", + }); + }); + + it("tests constructor with composable elements", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows: composableRows, + }, + metaData, + { + type: ContainerType.COMPOSABLE, + containerId: "containerId", + isMounted: true, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const inputEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName + ); + const inputCb = inputEvent[0][1]; + const cb2 = jest.fn(); + + inputCb( + { + name: composableElementName, + event: ELEMENT_EVENTS_TO_CLIENT.FOCUS, + value: {}, + }, + cb2 + ); + inputCb( + { + name: composableElementName, + event: ELEMENT_EVENTS_TO_CLIENT.BLUR, + value: {}, + }, + cb2 + ); + inputCb( + { + name: composableElementName, + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + value: {}, + }, + cb2 + ); + inputCb( + { + name: composableElementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.elementType).toBe(input.type); + expect(element.isMounted()).toBe(false); + expect(element.isValidElement()).toBe(true); + expect(element.getID()).toBe(id); + + cb2(); + }); + + it("tests constructor with composable elements mounted", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows: composableRows, + }, + metaData, + { + type: ContainerType.COMPOSABLE, + containerId: "containerId", + isMounted: true, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const inputEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName + ); + const inputCb = inputEvent[0][1]; + const cb2 = jest.fn(); + + const mountedEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + elementName + ); + const mountCb = mountedEvent[0][1]; + const cb3 = jest.fn(); + + inputCb( + { + name: composableElementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.isMounted()).toBe(false); + + mountCb( + { + name: elementName, + }, + cb3 + ); + + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + cb3(); + + const heightCb = emitSpy.mock.calls[0][2]; + heightCb({ + name: elementName, + height: "123", + }); + }); + + it("tests mount collect element for invalid dom element", () => { + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + expect(() => { + element.mount("#123"); + }).not.toThrow(SkyflowError); + }); + + it("tests mount collect element after container mount for valid dom element", () => { + const onSpy = jest.spyOn(bus, "on"); + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const div = document.createElement("div"); + + expect(element.isMounted()).toBe(false); + + element.mount(div); + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + element.updateElementGroup(updateElementInput); + element.unmount(); + }); + + it("tests mount composable element for valid dom element", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows: composableRows, + }, + metaData, + { + type: ContainerType.COMPOSABLE, + containerId: "containerId", + isMounted: true, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const div = document.createElement("div"); + + expect(element.isMounted()).toBe(false); + + // groupOnCb({containerId:'containerId'}); + element.mount(div); + // const frameReayEvent = onSpy.mock.calls + // .filter((data) => data[0] === `${ELEMENT_EVENTS_TO_IFRAME.FRAME_READY}containerId`); + // const frameReadyCb = frameReayEvent[0][1]; + // const cb2 = jest.fn(); + // frameReadyCb({ + // name: `${elementName}:containerId` + `:ERROR:${btoa(clientDomain)}`, + // }, cb2); + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + element.updateElementGroup(updateElementInput); + element.unmount(); + }); + + it("tests mount collect element before conatiner mount for valid dom element", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: true, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const div = document.createElement("div"); + + expect(element.isMounted()).toBe(false); + + element.mount(div); + // const frameReayEvent = onSpy.mock.calls + // .filter((data) => data[0] === `${ELEMENT_EVENTS_TO_IFRAME.FRAME_READY}containerId`); + // const frameReadyCb = frameReayEvent[0][1]; + // const cb2 = jest.fn(); + // frameReadyCb({ + // name: `${elementName}:containerId` + ':ERROR', + // }, cb2); + // groupOnCb({containerId:'containerId'}); + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + element.updateElementGroup(updateElementInput); + element.unmount(); + }); + + it("tests mount collect element before conatiner mount for valid dom element with isMounted false", () => { + const onSpy = jest.spyOn(bus, "on"); + + const element = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD }, + groupEmiitter + ); + + const div = document.createElement("div"); + + expect(element.isMounted()).toBe(false); + + element.mount(div); + + // groupOnCb({containerId:'containerId'}); + setTimeout(() => { + expect(element.isMounted()).toBe(true); + }, 0); + element.update(updateElementInput); + element.unmount(); + }); + + it("should update element properties when element is mounted", () => { + const onSpy = jest.spyOn(bus, "on"); + const element = new CollectElement( + id, + { elementName, rows }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const inputEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName + ); + const inputCb = inputEvent[0][1]; + const cb2 = jest.fn(); + + const mountedEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + elementName + ); + const mountCb = mountedEvent[0][1]; + const cb3 = jest.fn(); + + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.isMounted()).toBe(false); + mountCb({ name: elementName }, cb3); + expect(element.isMounted()).toBe(true); + element.update({ label: "Henry" }); + }); + + it("should update element properties when element is not mounted", () => { + const element = new CollectElement( + id, + { elementName, rows }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + expect(element.isMounted()).toBe(false); + expect(element.isUpdateCalled()).toBe(false); + element.update({ label: "Henry" }); + emitterSpy(); + expect(element.isMounted()).toBe(true); + }); + + it("should update element group", () => { + const onSpy = jest.spyOn(bus, "on"); + const element = new CollectElement( + id, + { elementName, rows }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const inputEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + elementName + ); + const inputCb = inputEvent[0][1]; + const cb2 = jest.fn(); + + const mountedEvent = onSpy.mock.calls.filter( + (data) => data[0] === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + elementName + ); + const mountCb = mountedEvent[0][1]; + const cb3 = jest.fn(); + + inputCb( + { + name: elementName, + event: ELEMENT_EVENTS_TO_CLIENT.READY, + value: {}, + }, + cb2 + ); + + expect(element.isMounted()).toBe(false); + mountCb({ name: elementName }, cb3); + expect(element.isMounted()).toBe(true); + element.updateElementGroup({ elementName, rows }); + }); +}); + +const row = { + elementName, + elementType: "CVV", + name: input.column, + labelStyles, + errorTextStyles, + ...input, +}; + +describe("testing collect element validations", () => { + it("Invalid ElementType", () => { + const invalidElementType = [ + { + elements: [ + { + ...row, + elementType: "inValidElementType", + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidElementType, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_ELEMENT_TYPE, [], true) + ); + }); + + it("Invalid validations type", () => { + const invalidValidations = [ + { + elements: [ + { + ...row, + validations: "", + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidValidations, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_VALIDATIONS_TYPE, [], true) + ); + }); + + it("Empty validations rule", () => { + const invalidValidationRule = [ + { + elements: [ + { + ...row, + validations: [{}], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidValidationRule, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.MISSING_VALIDATION_RULE_TYPE, + [0], + true + ) + ); + }); + + it("Invalid validations RuleType", () => { + const invalidValidationRule = [ + { + elements: [ + { + ...row, + validations: [ + { + type: "Invalid Rule", + }, + ], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidValidationRule, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.INVALID_VALIDATION_RULE_TYPE, + [0], + true + ) + ); + }); + + it("Missing params in validations Rule", () => { + const invalidValidationRule = [ + { + elements: [ + { + ...row, + validations: [ + { + type: ValidationRuleType.LENGTH_MATCH_RULE, + }, + ], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidValidationRule, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.MISSING_VALIDATION_RULE_PARAMS, + [0], + true + ) + ); + }); + + // above tests in this block are not necessary for typescript ideally. + + it("should throw error for invalid params in validations Rule", () => { + const invalidValidationRule = [ + { + elements: [ + { + ...row, + validations: [ + { + type: ValidationRuleType.LENGTH_MATCH_RULE, + params: "", + }, + ], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidValidationRule, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.INVALID_VALIDATION_RULE_PARAMS, + [0], + true + ) + ); + }); + + it("should throw error for missing regex param in REGEX_MATCH_RULE", () => { + const invalidParams = [ + { + elements: [ + { + ...row, + validations: [ + { + type: ValidationRuleType.REGEX_MATCH_RULE, + params: { + error: "Regex match failed", + }, + }, + ], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidParams, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.MISSING_REGEX_IN_REGEX_MATCH_RULE, + [0], + true + ) + ); + }); + + it("should throw error for missing min,max params in LENGTH_MATCH_RULE", () => { + const invalidParams = [ + { + elements: [ + { + ...row, + validations: [ + { + type: ValidationRuleType.LENGTH_MATCH_RULE, + params: { + error: "length match failed", + }, + }, + ], + }, + ], + }, + ]; + + const createElement = () => { + const element = new CollectElement( + id, + { + elementName, + rows: invalidParams, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + }; + + expect(createElement).toThrow( + new SkyflowError( + SKYFLOW_ERROR_CODE.MISSING_MIN_AND_MAX_IN_LENGTH_MATCH_RULE, + [0], + true + ) + ); + }); + + it("should throw error for missing element param in ELEMENT_VALUE_MATCH_RULE", () => { + const invalidParams = [ + { + elements: [ + { + ...row, + validations: [ + { + type: ValidationRuleType.ELEMENT_VALUE_MATCH_RULE, + params: { + error: "length match failed", + }, + }, + ], + }, + ], + }, + ]; + + try { + const element = new CollectElement( + id, + { + elementName, + rows: invalidParams, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + } catch (err) { + expect(err).toBeUndefined(); + } + }); +}); + +describe("testing collect element methods", () => { + // const emitSpy = jest.spyOn(bus, "emit"); + // const onSpy = jest.spyOn(bus, "on"); + const testCollectElementProd = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const testCollectElementDev = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.DEV } + ); + + it("tests valid on listener return state in handler for element in DEV env", () => { + let handlerState; + const handler = (state: ElementState) => { + handlerState = state; + }; + const mockState = { + name: "cardnumberiframe", + isEmpty: false, + isValid: false, + isFocused: true, + value: "4111", + elementType: "CARD_NUMBER", + isRequired: true, + selectedCardScheme: "", + isComplete: false, + }; + testCollectElementDev.on("CHANGE", handler); + emitterSpy(mockState); + expect(handlerState).toEqual(mockState); + }); + + it("tests valid on listener return state in handler for element in PROD env", () => { + let handlerState; + const handler = (state: ElementState) => { + handlerState = state; + }; + const mockState = { + name: "cardnumberiframe", + isEmpty: false, + isValid: false, + isFocused: true, + value: undefined, + elementType: "CVV", + isRequired: true, + selectedCardScheme: "", + isComplete: false, + }; + testCollectElementProd.on("CHANGE", handler); + emitterSpy(mockState); + expect(handlerState).toEqual(mockState); + }); + + it("should create a ResizeObserver when mounted", () => { + const testCollectElementProd = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + let div = document.createElement("div"); + div.setAttribute("id", "id1"); + testCollectElementProd.mount(div); + + expect(ResizeObserver).toHaveBeenCalled(); + expect(testCollectElementProd.resizeObserver?.observe).toHaveBeenCalledWith( + div + ); + div.style.display = "none"; + expect(ResizeObserver).toHaveBeenCalled(); + expect(testCollectElementProd.resizeObserver?.observe).toHaveBeenCalledWith( + div + ); + testCollectElementProd.unmount(); + expect(ResizeObserver).toHaveBeenCalled(); + expect( + testCollectElementProd.resizeObserver?.disconnect + ).toHaveBeenCalled(); + }); + + it("ResizeObserver should get disconnect when unmounted", () => { + const testCollectElementProd = new CollectElement( + id, + { + elementName, + rows, + }, + metaData, + { + type: ContainerType.COLLECT, + containerId: "containerId", + isMounted: false, + }, + true, + destroyCallback, + updateCallback, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + let div = document.createElement("div"); + div.setAttribute("id", "id1"); + document.body.appendChild(div); + testCollectElementProd.mount("#id1"); + + expect(ResizeObserver).toHaveBeenCalled(); + expect(testCollectElementProd.resizeObserver?.observe).toHaveBeenCalledWith( + document.querySelector("#id1") + ); + + testCollectElementProd.unmount(); + expect(ResizeObserver).toHaveBeenCalled(); + expect( + testCollectElementProd.resizeObserver?.disconnect + ).toHaveBeenCalled(); + }); +}); diff --git a/tests/core/external/collect/composable-container.test.js b/tests/core/external/collect/composable-container.test.js index 4dd7be88..c77561f2 100644 --- a/tests/core/external/collect/composable-container.test.js +++ b/tests/core/external/collect/composable-container.test.js @@ -187,17 +187,17 @@ describe('test composable container class',()=>{ it('test constructor', () => { - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); expect(container).toBeInstanceOf(ComposableContainer); }); it('test create method',()=>{ - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); const element = container.create(cvvElement); expect(element).toBeInstanceOf(ComposableElement); }); it('should throw error when create method is called with no element',(done)=>{ - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); container.collect().catch((err) => { done(); expect(err).toBeDefined(); @@ -209,7 +209,7 @@ describe('test composable container class',()=>{ }) it('should throw error when create method is called with no element case 2',(done)=>{ - const container = new ComposableContainer({layout:[1]}, metaData2, {}, context); + const container = new ComposableContainer(metaData2, {}, context, {layout:[1]}); container.collect().catch((err) => { done(); expect(err).toBeDefined(); @@ -221,7 +221,7 @@ describe('test composable container class',()=>{ }) it('test create method with callback',()=>{ - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); const element = container.create(cvvElement); // on.mock.calls[0][1]({name : "collect_controller1234"},()=>{}); // on.mock.calls[1][1]({name : "collect_controller"},()=>{}); @@ -232,7 +232,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2]}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -250,10 +250,10 @@ describe('test composable container class',()=>{ document.body.append(div); const container = new ComposableContainer( - { layout: [2], styles: { base: { width: '100px' } } }, metaData, {}, - context + context, + { layout: [2], styles: { base: { width: '100px' } } } ); const element1 = container.create(cvvElement); @@ -306,7 +306,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -330,7 +330,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -352,7 +352,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -389,7 +389,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData2, {}, context); + const container = new ComposableContainer(metaData2, {}, context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); emitterSpy(); @@ -411,7 +411,7 @@ describe('test composable container class',()=>{ div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData2, {}, context); + const container = new ComposableContainer(metaData2, {}, context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); @@ -431,7 +431,7 @@ describe('test composable container class',()=>{ const div = document.createElement('div'); div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2],styles:{base:{width:'100px',}}}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2],styles:{base:{width:'100px',}}}); const element1 = container.create(cvvElement); const element2 = container.create(cardNumberElement); try{ @@ -448,7 +448,8 @@ describe('test composable container class',()=>{ }); it("test container collect", () => { - let container = new ComposableContainer({layout:[2],styles:{base:{width:'100px'}},errorTextStyles:{base:{color:'red'}}}, metaData, {}, context); + const containerOptions = {layout:[2],styles:{base:{width:'100px'}},errorTextStyles:{base:{color:'red'}}}; + let container = new ComposableContainer(metaData, [], context, containerOptions); // const div = document.createElement('div'); // div.id = 'composable' // document.body.append(div); @@ -488,7 +489,7 @@ describe('test composable container class',()=>{ div.id = 'composable' document.body.append(div); - const container = new ComposableContainer({layout:[2]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[2]}); // const frameReadyCb = on.mock.calls[0][1]; // const cb2 = jest.fn(); // frameReadyCb({ @@ -506,7 +507,7 @@ describe('test composable container class',()=>{ it('test on method without parameters will throw error',()=>{ try{ - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]},); const element = container.create(cvvElement); container.on(); expect(element).toBeInstanceOf(ComposableElement); @@ -517,7 +518,7 @@ describe('test composable container class',()=>{ it('test on method without event name will throw error',()=>{ try { - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); const element = container.create(cvvElement); container.on("CHANGE"); expect(element).toBeInstanceOf(ComposableElement); @@ -528,7 +529,7 @@ describe('test composable container class',()=>{ it('test on method passing handler as invalid type will throw error',()=>{ try { - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); const element = container.create(cvvElement); container.on("CHANGE","test"); expect(element).toBeInstanceOf(ComposableElement); @@ -538,7 +539,7 @@ describe('test composable container class',()=>{ }); it('test on method without error',()=>{ - const container = new ComposableContainer({layout:[1]}, metaData, {}, context); + const container = new ComposableContainer(metaData, [], context, {layout:[1]}); const element = container.create(cvvElement); container.on("CHANGE",()=>{}); expect(element).toBeInstanceOf(ComposableElement); diff --git a/tests/core/external/collect/composable-container.test.ts b/tests/core/external/collect/composable-container.test.ts new file mode 100644 index 00000000..94eee38d --- /dev/null +++ b/tests/core/external/collect/composable-container.test.ts @@ -0,0 +1,453 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import { + ELEMENT_EVENTS_TO_IFRAME, + ElementType, +} from "../../../../src/core/constants"; +import { + LogLevel, + Env, + ValidationRuleType, + CollectElementInput, + CollectResponse, + Context, + ICollectOptions, +} from "../../../../src/utils/common"; +import ComposableContainer from "../../../../src/core/external/collect/compose-collect-container"; +import ComposableElement from "../../../../src/core/external/collect/compose-collect-element"; +import CollectElement from "../../../../src/core/external/collect/collect-element"; +import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; +import EventEmitter from "../../../../src/event-emitter"; +import { parameterizedString } from "../../../../src/utils/logs-helper"; +import SkyflowError from "../../../../src/libs/skyflow-error"; +import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import { ContainerType } from "../../../../src/skyflow"; +import { Metadata } from "../../../../src/core/internal/internal-types"; + +const bus = require("framebus"); + +jest.mock("../../../../src/iframe-libs/iframer", () => { + const actualModule = jest.requireActual( + "../../../../src/iframe-libs/iframer" + ); + const mockedModule = { ...actualModule }; + mockedModule.__esModule = true; + mockedModule.getIframeSrc = jest.fn(() => "https://google.com"); + return mockedModule; +}); + +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); + +const mockUuid = "1234"; +jest.mock("../../../../src/libs/uuid", () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const mockUnmount = jest.fn(); +const updateMock = jest.fn(); +jest.mock("../../../../src/core/external/collect/collect-element"); + +(CollectElement as unknown as jest.Mock).mockImplementation( + (_, tempElements) => { + tempElements.rows[0].elements.forEach((element) => { + element.isMounted = true; + }); + return { + isMounted: () => true, + mount: jest.fn(), + isValidElement: () => true, + unmount: mockUnmount, + updateElement: updateMock, + }; + } +); + +jest.mock("../../../../src/event-emitter"); +const emitMock = jest.fn(); + +let emitterSpy: Function; +let composableUpdateSpy: Function; +(EventEmitter as unknown as jest.Mock).mockImplementation(() => ({ + on: jest.fn().mockImplementation((name, cb) => { + if (name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_UPDATE_OPTIONS) { + composableUpdateSpy = cb; + } + emitterSpy = cb; + }), + _emit: emitMock, +})); + +const metaData: Metadata = { + uuid: "123", + sdkVersion: "", + sessionId: "1234", + clientDomain: "http://abc.com", + containerType: ContainerType.COMPOSABLE, + clientJSON: { + config: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev", + getBearerToken, + }, + metaData: { + uuid: "123", + clientDomain: "http://abc.com", + }, + }, + skyflowContainer: { + isControllerFrameReady: true, + } as unknown as SkyflowContainer, +}; + +const metaData2: Metadata = { + ...metaData, + skyflowContainer: { + isControllerFrameReady: false, + } as unknown as SkyflowContainer, +}; + +const collectStylesOptions = { + inputStyles: { + cardIcon: { + position: "absolute", + left: "8px", + top: "calc(50% - 10px)", + }, + }, +}; + +const cvvElementInput: CollectElementInput = { + table: "pii_fields", + column: "primary_card.cvv", + placeholder: "cvv", + label: "cvv", + type: ElementType.CVV, + validations: [ + { + type: ValidationRuleType.LENGTH_MATCH_RULE, + params: { + min: 2, + max: 4, + error: "Error", + }, + }, + ], + ...collectStylesOptions, +}; + +const cardNumberElement: CollectElementInput = { + table: "pii_fields", + column: "primary_card.card_number", + type: ElementType.CARD_NUMBER, + ...collectStylesOptions, +}; + +const context: Context = { logLevel: LogLevel.ERROR, env: Env.PROD }; +const on = jest.fn(); + +const collectResponse: CollectResponse = { + records: [ + { + table: "table", + fields: { + first_name: "token1", + primary_card: { + card_number: "token2", + cvv: "token3", + }, + }, + }, + ], +}; + +describe("test composable container class", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let onSpy: jest.SpyInstance; + let eventEmitterSpy: jest.SpyInstance; + + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + eventEmitterSpy = jest.spyOn(EventEmitter.prototype, "on"); + onSpy = jest.spyOn(bus, "on"); + targetSpy.mockReturnValue({ + on, + off: jest.fn(), + }); + }); + + it("tests constructor", () => { + const container = new ComposableContainer(metaData, [], context, { + layout: [1], + }); + expect(container).toBeInstanceOf(ComposableContainer); + }); + + it("tests create method", () => { + const container = new ComposableContainer(metaData, [], context, { + layout: [1], + }); + const element = container.create(cvvElementInput); + expect(element).toBeInstanceOf(ComposableElement); + }); + + it("should throw error when create method is called with no element", (done) => { + const container = new ComposableContainer(metaData, [], context, { + layout: [1], + }); + container.collect().catch((err) => { + done(); + expect(err).toBeDefined(); + expect(err).toBeInstanceOf(SkyflowError); + expect(err.error.code).toBe( + SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE.code + ); + expect(err.error.description).toBe( + parameterizedString( + SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE.description + ) + ); + }); + }); + + it("should throw error when create method is called with no element case 2", (done) => { + const container = new ComposableContainer(metaData2, [], context, { + layout: [1], + }); + container.collect().catch((err) => { + done(); + expect(err).toBeDefined(); + expect(err).toBeInstanceOf(SkyflowError); + expect(err.error.code).toBe( + SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE.code + ); + expect(err.error.description).toBe( + parameterizedString( + SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE.description + ) + ); + }); + }); + + it("test create method with callback", () => { + const container = new ComposableContainer(metaData, [], context, { + layout: [1], + }); + const element = container.create(cvvElementInput); + expect(element).toBeInstanceOf(ComposableElement); + }); + + it("tests mount", () => { + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + const container = new ComposableContainer(metaData, [], context, { + layout: [2], + }); + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + emitterSpy(); + container.mount("#composable"); + }); + + it("tests collect with success and error scenarios", async () => { + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + + const container = new ComposableContainer(metaData, [], context, { + layout: [2], + styles: { base: { width: "100px" } }, + }); + + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + + container.mount("#composable"); + + const options: ICollectOptions = { + tokens: true, + additionalFields: { + records: [ + { + table: "string", + fields: { + column1: "value", + }, + }, + ], + }, + upsert: [ + { + table: "table", + column: "column", + }, + ], + }; + + const collectPromiseSuccess: Promise = + container.collect(options); + + const collectCb1 = emitSpy.mock.calls[0][2]; + collectCb1(collectResponse); + + const successResult = await collectPromiseSuccess; + expect(successResult).toEqual(collectResponse); + + const collectPromiseError: Promise = + container.collect(options); + const collectCb2 = emitSpy.mock.calls[1][2]; + collectCb2({ error: "Error occurred" }); + + await expect(collectPromiseError).rejects.toEqual("Error occurred"); + }); + + it("tests collect when isMount is false", async () => { + let readyCb: Function; + on.mockImplementation((_, cb) => { + readyCb = cb; + }); + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + const container = new ComposableContainer(metaData, [], context, { + layout: [2], + styles: { base: { width: "100px" } }, + }); + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + emitterSpy(); + + container.mount("#composable"); + Object.defineProperty(container, "#isMounted", { + value: false, + writable: true, + }); + + container.collect(); + + on.mockImplementation((_, cb) => { + emitterSpy = cb; + }); + }); + + it("tests updateListeners function", () => { + let readyCb: Function; + on.mockImplementation((_, cb) => { + readyCb = cb; + }); + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + + const container = new ComposableContainer(metaData2, [], context, { + layout: [2], + styles: { base: { width: "100px" } }, + }); + + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + emitterSpy(); + composableUpdateSpy({ elementName: "element:CARD_NUMBER:MTIzNA==" }); + + container.mount("#composable"); + container.collect(); + + on.mockImplementation((name, cb) => { + emitterSpy = cb; + }); + }); + + it("tests collect without mounting the container", (done) => { + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + const container = new ComposableContainer(metaData, [], context, { + layout: [2], + styles: { base: { width: "100px" } }, + }); + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + try { + container + .collect() + .then((res) => { + done(res); + }) + .catch((err) => { + expect(err.error.description).toBe( + parameterizedString( + SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED.description + ) + ); + done(); + }); + } catch (err) { + done(err); + } + }); + + it("tests container collect", () => { + const containerOptions = { + layout: [2], + styles: { base: { width: "100px" } }, + errorTextStyles: { base: { color: "red" } }, + }; + let container = new ComposableContainer( + metaData, + [], + context, + containerOptions + ); + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + container.mount("#composable"); + + const options: ICollectOptions = { + tokens: true, + additionalFields: { + records: [ + { + table: "string", //table into which record should be inserted + fields: { + column1: "value", + }, + }, + ], + }, + upsert: [ + { + table: "table", + column: "column", + }, + ], + }; + emitterSpy(); + setTimeout(() => { + container.collect(options); + const collectCb = emitSpy.mock.calls[0][2]; + collectCb(collectResponse); + collectCb({ error: "Error occured" }); + }, 200); + }); + + it("test container unmount", () => { + const div = document.createElement("div"); + div.id = "composable"; + document.body.append(div); + + const container = new ComposableContainer(metaData, [], context, { + layout: [2], + }); + const element1 = container.create(cvvElementInput); + const element2 = container.create(cardNumberElement); + setTimeout(() => { + container.mount("#composable"); + container.unmount(); + expect(mockUnmount).toBeCalled(); + }, 0); + }); +}); diff --git a/tests/core/external/collect/composable-element.test.ts b/tests/core/external/collect/composable-element.test.ts new file mode 100644 index 00000000..9ce17816 --- /dev/null +++ b/tests/core/external/collect/composable-element.test.ts @@ -0,0 +1,111 @@ +/* + Copyright (c) 2025 Skyflow, Inc. +*/ +import ComposableElement from "../../../../src/core/external/collect/compose-collect-element"; +import EventEmitter from "../../../../src/event-emitter"; +import { ContainerType } from "../../../../src/skyflow"; +import { ElementState } from "../../../../src/utils/common"; + +describe("test composable element", () => { + const emitter = jest.fn(); + let emitSpy: Function; + const testEventEmitter: EventEmitter = { + _emit: emitter, + on: (name: string, cb: Function) => { + if (name.includes("FOCUS")) { + cb({ + isValid: true, + isComplete: true, + name: "element", + value: undefined, + }); + } else if (name.includes("testce2")) { + emitSpy = cb; + } else { + cb({ + isValid: true, + isComplete: true, + name: "element", + value: "", + }); + } + }, + off: jest.fn(), + events: {}, + hasListener: jest.fn(), + resetEvents: jest.fn(), + }; + + const handler = jest.fn(); + const iframeName = "controller_iframe"; + const testElement = new ComposableElement( + "testce1", + testEventEmitter, + iframeName + ); + const testElement2 = new ComposableElement( + "testce2", + testEventEmitter, + iframeName + ); + const testElement3 = new ComposableElement( + "testce3", + testEventEmitter, + iframeName + ); + + it("tests for iframe name to be correct", () => { + expect(testElement3.type).toBe(ContainerType.COMPOSABLE); + const iframe = testElement3.iframeName(); + expect(iframe).toBe(iframeName); + }); + + it("tests for element name to be correct", () => { + expect(testElement3.type).toBe(ContainerType.COMPOSABLE); + const id = testElement3.getID(); + expect(id).toBe("testce3"); + }); + + it("tests valid CHANGE listener on composable element", () => { + expect(testElement.type).toBe(ContainerType.COMPOSABLE); + testElement.on("CHANGE", handler); + expect(handler).toBeCalledWith({ value: "", isValid: true }); + }); + + it("tests valid FOCUS listener on composable element", () => { + expect(testElement.type).toBe(ContainerType.COMPOSABLE); + testElement.on("FOCUS", handler); + expect(handler).toBeCalledWith({ value: "", isValid: true }); + }); + + it("tests invalid listener on composable element", () => { + try { + testElement.on("invalid_listener", (_: ElementState) => {}); + } catch (err) { + expect(err).toBeDefined(); + } + }); + + it("should update element propeties when element is mounted", () => { + const testUpdateOptions = { table: "table" }; + testElement.update(testUpdateOptions); + expect(emitter).toBeCalledWith("COMPOSABLE_UPDATE_OPTIONS", { + elementName: "testce1", + elementOptions: testUpdateOptions, + }); + }); + + it("should update element propeties when element is not mounted", () => { + const testUpdateOptions = { table: "table" }; + testElement2.update(testUpdateOptions); + expect(emitter).not.toBeCalledWith("COMPOSABLE_UPDATE_OPTIONS", { + elementName: "testce2", + elementOptions: testUpdateOptions, + }); + emitSpy(); + expect(emitter).toBeCalledWith("COMPOSABLE_UPDATE_OPTIONS", { + elementName: "testce2", + elementOptions: testUpdateOptions, + }); + }); +}); diff --git a/tests/core/external/reveal/reveal-container.test.ts b/tests/core/external/reveal/reveal-container.test.ts new file mode 100644 index 00000000..59f39777 --- /dev/null +++ b/tests/core/external/reveal/reveal-container.test.ts @@ -0,0 +1,437 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import RevealContainer from "../../../../src/core/external/reveal/reveal-container"; +import { + ELEMENT_EVENTS_TO_CLIENT, + ELEMENT_EVENTS_TO_CONTAINER, + ELEMENT_EVENTS_TO_IFRAME, + REVEAL_FRAME_CONTROLLER, + REVEAL_TYPES, +} from "../../../../src/core/constants"; +import bus from "framebus"; +import { LogLevel, Env } from "../../../../src/utils/common"; +import RevealElement from "../../../../src/core/external/reveal/reveal-element"; +import SKYFLOW_ERROR_CODE from "../../../../src/utils/constants"; +import { parameterizedString } from "../../../../src/utils/logs-helper"; +import SkyflowError from "../../../../src/libs/skyflow-error"; +import logs from "../../../../src/utils/logs"; +import { Metadata } from "../../../../src/core/internal/internal-types"; +import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import { ContainerType, RevealResponse } from "../../../../src/index-node"; +import { ISkyflow } from "../../../../src/skyflow"; +import assert, { AssertionError, fail } from "assert"; + +jest.mock("../../../../src/iframe-libs/iframer", () => { + const actualModule = jest.requireActual( + "../../../../src/iframe-libs/iframer" + ); + const mockedModule = { ...actualModule }; + mockedModule.__esModule = true; + mockedModule.getIframeSrc = jest.fn(() => "https://google.com"); + return mockedModule; +}); + +const mockUuid = "1234"; +jest.mock("../../../../src/libs/uuid", () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const on = jest.fn(); +const off = jest.fn(); +jest.setTimeout(40000); + +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); +const testMetaData: Metadata = { + uuid: "123", + sdkVersion: "", + sessionId: "1234", + clientDomain: "http://abc.com", + containerType: ContainerType.REVEAL, + clientJSON: { + config: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev", + getBearerToken, + }, + metaData: { + uuid: "123", + clientDomain: "http://abc.com", + }, + }, + skyflowContainer: { + isControllerFrameReady: true, + } as unknown as SkyflowContainer, +}; + +const testMetaData2: Metadata = { + ...testMetaData, + skyflowContainer: { + isControllerFrameReady: true, + } as unknown as SkyflowContainer, +}; + +const testRecord = { + token: "1677f7bd-c087-4645-b7da-80a6fd1a81a4", + label: "", + styles: { + base: { + color: "#32ce21", + }, + }, +}; + +const testRevealContainer1 = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, +}); + +const testRevealContainer2 = new RevealContainer(testMetaData2, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, +}); + +describe("Reveal Container Class", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let onSpy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + onSpy = jest.spyOn(bus, "on"); + targetSpy.mockReturnValue({ + on, + off, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test("reveal should throw error with no elements", (done) => { + const container = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + container.reveal().catch((error) => { + done(); + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(SkyflowError); + expect(error.error.code).toEqual(400); + expect(error.error.description).toEqual( + logs.errorLogs.NO_ELEMENTS_IN_REVEAL + ); + }); + }); + + test("constructor", () => { + expect(testRevealContainer1).toBeInstanceOf(RevealContainer); + expect(testRevealContainer1).toBeInstanceOf(Object); + expect(testRevealContainer1).toHaveProperty("create"); + expect(testRevealContainer1).toHaveProperty("reveal"); + expect(testRevealContainer1).toHaveProperty("type"); + }); + + test("create() will return a Reveal Element", () => { + const testRevealElement = testRevealContainer1.create(testRecord); + expect(testRevealElement).toBeInstanceOf(RevealElement); + }); + + test("create() will throw error if record id invalid", () => { + try { + testRevealContainer1.create({ token: "" }); + } catch (error) { + expect(error.message).toBe("Invalid Token Id "); + } + }); + + test("create() will throw error for invalid input format options", (done) => { + try { + testRevealContainer1.create({ token: "1244" }, { format: undefined }); + done("should throw error"); + } catch (error) { + expect(error.error.description).toEqual( + parameterizedString( + SKYFLOW_ERROR_CODE.INVALID_INPUT_OPTIONS_FORMAT.description + ) + ); + done(); + } + }); + + test("handle reveal errors with 404 response", async () => { + const testRevealContainer = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const element = testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const div = document.createElement("div"); + element.mount(div); + + // First emit the mounted event + emitSpy.mockImplementation((eventName, _, callback) => { + if (eventName.includes(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS)) { + callback({ error: { code: 404, description: "Not Found" } }); + } + }); + + // Trigger the mounted callback + const mountedCallback = on.mock.calls.find((call) => + call[0].includes(ELEMENT_EVENTS_TO_CLIENT.MOUNTED) + )[1]; + mountedCallback({ name: element.iframeName() }); + + // Now try to reveal + await expect(testRevealContainer.reveal()).rejects.toEqual({ + code: 404, + description: "Not Found", + }); + }); + + test("handle successful reveal when called before mounting", async () => { + const testRevealContainer = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const element = testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const div = document.createElement("div"); + + // Setup emit spy to handle reveal request + emitSpy.mockImplementation((eventName, _, callback) => { + if (eventName.includes(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS)) { + callback({ success: [{ token: "1815-6223-1073-1425" }] }); + } + }); + + const revealPromise = testRevealContainer.reveal(); + + // Mount and trigger mounted event + element.mount(div); + const mountedCallback = on.mock.calls.find((call) => + call[0].includes(ELEMENT_EVENTS_TO_CLIENT.MOUNTED) + )[1]; + mountedCallback({ name: element.iframeName() }); + + const response = await revealPromise; + expect(response.success).toBeDefined(); + expect(response.success![0].token).toBe("1815-6223-1073-1425"); + }); + + test("frame controller ready event correctly", async () => { + const testRevealContainer = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const element = testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const div = document.createElement("div"); + element.mount(div); + + // Mock frame controller ready event + emitSpy.mockImplementation((eventName, _, callback) => { + if (eventName.includes(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS)) { + callback({ success: [{ token: "1815-6223-1073-1425" }] }); + } + }); + + const mountedCallback = on.mock.calls.find((call) => + call[0].includes(ELEMENT_EVENTS_TO_CLIENT.MOUNTED) + )[1]; + mountedCallback({ name: element.iframeName() }); + + const response = await testRevealContainer.reveal(); + expect(response.success).toBeDefined(); + expect(response.success![0].token).toBe("1815-6223-1073-1425"); + }); + + test("on container mounted else call back", async () => { + const testRevealContainer = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const element = testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const div = document.createElement("div"); + element.mount(div); + + // Mock error response + emitSpy.mockImplementation((eventName, _, callback) => { + if (eventName.includes(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS)) { + callback({ error: { code: 404, description: "Not Found" } }); + } + }); + + const mountedCallback = on.mock.calls.find((call) => + call[0].includes(ELEMENT_EVENTS_TO_CLIENT.MOUNTED) + )[1]; + mountedCallback({ name: element.iframeName() }); + + await expect(testRevealContainer.reveal()).rejects.toEqual({ + code: 404, + description: "Not Found", + }); + }); + + test("on container mounted else call back 1", async () => { + const testRevealContainer = new RevealContainer(testMetaData, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + const element = testRevealContainer.create({ + token: "1815-6223-1073-1425", + }); + const div = document.createElement("div"); + element.mount(div); + + // Mock successful response + emitSpy.mockImplementation((eventName, _, callback) => { + if (eventName.includes(ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS)) { + callback({ success: [{ token: "1815-6223-1073-1425" }] }); + } + }); + + const mountedCallback = on.mock.calls.find((call) => + call[0].includes(ELEMENT_EVENTS_TO_CLIENT.MOUNTED) + )[1]; + mountedCallback({ name: element.iframeName() }); + + const response = await testRevealContainer.reveal(); + expect(response.success).toBeDefined(); + expect(response.success![0].token).toBe("1815-6223-1073-1425"); + }); + + test("reveal before skyflow frame ready event", async () => { + const testRevealContainer = new RevealContainer(testMetaData2, [], { + logLevel: LogLevel.ERROR, + env: Env.PROD, + }); + + // Create reveal element with test token + const testToken = "1815-6223-1073-1425"; + const div = document.createElement("div"); + const revealElement = testRevealContainer.create({ + token: testToken, + }); + + // Mount the element + revealElement.mount(div); + + // Mock success response data + const successResponse: RevealResponse = { + success: [ + { + token: testToken, + valueType: "string", + }, + ], + }; + + // Setup event listeners and callbacks before calling reveal + const elementMountedEvent = + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + mockUuid; + const revealRequestEvent = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + testMetaData2.uuid; + + // Handle element mounted event + bus.emit(elementMountedEvent, { + token: testToken, + containerId: mockUuid, + }); + + // Get and execute the mounted callback + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(elementMountedEvent); + const onCb = on.mock.calls[0][1]; + onCb({ token: testToken, containerId: mockUuid }); + + // Call reveal and await response + const revealPromise = testRevealContainer.reveal(); + + // Get and execute the reveal request callback + const emitEventName = emitSpy.mock.calls[1][0]; + const emitData = emitSpy.mock.calls[1][1]; + const emitCb = emitSpy.mock.calls[1][2]; + + expect(emitEventName).toBe(revealRequestEvent); + expect(emitData).toEqual({ + type: REVEAL_TYPES.REVEAL, + containerId: mockUuid, + records: [{ token: testToken }], + }); + + // Simulate successful reveal response + emitCb(successResponse); + + // Verify the final response + const response = await revealPromise; + expect(response).toEqual(successResponse); + expect(response.success).toBeDefined(); + expect(response.success![0].token).toBe(testToken); + expect(response.success![0].valueType).toBe("string"); + }); + + test("reveal before skyflow frame ready when element have error", (done) => { + var element = testRevealContainer2.create({ token: "1815-6223-1073-1425" }); + element.setError("error occ"); + + testRevealContainer2.reveal().catch((error: RevealResponse) => { + done(); + expect(error).toBeDefined(); + expect(error.errors).toBeDefined(); + expect(error.errors![0].error.code).toEqual(400); + expect(error.errors![0].error.description).toEqual( + logs.errorLogs.REVEAL_ELEMENT_ERROR_STATE + ); + }); + }); + + test("reveal before skyflow frame ready", (done) => { + var element = testRevealContainer1.create({ token: "1815-6223-1073-1425" }); + element.setError("error occ"); + + testRevealContainer1.reveal().catch((error: RevealResponse) => { + done(); + expect(error).toBeDefined(); + expect(error.errors).toBeDefined(); + expect(error.errors![0].error.code).toEqual(400); + expect(error.errors![0].error.description).toEqual( + logs.errorLogs.REVEAL_ELEMENT_ERROR_STATE + ); + }); + }); + + test("reveal when elment is empty when skyflow ready", (done) => { + testRevealContainer2.reveal().catch((error: RevealResponse) => { + done(); + expect(error).toBeDefined(); + expect(error.errors).toBeDefined(); + expect(error.errors![0].error.code).toEqual(400); + expect(error.errors![0].error.description).toEqual( + logs.errorLogs.NO_ELEMENTS_IN_REVEAL + ); + }); + }); + + test("reveal when element is empty when skyflow frame not ready", (done) => { + testRevealContainer1.reveal().catch((error: RevealResponse) => { + done(); + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(SkyflowError); + expect(error.errors).toBeDefined(); + expect(error.errors![0].error.code).toEqual(400); + expect(error.errors![0].error.description).toEqual( + logs.errorLogs.NO_ELEMENTS_IN_REVEAL + ); + }); + }); +}); diff --git a/tests/core/external/reveal/reveal-element.test.ts b/tests/core/external/reveal/reveal-element.test.ts new file mode 100644 index 00000000..35ecdb71 --- /dev/null +++ b/tests/core/external/reveal/reveal-element.test.ts @@ -0,0 +1,1019 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import { LogLevel, Env } from "../../../../src/utils/common"; +import { + ELEMENT_EVENTS_TO_IFRAME, + FRAME_REVEAL, + ELEMENT_EVENTS_TO_CLIENT, + REVEAL_TYPES, + REVEAL_ELEMENT_OPTIONS_TYPES, +} from "../../../../src/core/constants"; +import RevealElement from "../../../../src/core/external/reveal/reveal-element"; +import SkyflowContainer from "../../../../src/core/external/skyflow-container"; +import Client from "../../../../src/client"; + +import * as busEvents from "../../../../src/utils/bus-events"; + +import bus from "framebus"; +import { JSDOM } from "jsdom"; +import { Metadata } from "../../../../src/core/internal/internal-types"; +import { ContainerType, ISkyflow } from "../../../../src/skyflow"; +import { IRevealElementInput } from "../../../../src/core/external/reveal/reveal-container"; +import EventEmitter from "../../../../src/event-emitter"; +import { RevealElementInput } from "../../../../src/index-node"; + +jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject("access token")); + +const mockUuid = "1234"; +const elementId = "id"; +jest.mock("../../../../src/libs/uuid", () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const getBearerToken = jest.fn(); +const getBearerTokenReject = jest + .fn() + .mockImplementation(() => Promise.reject()); +const groupEmittFn = jest.fn(); + +let groupOnCb: Function; +const groupEmiitter: EventEmitter = { + _emit: groupEmittFn, + on: jest.fn().mockImplementation((args, cb) => { + groupOnCb = cb; + }), + events: {}, + off: jest.fn(), + hasListener: jest.fn(), + resetEvents: jest.fn(), +}; + +jest.mock("../../../../src/libs/jss-styles", () => { + return { + __esModule: true, + default: jest.fn(), + generateCssWithoutClass: jest.fn(), + getCssClassesFromJss: jest.fn().mockReturnValue({ + base: { color: "red" }, + global: { backgroundColor: "black" }, + }), + }; +}); + +jest.mock("../../../../src/core/external/skyflow-container", () => { + return { + __esModule: true, + default: jest.fn(), + }; +}); + +const skyflowConfig: ISkyflow = { + vaultID: "e20afc3ae1b54f0199f24130e51e0c11", + vaultURL: "https://testurl.com", + getBearerToken: jest.fn(), + options: { trackMetrics: true, trackingKey: "key" }, +}; +const clientDomain = "http://abc.com"; +const metaData: Metadata = { + uuid: "123", + clientDomain: clientDomain, + sdkVersion: "", + sessionId: "1234", + containerType: ContainerType.REVEAL, + clientJSON: { + config: { + vaultID: "vault123", + vaultURL: "https://sb.vault.dev", + getBearerToken: getBearerToken, + }, + metaData: { + uuid: "123", + clientDomain: clientDomain, + }, + }, + skyflowContainer: { + isControllerFrameReady: true, + } as unknown as SkyflowContainer, +}; + +const metaData2: Metadata = { + ...metaData, + skyflowContainer: { + isControllerFrameReady: false, + } as unknown as SkyflowContainer, +}; + +// const clientData = { +// uuid: "123", +// client: { +// config: { ...skyflowConfig }, +// metadata: { uuid: "123", skyflowContainer: controller }, +// }, +// clientJSON: { +// context: { logLevel: LogLevel.ERROR, env: Env.PROD }, +// config: { +// ...skyflowConfig, +// getBearerToken: jest.fn().toString(), +// }, +// }, +// skyflowContainer: { +// isControllerFrameReady: true, +// }, +// clientDomain: clientDomain, +// }; + +// const clientData2: Metadata = { +// uuid: "123", +// clientDomain: clientDomain, +// client: { +// config: { ...skyflowConfig }, +// metadata: { +// uuid: "123", +// skyflowContainer: { +// isControllerFrameReady: true, +// } as unknown as SkyflowContainer +// }, +// }, +// clientJSON: { +// context: { logLevel: LogLevel.ERROR, env: Env.PROD }, +// config: { +// ...skyflowConfig, +// getBearerToken: jest.fn(), +// }, +// }, +// skyflowContainer: { +// isControllerFrameReady: false, +// } as unknown as SkyflowContainer, +// }; + +// const client: Client = new Client(clientData.client.config, clientData); + +// let controller = new SkyflowContainer(client, { +// logLevel: LogLevel.DEBUG, +// env: Env.DEV, +// }); + +const testRecord: IRevealElementInput = { + token: "1677f7bd-c087-4645-b7da-80a6fd1a81a4", +}; +const on = jest.fn(); +const off = jest.fn(); +let skyflowContainer: SkyflowContainer; + +describe("Reveal Element Class", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + off, + }); + const client = new Client(skyflowConfig, metaData); + skyflowContainer = new SkyflowContainer(client, { + logLevel: LogLevel.DEBUG, + env: Env.PROD, + }); + }); + + const containerId = mockUuid; + + test("Constructor for reveal element", () => { + const testRevealElement = new RevealElement( + testRecord, + undefined, + metaData, + { + containerId: containerId, + isMounted: false, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + expect(testRevealElement).toBeInstanceOf(RevealElement); + }); + + test("Mount method for reveal element", () => { + const testRevealElement = new RevealElement( + testRecord, + undefined, + metaData, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + + expect(document.getElementById("testDiv")).not.toBeNull(); + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + expect(document.querySelector("iframe")).toBeTruthy(); + + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + const eventListenerName = ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testIframeName; + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(eventListenerName); + + const onCb = on.mock.calls[0][1]; + onCb({ name: testIframeName }); + + expect(testRevealElement.isMounted()).toBe(true); + expect(testRevealElement.iframeName()).toBe(testIframeName); + expect(testRevealElement.hasToken()).toBe(true); + }); + + test("Mount method for file render element", () => { + const testRevealElement = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "alt text", + }, + undefined, + metaData, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + + expect(document.getElementById("testDiv")).not.toBeNull(); + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + + expect(document.querySelector("iframe")).toBeTruthy(); + + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + const eventListenerName = ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testIframeName; + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(eventListenerName); + + const onCb = on.mock.calls[0][1]; + onCb({ name: testIframeName }); + + expect(testRevealElement.isMounted()).toBe(true); + expect(testRevealElement.iframeName()).toBe(testIframeName); + }); + + test("file render success scenario", () => { + const testRevealElement = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "alt text", + }, + {}, + metaData2, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + + expect(document.querySelector("iframe")).toBeTruthy(); + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + const eventListenerName = ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testIframeName; + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(eventListenerName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testIframeName, + }); + + expect(testRevealElement.isMounted()).toBe(true); + expect(testRevealElement.iframeName()).toBe(testIframeName); + + testRevealElement + .renderFile() + .then((data) => + expect(data).toEqual({ + success: { skyflow_id: "1244", column: "column" }, + }) + ) + .catch((error) => console.log("error", error)); + const frameReadyEvent = on.mock.calls[1][0]; + expect(frameReadyEvent).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + "123" + ); + const onCallback = on.mock.calls[1][1]; + const cb = jest.fn(); + onCallback({}, cb); + expect(emitSpy.mock.calls[3][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + "123" + ); + expect(emitSpy.mock.calls[3][1]).toEqual({ + type: REVEAL_TYPES.RENDER_FILE, + records: { + altText: "alt text", + skyflowID: "1244", + column: "column", + table: "table", + }, + containerId: mockUuid, + iframeName: testIframeName, + }); + const emitCb = emitSpy.mock.calls[3][2]; + emitCb({ success: { skyflow_id: "1244", column: "column" } }); + }); + + test("renderFile when SKYFLOW_FRAME_CONTROLLER_READY is not triggered success case", (done) => { + const testRevealElement = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "alt text", + }, + undefined, + metaData2, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + // Call renderFile before triggering SKYFLOW_FRAME_CONTROLLER_READY + const renderPromise = testRevealElement.renderFile(); + + // Verify that the else block is executed + const frameReadyEventName = + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + "123"; + const onCbName = on.mock.calls[1][0]; + expect(onCbName).toBe(frameReadyEventName); + + // Simulate the SKYFLOW_FRAME_CONTROLLER_READY event + const onCb = on.mock.calls[1][1]; + const cb = jest.fn(); + onCb({}, cb); + + const emitCb = emitSpy.mock.calls[0][2]; + emitCb({ success: { skyflow_id: "1244", column: "column" } }); + + // Verify the renderFile promise resolves correctly + renderPromise + .then((data) => { + expect(data).toEqual({}); + done(); + }) + .catch((error) => { + console.error("Error:", error); + done(error); + }); + }); + + test("renderFile when SKYFLOW_FRAME_CONTROLLER_READY is not triggered error case", (done) => { + const testRevealElement = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "alt text", + }, + undefined, + metaData, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + // Call renderFile before triggering SKYFLOW_FRAME_CONTROLLER_READY + const renderPromise = testRevealElement.renderFile(); + + const emitCb = emitSpy.mock.calls[0][2]; + emitCb({ + errors: { + column: "column", + skyflowId: "1244", + error: { code: 400, description: "No Records Found" }, + }, + }); + + // Verify the renderFile promise resolves correctly + renderPromise.catch((error) => { + expect(error).toEqual({ + errors: { + column: "column", + skyflowId: "1244", + error: { + code: 400, + description: "No Records Found", + }, + }, + }); + done(); + }); + }); + + test("file render error case", () => { + const testRevealElement = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "alt text", + }, + undefined, + metaData, + { + containerId: containerId, + isMounted: true, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + + expect(document.querySelector("iframe")).toBeTruthy(); + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + const eventListenerName = ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testIframeName; + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(eventListenerName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testIframeName, + }); + expect(testRevealElement.isMounted()).toBe(true); + expect(testRevealElement.iframeName()).toBe(testIframeName); + testRevealElement + .renderFile() + .then((data) => console.log("data", data)) + .catch((error) => { + expect(error).toEqual({ + errors: { + grpc_code: 5, + http_code: 404, + message: "No Records Found", + http_status: "Not Found", + details: [], + }, + }); + }); + + expect(emitSpy.mock.calls[3][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + "123" + ); + expect(emitSpy.mock.calls[3][1]).toEqual({ + type: REVEAL_TYPES.RENDER_FILE, + records: { + altText: "alt text", + skyflowID: "1244", + column: "column", + table: "table", + }, + containerId: mockUuid, + iframeName: testIframeName, + }); + const emitCb = emitSpy.mock.calls[3][2]; + emitCb({ + errors: { + grpc_code: 5, + http_code: 404, + message: "No Records Found", + http_status: "Not Found", + details: [], + }, + }); + }); + + test("Mount method with ready to mount false", () => { + const testRevealElement = new RevealElement( + testRecord, + undefined, + metaData, + { + containerId: containerId, + isMounted: false, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + + expect(document.querySelector("iframe")).toBeTruthy(); + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + expect(testRevealElement.iframeName()).toBe(testIframeName); + expect(testRevealElement.hasToken()).toBe(true); + }); + + test("Mount method with ready to mount false case 2", () => { + const testRevealElement = new RevealElement( + testRecord, + undefined, + metaData, + { + containerId: containerId, + isMounted: false, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + const testEmptyDiv = document.createElement("div"); + testEmptyDiv.setAttribute("id", "testDiv"); + document.body.appendChild(testEmptyDiv); + expect(document.getElementById("testDiv")).not.toBeNull(); + + expect(testRevealElement.isMounted()).toBe(false); + + testRevealElement.mount("#testDiv"); + + expect(document.querySelector("iframe")).toBeTruthy(); + const testIframeName = `${FRAME_REVEAL}:${btoa( + mockUuid + )}:${containerId}:ERROR:${btoa(clientDomain)}`; + expect(document.querySelector("iframe")?.name).toBe(testIframeName); + + expect(testRevealElement.iframeName()).toBe(testIframeName); + expect(testRevealElement.hasToken()).toBe(true); + }); + + test("has token should return false, without token", () => { + const testRevealElement = new RevealElement( + {}, + undefined, + metaData, + { + containerId: containerId, + isMounted: false, + eventEmitter: groupEmiitter, + }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + expect(testRevealElement.hasToken()).toBe(false); + }); +}); + +describe("Reveal Element Methods", () => { + const containerId = mockUuid; + const testRevealElement = new RevealElement( + { + token: "1244", + }, + undefined, + metaData, + { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + const testRevealElement2 = new RevealElement( + { + skyflowID: "1244", + column: "column", + table: "table", + altText: "demo", + inputStyles: { + base: { + border: "5px solid orange", + padding: "10px 10px", + borderRadius: "10px", + color: "#1d1d1d", + marginTop: "4px", + height: "260px", + width: "400px", + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + }, + errorTextStyles: { + base: { + border: "5px solid orange", + padding: "10px 10px", + borderRadius: "10px", + color: "#1d1d1d", + marginTop: "4px", + height: "260px", + width: "400px", + }, + global: { + "@import": + 'url("https://fonts.googleapis.com/css2?family=Roboto&display=swap")', + }, + }, + }, + undefined, + metaData, + { containerId: containerId, isMounted: false, eventEmitter: groupEmiitter }, + elementId, + { logLevel: LogLevel.ERROR, env: Env.PROD } + ); + + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + jest.clearAllMocks(); + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + off, + }); + }); + + test("unmount method", () => { + testRevealElement.unmount(); + testRevealElement2.unmount(); + }); + + test("check for isSetError False", () => { + expect(testRevealElement.isClientSetError()).toBe(false); + }); + + test("setError method", () => { + testRevealElement.mount("#testDiv"); + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + testRevealElement.setError("errorText"); + expect(testRevealElement.isClientSetError()).toBe(true); + expect(emitSpy.mock.calls[1][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[1][1]).toEqual({ + name: testRevealElement.iframeName(), + clientErrorText: "errorText", + isTriggerError: true, + }); + expect(emitSpy).toBeCalled(); + }); + + test("when element is not mounted then setError method", () => { + testRevealElement.unmount(); + testRevealElement.setError("errorText"); + + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + expect(testRevealElement.isClientSetError()).toBe(true); + expect(emitSpy).toBeCalled(); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + clientErrorText: "errorText", + isTriggerError: true, + }); + expect(emitSpy).toBeCalled(); + testRevealElement.mount("123"); + }); + + test("setErrorOverride method", () => { + testRevealElement.setErrorOverride("errorText"); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + clientErrorText: "errorText", + isTriggerError: true, + }); + expect(emitSpy).toBeCalled(); + }); + + test("setErrorOverride method when element is not mounted", () => { + testRevealElement.unmount(); + testRevealElement.setErrorOverride("errorText"); + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + clientErrorText: "errorText", + isTriggerError: true, + }); + expect(emitSpy).toBeCalled(); + testRevealElement.mount("123"); + }); + + test("check for isSetError True", () => { + expect(testRevealElement.isClientSetError()).toBe(true); + }); + + test("resetError method", () => { + testRevealElement.resetError(); + }); + + test("resetError method when element is not mounted", () => { + testRevealElement.unmount(); + testRevealElement.resetError(); + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + expect(testRevealElement.isClientSetError()).toBe(false); + expect(emitSpy).toBeCalled(); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + isTriggerError: false, + }); + expect(emitSpy).toBeCalled(); + testRevealElement.mount("123"); + }); + + test("setAltText method", () => { + testRevealElement.setAltText("altText"); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: "altText", + }); + expect(emitSpy).toBeCalled(); + }); + + test("setAltText method when element is not mounted", () => { + testRevealElement.unmount(); + testRevealElement.setAltText("altText"); + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: "altText", + }); + expect(emitSpy).toBeCalled(); + testRevealElement.mount("123"); + }); + + test("clearAltText method", () => { + testRevealElement.clearAltText(); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: null, + }); + expect(emitSpy).toBeCalled(); + }); + + test("clearAltText method when element is not mounted", () => { + testRevealElement.unmount(); + testRevealElement.clearAltText(); + + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: null, + }); + expect(emitSpy).toBeCalled(); + testRevealElement.mount("123"); + }); + + test("getRecord Data", () => { + const testRecordData = testRevealElement.getRecordData(); + expect(testRecordData).toStrictEqual({ token: "1244" }); + }); + + test("setToken method", () => { + testRevealElement.setToken("testToken"); + }); + + test("setToken method when mount event not happen", () => { + testRevealElement.unmount(); + testRevealElement.setToken("testToken"); + + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement.iframeName(), + }); + }); + + test("getRecord Data", () => { + const testRecordData = testRevealElement.getRecordData(); + expect(testRecordData).toStrictEqual({ token: "testToken" }); + }); + + test("update the properties of elements when element is mounted", () => { + const { window } = new JSDOM('
'); + document = window._document; + const element = document.createElement("div"); + element.setAttribute("id", "#mockElement"); + testRevealElement2.mount("#mockElement"); + + const testUpdateOptions: RevealElementInput = { + label: "Updated Label", + inputStyles: { + base: { + borderWitdth: "5px", + }, + }, + }; + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement2.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement2.iframeName(), + }); + testRevealElement2.update(testUpdateOptions); + expect(emitSpy.mock.calls[2][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement2.iframeName() + ); + expect(emitSpy.mock.calls[2][1]).toEqual({ + name: testRevealElement2.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: { + label: "Updated Label", + inputStyles: { base: { borderWitdth: "5px" } }, + }, + }); + expect(emitSpy).toBeCalled(); + }); + + test("update the properties of elements when element is unmounted", () => { + testRevealElement2.unmount(); + + const testUpdateOptions: RevealElementInput = { + label: "Updated Label", + inputStyles: { + base: { + borderWitdth: "5px", + }, + }, + }; + testRevealElement2.update(testUpdateOptions); + const mountedEventName = + ELEMENT_EVENTS_TO_CLIENT.MOUNTED + testRevealElement2.iframeName(); + const onCbName = on.mock.calls[0][0]; + expect(onCbName).toBe(mountedEventName); + const onCb = on.mock.calls[0][1]; + onCb({ + name: testRevealElement2.iframeName(), + }); + expect(emitSpy.mock.calls[0][0]).toBe( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + testRevealElement2.iframeName() + ); + expect(emitSpy.mock.calls[0][1]).toEqual({ + name: testRevealElement2.iframeName(), + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: { + label: "Updated Label", + inputStyles: { base: { borderWitdth: "5px" } }, + }, + }); + expect(emitSpy).toBeCalled(); + }); +}); diff --git a/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js b/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js index 6ae0d80c..72df6ea2 100644 --- a/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js +++ b/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.js @@ -102,17 +102,14 @@ describe('Uploading files to the vault', () => { }} } })); - const clientReq = jest.fn(() => Promise.resolve({ - fileUploadResponse: [ - {skyflow_id:"file-upload-skyflow-id"} - ] - })); + const clientReq = jest.fn(() => Promise.resolve( + JSON.stringify({ skyflow_id:"file-upload-skyflow-id" }))); jest.spyOn(clientModule, 'fromJSON').mockImplementation(() => ({ ...clientData.client, request: clientReq, toJSON: toJson })); - + SkyflowFrameController.init(); const emitEventName = emitSpy.mock.calls[1][0]; @@ -123,27 +120,27 @@ describe('Uploading files to the vault', () => { const onCb = on.mock.calls[1][1]; const data = { type: COLLECT_TYPES.FILE_UPLOAD, - elementIds: [ - "element:FILE_INPUT:ID" - ], - containerId: "CONTAINER-ID" + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID" }; const cb2 = jest.fn(); onCb(data, cb2); setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + expect(cb2.mock.calls[0][0]).toBeDefined(); expect(cb2.mock.calls[0][0].fileUploadResponse).toBeDefined(); expect(cb2.mock.calls[0][0].fileUploadResponse.length).toBe(1); done(); }, 1000); }); test('should successfully handle FILE_UPLOAD validation', (done) => { - testValue.iFrameFormElement.state.value.name = 'test file.txt'; + testValue.iFrameFormElement.state.value.name = 'test file.txt'; windowSpy.mockImplementation(()=>({ frames:{ 'element:FILE_INPUT:ID:CONTAINER-ID:ERROR:':{document:{ - getElementById:()=>(testValue) + getElementById:()=>(testValue) }} } })); @@ -158,10 +155,8 @@ describe('Uploading files to the vault', () => { const onCb = on.mock.calls[1][1]; const data = { type: COLLECT_TYPES.FILE_UPLOAD, - elementIds: [ - "element:FILE_INPUT:ID" - ], - containerId: "CONTAINER-ID" + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID" }; const cb2 = jest.fn(); @@ -173,7 +168,7 @@ describe('Uploading files to the vault', () => { }, 1000); }); test('should successfully handle FILE_UPLOAD validation case 2', (done) => { - testValue.iFrameFormElement.state.value.name = 'test-file.txt'; + testValue.iFrameFormElement.state.value.name = 'test-file.txt'; testValue.iFrameFormElement.state.value.size = 1024 * 1024 * 32; windowSpy.mockImplementation(()=>({ frames:{ @@ -253,16 +248,16 @@ describe('Uploading files to the vault', () => { windowSpy.mockImplementation(()=>({ frames:{ 'element:FILE_INPUT:ID:CONTAINER-ID:ERROR:':{document:{ - getElementById:()=>(testValue) + getElementById:()=>(testValue) }} } })); - const clientReq = jest.fn(() => Promise.resolve({ + const clientReq = jest.fn(() => Promise.resolve(JSON.stringify({ fileUploadResponse: [ {skyflow_id:"file-upload-skyflow-id"} ], error: "error" - })); + }))); jest.spyOn(clientModule, 'fromJSON').mockImplementation(() => ({ ...clientData.client, request: clientReq, @@ -279,10 +274,8 @@ describe('Uploading files to the vault', () => { const onCb = on.mock.calls[1][1]; const data = { type: COLLECT_TYPES.FILE_UPLOAD, - elementIds: [ - "element:FILE_INPUT:ID" - ], - containerId: "CONTAINER-ID" + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID" }; const cb2 = jest.fn(); @@ -293,6 +286,86 @@ describe('Uploading files to the vault', () => { done(); }, 1000); }); + + test('should handle partial success/error in multiple FILE_UPLOAD attempts', (done) => { + // Mock two file inputs + windowSpy.mockImplementation(() => ({ + frames: { + 'element1:FILE_INPUT:ID:CONTAINER-ID:ERROR:': { + document: { + getElementById: () => (testValue) + } + }, + 'element2:FILE_INPUT:ID:CONTAINER-ID:ERROR:': { + document: { + getElementById: () => ({ + iFrameFormElement: { + ...testValue.iFrameFormElement, + state: { + ...testValue.iFrameFormElement.state, + name: 'file2' + } + } + }) + } + } + } + })); + + // Mock client request to succeed for first file and fail for second + const clientReq = jest.fn((request) => { + if (request.body.get('file2')) { + return Promise.reject({ error: { code: 400, description: "Upload failed" } }); + } else { + return Promise.resolve(JSON.stringify({ skyflow_id: "success-file-id" })); + } + }); + + jest.spyOn(clientModule, 'fromJSON').mockImplementation(() => ({ + ...clientData.client, + request: clientReq, + toJSON: toJson + })); + + SkyflowFrameController.init(); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element1:FILE_INPUT:ID", "element2:FILE_INPUT:ID"], + containerId: "CONTAINER-ID" + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + const result = cb2.mock.calls[0][0]; + + console.log('mock result is\t', result); + + // Should have both success and error responses + expect(result.error.fileUploadResponse).toBeDefined(); + expect(result.error.errorResponse).toBeDefined(); + + // Check successful upload + expect(result.error.fileUploadResponse).toHaveLength(1); + const parsedResponse = JSON.parse(result.error.fileUploadResponse[0]); + expect(parsedResponse.skyflow_id).toBe('success-file-id'); + + // Check failed upload + expect(result.error.errorResponse).toHaveLength(1); + expect(result.error.errorResponse[0].error).toBeDefined(); + + done(); + }, 1000); + }); test('should fail upload files', (done) => { const clientReq = jest.fn(() => Promise.resolve({ fileUploadResponse: [ diff --git a/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts b/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts new file mode 100644 index 00000000..ada8f52f --- /dev/null +++ b/tests/core/internal/skyflow-frame/skyflow-frame-controller-upload-tokenize.test.ts @@ -0,0 +1,1873 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import bus from "framebus"; +import { + COLLECT_TYPES, + ELEMENT_EVENTS_TO_IFRAME, +} from "../../../../src/core/constants"; +import clientModule from "../../../../src/client"; +import * as busEvents from "../../../../src/utils/bus-events"; +import { LogLevel, Env, InsertResponse } from "../../../../src/utils/common"; +import SkyflowFrameController from "../../../../src/core/internal/skyflow-frame/skyflow-frame-controller"; +import Client from "../../../../src/client"; +import { ISkyflow } from "../../../../src/skyflow"; +import { + TokenizeDataInput, + UploadFileDataInput, +} from "../../../../src/core/internal/internal-types"; + +jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve("access token")); + +const on = jest.fn(); +const emit = jest.fn(); + +jest.mock("../../../../src/libs/uuid", () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const mockUuid = "1244"; +const skyflowConfig: ISkyflow = { + vaultID: "e20afc3ae1b54f0199f24130e51e0c11", + vaultURL: "https://testurl.com", + getBearerToken: jest.fn(), +}; + +const clientData = { + client: { + config: { ...skyflowConfig }, + metadata: { + uuid: mockUuid, + clientDomain: "test-domain", + }, + }, + context: { logLevel: LogLevel.ERROR, env: Env.PROD }, +}; + +const toJson = jest.fn(() => ({ + config: {}, + metaData: { + uuid: "", + sdkVersion: "skyflow-react-js@1.2.3", + }, +})); + +describe("Uploading files to the vault", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let windowSpy: jest.SpyInstance; + let testValue: any; + + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + + testValue = { + iFrameFormElement: { + fieldType: "FILE_INPUT", + state: { + value: { + type: "file", + name: "test-file.txt", + size: 1024, + }, + isFocused: false, + isValid: false, + isEmpty: true, + isComplete: false, + name: "test-name", + isRequired: true, + isTouched: false, + selectedCardScheme: "", + }, + tableName: "test-table-name", + preserveFileName: true, + onFocusChange: jest.fn(), + }, + }; + windowSpy = jest.spyOn(window, "parent", "get"); + windowSpy.mockImplementation(() => ({ + frames: {}, + })); + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve("access token")); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + jest.restoreAllMocks(); + Object.defineProperty(window.parent, "frames", { + value: undefined, + writable: true, + }); + if (windowSpy) { + windowSpy.mockRestore(); + } + }); + + test("should successfully handle FILE_UPLOAD validation case 1", (done) => { + testValue.iFrameFormElement.state.value.name = "test file.txt"; + windowSpy.mockImplementation(() => ({ + frames: { + "element:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + console.log(cb2.mock.calls[0][0].error.errorResponse[0]); + done(); + }, 1000); + }); + + test("should successfully handle FILE_UPLOAD validation case 2", (done) => { + testValue.iFrameFormElement.state.value.name = "test-file.txt"; + testValue.iFrameFormElement.state.value.size = 1024 * 1024 * 32; + windowSpy.mockImplementation(() => ({ + frames: { + "element:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + console.log(cb2.mock.calls[0][0].error.errorResponse[0]); + done(); + }, 1000); + }); + + test("should successfully handle FILE_UPLOAD validation case 3", (done) => { + testValue.iFrameFormElement.state.value.name = "test-file.txt"; + testValue.iFrameFormElement.state.value.size = 0; + windowSpy.mockImplementation(() => ({ + frames: { + "element:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + const clientReq = jest.fn(); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + console.log(cb2.mock.calls[0][0].error.errorResponse[0]); + done(); + }, 1000); + }); + + test("should successfully handle FILE_UPLOAD event and upload files", (done) => { + windowSpy.mockImplementation(() => ({ + frames: { + "element:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + const clientReq = jest.fn(() => + Promise.resolve(JSON.stringify({ skyflow_id: "file-upload-skyflow-id" })) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + expect(cb2.mock.calls[0][0]).toBeDefined(); + expect(cb2.mock.calls[0][0].fileUploadResponse).toBeDefined(); + expect(cb2.mock.calls[0][0].fileUploadResponse.length).toBe(1); + done(); + }, 1000); + }); + + test("should handle partial success/error in multiple FILE_UPLOAD attempts", (done) => { + windowSpy.mockImplementation(() => ({ + frames: { + "element1:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + "element2:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => ({ + iFrameFormElement: { + ...testValue.iFrameFormElement, + state: { + ...testValue.iFrameFormElement.state, + name: "file2", + }, + }, + }), + }, + }, + }, + })); + + // Mock client request to succeed for first file and fail for second + const clientReq = jest.fn((request) => { + if (request.body.get("file2")) { + return Promise.reject({ + error: { code: 400, description: "Upload failed" }, + }); + } else { + return Promise.resolve( + JSON.stringify({ skyflow_id: "success-file-id" }) + ); + } + }); + + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element1:FILE_INPUT:ID", "element2:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + const result = cb2.mock.calls[0][0]; + + console.log("mock result is\t", result); + + // Should have both success and error responses + expect(result.error.fileUploadResponse).toBeDefined(); + expect(result.error.errorResponse).toBeDefined(); + + // Check successful upload + expect(result.error.fileUploadResponse).toHaveLength(1); + const parsedResponse = JSON.parse(result.error.fileUploadResponse[0]); + expect(parsedResponse.skyflow_id).toBe('success-file-id'); + + // Check failed upload + expect(result.error.errorResponse).toHaveLength(1); + expect(result.error.errorResponse[0].error).toBeDefined(); + + done(); + }, 1000); + }); + + test("should fail upload files when client rejects promise", (done) => { + windowSpy.mockImplementation(() => ({ + frames: { + "element:FILE_INPUT:ID:CONTAINER-ID:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + const clientReq = jest.fn(() => + Promise.reject({ + error: "error", + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: UploadFileDataInput = { + type: COLLECT_TYPES.FILE_UPLOAD, + elementIds: ["element:FILE_INPUT:ID"], + containerId: "CONTAINER-ID", + }; + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + const firstCallArg = cb2.mock.calls[0][0]; + expect(firstCallArg).toBeDefined(); + expect(firstCallArg).toEqual({ + error: { errorResponse: [{ error: "error" }] }, + }); + + done(); + }, 1000); + }); +}); + +describe("SkyflowFrameController - tokenize function", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let windowSpy: jest.SpyInstance; + let testValue: any; + let testValue2: any; + + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + + testValue = { + iFrameFormElement: { + fieldType: "TEXT_INPUT", + state: { + value: "test-value", + isFocused: false, + isValid: true, + isEmpty: false, + isComplete: true, + name: "test-name", + isRequired: true, + isTouched: false, + selectedCardScheme: "", + }, + tableName: "test-table-name", + onFocusChange: jest.fn(), + getUnformattedValue: jest.fn(() => "unformatted-value"), + }, + }; + + testValue2 = { + iFrameFormElement: { + fieldType: "TEXT_INPUT", + state: { + value: "test-value2", + isFocused: false, + isValid: true, + isEmpty: false, + isComplete: true, + name: "test-name2", + isRequired: true, + isTouched: false, + selectedCardScheme: "", + }, + tableName: "test-table-name2", + skyflowID: "id", + onFocusChange: jest.fn(), + getUnformattedValue: jest.fn(() => "unformatted-value2"), + }, + }; + + windowSpy = jest.spyOn(window, "parent", "get"); + windowSpy.mockImplementation(() => ({ + frames: {}, + })); + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve("access token")); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + jest.restoreAllMocks(); + Object.defineProperty(window.parent, "frames", { + value: undefined, + writable: true, + }); + if (windowSpy) { + windowSpy.mockRestore(); + } + }); + + test("should tokenize data successfully", async () => { + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + const insertResponse = { + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1", "*": "ignored" } }, + ], + }; + const updateError = { + errors: [{ error: { code: 404, description: "Record not found" } }], + }; + + let requestCount = 0; + const clientReq = jest.fn((arg) => { + requestCount++; + if (arg.requestMethod === "PUT") { + return Promise.reject(updateError); + } + if (arg.requestMethod === "POST" && !arg.url.includes("/files")) { + return Promise.resolve(insertResponse); + } + return Promise.resolve(insertResponse); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + console.log("=======================>>>", cb2.mock.calls); + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should tokenize data successfully case 2", async () => { + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + const insertResponse = { + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1", "*": "ignored" } }, + ], + }; + const updateRes = { + records: [ + { + skyflow_id: "id", + fields: { + card_number: "4111-xxxx-xxxx-1111", + cvv: "123", + }, + }, + ], + }; + + let requestCount = 0; + const clientReq = jest.fn((arg) => { + requestCount++; + if (arg.requestMethod === "PUT") { + return Promise.resolve(updateRes); + } + if (arg.requestMethod === "POST" && !arg.url.includes("/files")) { + return Promise.resolve(insertResponse); + } + return Promise.resolve(insertResponse); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should tokenize data successfully case 3", async () => { + windowSpy.mockImplementation(() => ({ + frames: { + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + const insertResponse = { + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1", "*": "ignored" } }, + ], + }; + const updateRes = { + records: [ + { + skyflow_id: "id", + fields: { + card_number: "4111-xxxx-xxxx-1111", + cvv: "123", + }, + }, + ], + }; + + let requestCount = 0; + const clientReq = jest.fn((arg) => { + console.log("Request Count:", requestCount, "Arg:", arg); + requestCount++; + if (arg.requestMethod === "PUT") { + return Promise.resolve(updateRes); + } + if (arg.requestMethod === "POST" && !arg.url.includes("/files")) { + return Promise.resolve(insertResponse); + } + return Promise.resolve(insertResponse); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("tokenize data error case when skyflowID is empty", async () => { + testValue2.iFrameFormElement.skyflowID = ""; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].errors).toBeDefined(); + }, 1000); + }); + + test("tokenize data error case when skyflowID is null", async () => { + testValue2.iFrameFormElement.skyflowID = null; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].errors).toBeDefined(); + }, 1000); + }); + + test("tokenize data error case when isValid is false", async () => { + testValue2.iFrameFormElement.skyflowID = "null"; + testValue2.iFrameFormElement.state.isValid = false; + testValue2.iFrameFormElement.state.isRequired = false; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].errors).toBeDefined(); + }, 1000); + }); + + test("should tokenize data accessToken error ", async () => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => + Promise.reject({ error: "reject token error" }) + ); + + testValue2.iFrameFormElement.skyflowID = "dummy-skyflow-id"; + testValue2.iFrameFormElement.state.isValid = true; + testValue2.iFrameFormElement.state.isRequired = false; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId2:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue2), + }, + }, + }, + })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId2", + elementId: "elementId2", + }, + ], + }; + + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].errors).toBeDefined(); + }, 1000); + }); + + test("should tokenize data partial successfully", async () => { + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1", "*": "ignored" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should tokenize data partial successfully case 2", async () => { + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + const mockResponseBody = { + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1", "*": "ignored" } }, + ], + }; + let requestCount = 0; + const clientReq = jest.fn((arg) => { + console.log("Request Count:", requestCount, "Arg:", arg); + requestCount++; + if (arg.requestMethod === "POST" && !arg.url.includes("/files")) { + return Promise.resolve(mockResponseBody); + } + }); + + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should handle validations and set value when all conditions are met", async () => { + testValue.iFrameFormElement.validations = [{ rule: "regex", value: ".*" }]; + testValue.iFrameFormElement.state.isValid = true; + testValue.iFrameFormElement.state.isComplete = true; + const setValueMock = jest.fn(); + const onFocusChangeMock = jest.fn(); + testValue.iFrameFormElement.setValue = setValueMock; + testValue.iFrameFormElement.onFocusChange = onFocusChangeMock; + + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + jest + .spyOn( + require("../../../../src/core-utils/collect"), + "checkForElementMatchRule" + ) + .mockReturnValue(true); + jest + .spyOn( + require("../../../../src/core-utils/collect"), + "checkForValueMatch" + ) + .mockReturnValue(true); + + jest + .spyOn( + require("../../../../src/core-utils/collect"), + "constructElementsInsertReq" + ) + .mockImplementation(() => { + return [ + { records: [] }, + { + updateRecords: [ + { + table: "testTable", + fields: { key: "value" }, + skyflowID: "123", + }, + ], + }, + ]; + }); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(setValueMock).toHaveBeenCalledWith( + testValue.iFrameFormElement.state.value + ); + expect(onFocusChangeMock).toHaveBeenCalledWith(false); + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("successful insert and update requests", async () => { + testValue.iFrameFormElement.skyflowID = "test-id"; + windowSpy.mockImplementation(() => ({ + frames: { + "frame1:container123:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + "frame2:container123:ERROR:": { + document: { + getElementById: () => testValue, + }, + }, + }, + })); + const insertResponse = { + records: [{ skyflow_id: "inserted-id" }], + }; + const updateResponse = { + tokens: { + card_number: "token123", + cvv: "token456", + }, + }; + + const clientReq = jest.fn((arg) => { + return Promise.resolve(updateResponse); + }); + + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + const data: TokenizeDataInput = { + type: "COLLECT", + elementIds: [ + { frameId: "frame1", elementId: "element1" }, + { frameId: "frame2", elementId: "element2" }, + ], + containerId: "container123", + }; + const cb2 = jest.fn(); + onCb(data, cb2); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBeDefined(); + expect(cb2.mock.calls[0][0].error).toBeUndefined(); + }, 1000); + }); + + test("should successfully tokenize data when fieldType is checkbox", async () => { + testValue.iFrameFormElement.fieldType = "checkbox"; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { + records: [ + { + skyflow_id: "test-id-1", + }, + ], + }, + { + fields: { + "*": "some-random", + card_number: "4111-xxxx-xxxx-1111", + cvv: "123", + }, + }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should successfully tokenize data when fieldType is not checkbox", async () => { + testValue.iFrameFormElement.skyflowID = undefined; + testValue.iFrameFormElement.fieldType = "textarea"; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { + records: [ + { + skyflow_id: "test-id-1", + }, + ], + }, + { + fields: { + "*": "some-random", + card_number: "4111-xxxx-xxxx-1111", + cvv: "123", + }, + }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should successfully tokenize data when fieldType is not checkbox and validation exist", async () => { + testValue.iFrameFormElement.skyflowID = undefined; + testValue.iFrameFormElement.fieldType = "textarea"; + testValue.iFrameFormElement.validations = [ + { + rule: "regex", + value: ".*", + type: "ELEMENT_VALUE_MATCH_RULE", + }, + ]; + testValue.iFrameFormElement.isMatchEqual = jest.fn(() => true); + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { + records: [ + { + skyflow_id: "test-id-1", + }, + ], + }, + { + fields: { + "*": "some-random", + card_number: "4111-xxxx-xxxx-1111", + cvv: "123", + }, + }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should fail tokenize data when doesClientHasError is true", async () => { + testValue.iFrameFormElement.state.isValid = false; + testValue.iFrameFormElement.doesClientHasError = true; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + + const firstArg = cb2.mock.calls[0][0]; + expect(firstArg).toBeDefined(); + expect(firstArg).toHaveProperty("error"); + // done(); + }, 1000); + }); + + test("should fail tokenize data when doesClientHasError is false", async () => { + testValue.iFrameFormElement.state.isValid = false; + testValue.iFrameFormElement.doesClientHasError = false; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + + const firstArg = cb2.mock.calls[0][0]; + expect(firstArg).toBeDefined(); + expect(firstArg).toHaveProperty("error"); + // done(); + }, 1000); + }); + + test("should fail tokenize data when skyflowID is null or empty", async () => { + testValue.iFrameFormElement.state.isValid = false; + testValue.iFrameFormElement.doesClientHasError = false; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2).toHaveBeenCalled(); + + const firstArg = cb2.mock.calls[0][0]; + expect(firstArg).toBeDefined(); + expect(firstArg).toHaveProperty("error"); + // done(); + }, 1000); + }); + + test("should tokenize data when skyflowID is null or empty and not checkbox", async () => { + testValue.iFrameFormElement.fieldType = "textarea"; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should tokenize data when skyflowID is null or empty and not checkbox", async () => { + testValue.iFrameFormElement.fieldType = "textarea"; + testValue.iFrameFormElement.skyflowID = "test-skyflow-id"; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); + + test("should tokenize data when skyflowID is undefined and not checkbox", async () => { + testValue.iFrameFormElement.fieldType = "textarea"; + testValue.iFrameFormElement.skyflowID = undefined; + windowSpy.mockImplementation(() => ({ + frames: { + "frameId:containerId:ERROR:": { + document: { + getElementById: jest.fn(() => testValue), + }, + }, + }, + })); + + const clientReq = jest.fn(() => + Promise.resolve({ + responses: [ + { records: [{ skyflow_id: "id1" }] }, + { fields: { card_number: "token1" } }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[1][1]; + + const data: TokenizeDataInput = { + containerId: "containerId", + tokens: true, + type: "COLLECT", + elementIds: [ + { + frameId: "frameId", + elementId: "elementId", + }, + ], + }; + + const cb2 = jest.fn(); + + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeDefined(); + }, 1000); + }); +}); diff --git a/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts b/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts new file mode 100644 index 00000000..79786435 --- /dev/null +++ b/tests/core/internal/skyflow-frame/skyflow-frame-controller.test.ts @@ -0,0 +1,1697 @@ +/* +Copyright (c) 2025 Skyflow, Inc. +*/ +import bus from "framebus"; +import { + ELEMENT_EVENTS_TO_IFRAME, + PUREJS_TYPES, + REVEAL_TYPES, +} from "../../../../src/core/constants"; +import clientModule from "../../../../src/client"; +import * as busEvents from "../../../../src/utils/bus-events"; +import { + LogLevel, + Env, + RedactionType, + IRevealRecord, + IGetRecord, + IGetOptions, + IDeleteRecordInput, +} from "../../../../src/utils/common"; +import SkyflowFrameController from "../../../../src/core/internal/skyflow-frame/skyflow-frame-controller"; +import { InsertOptions } from "../../../../src/index-node"; +import { ISkyflow } from "../../../../src/skyflow"; +import Client from "../../../../src/client"; + +jest.mock("../../../../src/utils/bus-events", () => ({ + ...jest.requireActual("../../../../src/utils/bus-events"), + getAccessToken: jest.fn(() => Promise.resolve("access token")), +})); + +const on = jest.fn(); +const emit = jest.fn(); + +jest.mock("../../../../src/libs/uuid", () => ({ + __esModule: true, + default: jest.fn(() => mockUuid), +})); + +const mockUuid = "1244"; +const skyflowConfig: ISkyflow = { + vaultID: "e20afc3ae1b54f0199f24130e51e0c11", + vaultURL: "https://testurl.com", + getBearerToken: jest.fn(), +}; + +const clientData = { + client: { + config: { ...skyflowConfig }, + metadata: { + uuid: mockUuid, + }, + }, + context: { logLevel: LogLevel.ERROR, env: Env.PROD }, +}; + +const records = { + records: [ + { + table: "pii_fields", + fields: { + first_name: "Joseph", + primary_card: { + card_number: "4111111111111111", + cvv: "123", + }, + }, + }, + ], +}; + +const options: InsertOptions = { + tokens: true, + upsert: [ + { + table: "", + column: "", + }, + ], +}; + +const pushEventResponse = { + data: 1, +}; + +const insertResponse = { + vaultID: "vault123", + responses: [ + { + table: "table1", + records: [ + { + skyflow_id: "testId", + }, + ], + }, + { + table: "table1", + fields: { + "*": "testId", + first_name: "token1", + primary_card: { + card_number: "token2", + cvv: "token3", + }, + }, + }, + ], +}; + +const insertResponseWithoutTokens = { + vaultID: "vault123", + responses: [ + { + records: [ + { + skyflow_id: "testId", + }, + ], + }, + ], +}; + +const errorResponse = { + error: { + http_code: 403, + message: "RBAC: access denied", + }, +}; + +describe("push event", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + let onSpy: jest.SpyInstance; + + beforeEach(() => { + window.name = "controller:frameId:clientDomain:true"; + window.CoralogixRum = { + isInited: true, + init: jest.fn(), + info: jest.fn(), + }; + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + onSpy = jest.spyOn(bus, "on"); + targetSpy.mockReturnValue({ + on, + }); + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve("access token")); + }); + + test("before send function in init", (done) => { + const event = { + log_context: { + message: "SDK IFRAME EVENT", + }, + }; + window.CoralogixRum = { + isInited: false, + init: ({ beforeSend }) => { + beforeSend(event); + }, + }; + expect(event).toBeTruthy(); + const clientReq = jest.fn(() => Promise.resolve(pushEventResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + config: { + ...clientData.client.config, + options: { + ...clientData.client?.config?.options, + trackingKey: "aaaaabbbbbcccccdddddeeeeefffffggggg", + }, + }, + toJSON: toJson, + request: clientReq, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = onSpy.mock.calls[0][1]; + const data = { + event: { + element_id: "element123", + container_id: "container456", + vault_url: "http://example.com", + status: "Error", + events: ["MOUNTED"], + }, + }; + onCb(data); + setTimeout(() => { + expect(onCb).toBeTruthy(); + done(); + }, 1000); + }); + + test("init coralogix", (done) => { + const event = { + log_context: { + message: null, + }, + }; + window.CoralogixRum = { + isInited: false, + init: ({ beforeSend }) => { + beforeSend(event); + }, + }; + + const clientReq = jest.fn(() => Promise.resolve(pushEventResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + config: { + ...clientData.client.config, + options: { + ...clientData.client?.config?.options, + trackingKey: "aaaaabbbbbcccccdddddeeeeefffffggggg", + }, + }, + toJSON: toJson, + request: clientReq, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = onSpy.mock.calls[0][1]; + const data = { + event: { + element_id: "element123", + container_id: "container456", + vault_url: "http://example.com", + status: "Error", + events: ["MOUNTED"], + }, + }; + onCb(data); + setTimeout(() => { + expect(onCb).toBeTruthy(); + done(); + }, 1000); + }); + + test("push event with elementid", (done) => { + const clientReq = jest.fn(() => Promise.resolve(pushEventResponse)); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = onSpy.mock.calls[0][1]; + const data = { + event: { + element_id: "element123", + container_id: "container456", + vault_url: "http://example.com", + status: "Error", + events: ["MOUNTED"], + }, + }; + onCb(data); + setTimeout(() => { + expect(onCb).toBeTruthy(); + done(); + }, 1000); + }); + + test("push event with error", (done) => { + window.CoralogixRum = { + isInited: false, + init: jest.fn(), + info: jest.fn(), + }; + const clientReq = jest.fn(() => Promise.resolve(pushEventResponse)); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = onSpy.mock.calls[0][1]; + const data = {}; + onCb(data); + setTimeout(() => { + expect(onCb).toBeTruthy(); + done(); + }, 1000); + }); + + test("push event throw error resopnse", (done) => { + window.CoralogixRum = { + isInited: false, + init: jest.fn(), + }; + const clientReq = jest.fn(() => Promise.reject(errorResponse)); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = onSpy.mock.calls[0][1]; + const data = { + event: { + element_id: "element123", + container_id: "container456", + vault_url: "http://example.com", + status: "Error", + events: ["MOUNTED"], + }, + }; + onCb(data); + setTimeout(() => { + expect(onCb).toBeTruthy(); + done(); + }, 1000); + }); +}); + +describe("Inserting records into the vault", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("insert records with tokens as true", (done) => { + const clientReq = jest.fn(() => Promise.resolve(insertResponse)); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.INSERT, + records, + options, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + expect(cb2.mock.calls[0][0].records[0].fields).toBeDefined(); + expect(cb2.mock.calls[0][0].error).toBeUndefined(); + done(); + }, 1000); + }); + + test("insert records with tokens as false", (done) => { + const clientReq = jest.fn(() => + Promise.resolve(insertResponseWithoutTokens) + ); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.INSERT, + records, + options: { tokens: false, upsert: [{ table: "", column: " " }] }, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + expect(cb2.mock.calls[0][0].records[0].fields).toBeUndefined(); + expect(cb2.mock.calls[0][0].error).toBeUndefined(); + done(); + }, 1000); + }); + + test("insert records with error", (done) => { + const clientReq = jest.fn(() => Promise.reject(errorResponse)); + jest + .spyOn(clientModule, "fromJSON") + .mockImplementation( + () => + ({ ...clientData.client, request: clientReq } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.INSERT, + records, + options, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +const detokenizeRecords: IRevealRecord[] = [{ token: "token1" }]; + +const detokenizeRecordWithRedaction: IRevealRecord[] = [ + { + token: "token1", + redaction: RedactionType.MASKED, + }, +]; + +const detokenizeResponse = { + records: [ + { + token_id: "token1", + fields: { + cvv: "123", + }, + }, + ], +}; + +const detokenizeResponseWithRedaction = { + records: [ + { + token_id: "token1", + value: "123", + }, + ], +}; + +const detokenizeErrorResponse = { + error: { + grpc_code: 5, + http_code: 404, + message: "Token not found for token1", + http_status: "Not Found", + details: [], + }, +}; + +const toJson = jest.fn(() => ({ + config: {}, + metaData: { + uuid: "", + sdkVersion: "skyflow-react-js@1.2.3", + }, +})); + +describe("Retrieving data using skyflowId", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("getById success", (done) => { + const clientReq = jest.fn(() => Promise.resolve(getByIdRes)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET_BY_SKYFLOWID, + records: getByIdReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + done(); + }, 1000); + }); + + test("getById error", (done) => { + const clientReq = jest.fn(() => Promise.reject(errorResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET_BY_SKYFLOWID, + records: getByIdReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +describe("Retrieving data using skyflow tokens", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("detokenize success", (done) => { + const clientReq = jest.fn(() => Promise.resolve(detokenizeResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecords, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + done(); + }, 1000); + }); + + test("detokenize error", (done) => { + const clientReq = jest.fn(() => Promise.reject(detokenizeErrorResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecords, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeUndefined(); + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +const getByIdReq: IGetRecord[] = [ + { + ids: ["id1"], + redaction: RedactionType.PLAIN_TEXT, + table: "table1", + }, +]; + +const getByIdReqWithoutRedaction: IGetRecord[] = [ + { + ids: ["id1"], + table: "table1", + }, +]; + +const getOptionsTrue: IGetOptions = { tokens: true }; +const getOptionsFalse: IGetOptions = { tokens: false }; + +const getByColumnReq: IGetRecord[] = [ + { + columnValues: ["id1", "id2", "id3"], + columnName: "column1", + redaction: RedactionType.PLAIN_TEXT, + table: "table1", + }, +]; + +const getByIdRes = { + records: [ + { + fields: { + skyflow_id: "id1", + cvv: "123", + }, + }, + ], +}; + +describe("Retrieving data using skyflow tokens", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("detokenize success", (done) => { + const clientReq = jest.fn(() => + Promise.resolve(detokenizeResponseWithRedaction) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecordWithRedaction, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + done(); + }, 1000); + }); + + test("detokenize error", (done) => { + const clientReq = jest.fn(() => Promise.reject(detokenizeErrorResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecordWithRedaction, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records).toBeUndefined(); + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +describe("Retrieving data using get", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("get success", (done) => { + const clientReq = jest.fn(() => Promise.resolve(getByIdRes)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByIdReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + done(); + }, 1000); + }); + + test("get success second case", (done) => { + const clientReq = jest.fn(() => Promise.resolve(getByIdRes)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByColumnReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + done(); + }, 1000); + }); + + test("get success case should have single column_name for multiple column values ", (done) => { + let reqArg; + const clientReq = jest.fn((arg) => { + reqArg = arg; + return Promise.resolve(getByIdRes); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByColumnReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + try { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + expect(reqArg.url.match(/column_name=column1/gi)?.length).toBe(1); + done(); + } catch (err) { + done(err); + } + }, 1000); + }); + + test("get method should send request url with tokenization true and without redaction when tokens flag is true", (done) => { + let reqArg; + const clientReq = jest.fn((arg) => { + reqArg = arg; + return Promise.resolve(getByIdRes); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByIdReqWithoutRedaction, + options: getOptionsTrue, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + try { + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + expect(reqArg.url.includes("tokenization=true")).toBe(true); + expect(reqArg.url.includes("redaction=PLAIN_TEXT")).toBe(false); + done(); + }, 1000); + } catch (err) { + done(err); + } + }); + + test("get method should send request url with tokenization false and redaction when tokens flag is false", (done) => { + let reqArg; + const clientReq = jest.fn((arg) => { + reqArg = arg; + return Promise.resolve(getByIdRes); + }); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByIdReq, + options: getOptionsFalse, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + try { + setTimeout(() => { + expect(cb2.mock.calls[0][0].records.length).toBe(1); + expect(reqArg.url.includes("tokenization=false")).toBe(true); + expect(reqArg.url.includes("redaction=PLAIN_TEXT")).toBe(true); + done(); + }, 1000); + } catch (err) { + done(err); + } + }); + + test("get error", (done) => { + const clientReq = jest.fn(() => Promise.reject(errorResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.GET, + records: getByIdReq, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +describe("Failed to fetch accessToken get", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("accessToken error", (done) => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject({ error: "error" })); + SkyflowFrameController.init(mockUuid); + const onCb = on.mock.calls[0][1]; + + const insertData = { + type: PUREJS_TYPES.INSERT, + records, + options: { tokens: false, upsert: [{ table: "", column: " " }] }, + }; + const insertCb = jest.fn(); + onCb(insertData, insertCb); + + const detokenizeData = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecords, + }; + const detokenizeCb = jest.fn(); + onCb(detokenizeData, detokenizeCb); + + const getByIdData = { + type: PUREJS_TYPES.GET, + records: getByIdReq, + }; + const getByIdCb = jest.fn(); + onCb(getByIdData, getByIdCb); + + setTimeout(() => { + expect(insertCb.mock.calls[0][0].error).toBeDefined(); + expect(detokenizeCb.mock.calls[0][0].error).toBeDefined(); + expect(getByIdCb.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +describe("Failed to fetch accessToken Getbyid", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + }); + }); + + test("accessToken error", (done) => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject({ error: "error" })); + SkyflowFrameController.init(mockUuid); + const onCb = on.mock.calls[0][1]; + + const insertData = { + type: PUREJS_TYPES.INSERT, + records, + options: { tokens: false, upsert: [{ table: "", column: " " }] }, + }; + const insertCb = jest.fn(); + onCb(insertData, insertCb); + + const detokenizeData = { + type: PUREJS_TYPES.DETOKENIZE, + records: detokenizeRecords, + }; + const detokenizeCb = jest.fn(); + onCb(detokenizeData, detokenizeCb); + + const getByIdData = { + type: PUREJS_TYPES.GET, + records: getByIdReq, + }; + const getByIdCb = jest.fn(); + onCb(getByIdData, getByIdCb); + + setTimeout(() => { + expect(insertCb.mock.calls[0][0].error).toBeDefined(); + expect(detokenizeCb.mock.calls[0][0].error).toBeDefined(); + expect(getByIdCb.mock.calls[0][0].error).toBeDefined(); + done(); + }, 1000); + }); +}); + +const deleteRecords: IDeleteRecordInput = { + records: [ + { + table: "pii_fields", + id: "29ebda8d-5272-4063-af58-15cc674e332b", + }, + { + table: "pii_fields", + id: "29ebda8d-5272-4063-af58-15cc674e332b", + }, + ], +}; + +const deleteOptions = {}; + +const deleteResponse = { + skyflow_id: "29ebda8d-5272-4063-af58-15cc674e332b", + deleted: true, +}; + +const deleteErrorResponse = { + error: { + grpc_code: 5, + http_code: 404, + message: "No Records Found", + http_status: "Not Found", + details: [], + }, +}; + +describe("Deleting records from the vault", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + emit, + }); + }); + + test("delete records success", (done) => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve({ token: "token123" })); + const clientReq = jest.fn(() => Promise.resolve(deleteResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DELETE, + records: deleteRecords, + options: deleteOptions, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + try { + expect(cb2.mock.calls[0][0].records.length).toBe(2); + expect(cb2.mock.calls[0][0].records[0].deleted).toBeTruthy(); + expect(cb2.mock.calls[0][0].records[1].deleted).toBeTruthy(); + expect(cb2.mock.calls[0][0].error).toBeUndefined(); + done(); + } catch (err) { + done(err); + } + }, 1000); + }); + + test("delete records with error", (done) => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.resolve({ token: "token123" })); + const clientReq = jest.fn(() => Promise.reject(deleteErrorResponse)); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const data = { + type: PUREJS_TYPES.DELETE, + records: deleteRecords, + options: deleteOptions, + }; + const cb2 = jest.fn(); + onCb(data, cb2); + + setTimeout(() => { + try { + expect(cb2.mock.calls[0][0].error).toBeDefined(); + done(); + } catch (err) { + done(err); + } + }, 1000); + }); + + test("accessToken error while deleting records", (done) => { + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject({ error: "error" })); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[0][0]; + const emitCb = emitSpy.mock.calls[0][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.PUREJS_FRAME_READY + mockUuid + ); + emitCb(clientData); + + const onCb = on.mock.calls[0][1]; + const deleteData = { + type: PUREJS_TYPES.DELETE, + records: deleteRecords, + options: deleteOptions, + }; + const deleteCb = jest.fn(); + onCb(deleteData, deleteCb); + + setTimeout(() => { + try { + expect(deleteCb.mock.calls[0][0].error).toBeDefined(); + done(); + } catch (err) { + done(err); + } + }, 1000); + }); +}); + +describe("test render file request", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + emit, + }); + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject({ error: "error" })); + }); + + test("render files error case", () => { + const clientReq = jest.fn(() => + Promise.reject({ + errors: [ + { + skyflowID: "1815-6223-1073-1425", + error: { code: 404, description: "id not found" }, + }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = { + records: [ + { + skyflowID: "1815-6223-1073-1425", + column: "file", + table: "table1", + }, + ], + }; + const data1 = { + type: REVEAL_TYPES.RENDER_FILE, + records: data, + containerId: "123", + iframeName: "123", + }; + const emitterCb = jest.fn(); + + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + setTimeout(() => { + expect(emitterCb.mock.calls[0][0].error).toBeDefined(); + expect(emitterCb.mock.calls[0][0].error).toEqual({ + code: 404, + description: "id not found", + }); + }, 10000); + }); + + test("render files succes case", () => { + const clientReq = jest.fn(() => + Promise.resolve({ + fields: { skyflow_id: "1815-6223-1073-1425", file: "https://demo.com" }, + tokens: null, + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = { + skyflowID: "1815-6223-1073-1425", + column: "file", + table: "table1", + }; + const data1 = { + type: REVEAL_TYPES.RENDER_FILE, + records: data, + containerId: "123", + iframeName: "123", + }; + const emitterCb = jest.fn(); + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + }); +}); + +describe("test reveal request", () => { + let emitSpy: jest.SpyInstance; + let targetSpy: jest.SpyInstance; + beforeEach(() => { + emitSpy = jest.spyOn(bus, "emit"); + targetSpy = jest.spyOn(bus, "target"); + targetSpy.mockReturnValue({ + on, + emit, + }); + jest + .spyOn(busEvents, "getAccessToken") + .mockImplementation(() => Promise.reject({ error: "error" })); + }); + + test("reveal data error case", () => { + const clientReq = jest.fn(() => + Promise.reject({ + errors: [ + { + token: "1815-6223-1073-1425", + error: { code: 404, description: "token not found" }, + }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + SkyflowFrameController.init(mockUuid); + + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = { + records: [ + { + token: "1815-6223-1073-1425", + }, + ], + }; + const data1 = { + type: REVEAL_TYPES.REVEAL, + records: data, + containerId: "123", + iframeName: "123", + }; + const emitterCb = jest.fn(); + + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + setTimeout(() => { + expect(emitterCb.mock.calls[0][0].error).toBeDefined(); + expect(emitterCb.mock.calls[0][0].error).toEqual({ + code: 404, + description: "token not found", + }); + }, 10000); + }); + + test("reveal succes case", () => { + const clientReq = jest.fn(() => + Promise.resolve({ + records: [ + { token: "7402-2242-2342-232", value: "231", valueType: "STRING" }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = [ + { + token: "1815-6223-1073-1425", + }, + ]; + const data1 = { + type: REVEAL_TYPES.REVEAL, + records: data, + containerId: "123", + iframeName: "123", + }; + const emitterCb = jest.fn(); + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + }); + + test("reveal data with redaction type", () => { + const clientReq = jest.fn(() => + Promise.resolve({ + records: [ + { token: "7402-2242-2342-232", value: "231", valueType: "STRING" }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = [ + { + token: "1815-6223-1073-1425", + redaction: RedactionType.MASKED, + }, + ]; + const data1 = { + type: REVEAL_TYPES.REVEAL, + records: data, + containerId: "123", + iframeName: "123", + }; + const emitterCb = jest.fn(); + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + }); + + test("reveal data with redaction type and without token", () => { + const clientReq = jest.fn(() => + Promise.resolve({ + records: [ + { token: "7402-2242-2342-232", value: "231", valueType: "STRING" }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = [ + { + token: "1815-6223-1073-1425", + redaction: RedactionType.MASKED, + }, + ]; + const data1 = { + type: REVEAL_TYPES.REVEAL, + records: data, + containerId: "123", + iframeName: "123", + options: { tokens: false }, + }; + const emitterCb = jest.fn(); + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + }); + + test("reveal data with redaction type and without token and without tokenization", () => { + const clientReq = jest.fn(() => + Promise.resolve({ + records: [ + { token: "7402-2242-2342-232", value: "231", valueType: "STRING" }, + ], + }) + ); + jest.spyOn(clientModule, "fromJSON").mockImplementation( + () => + ({ + ...clientData.client, + request: clientReq, + toJSON: toJson, + } as unknown as Client) + ); + + SkyflowFrameController.init(mockUuid); + const emitEventName = emitSpy.mock.calls[1][0]; + const emitCb = emitSpy.mock.calls[1][2]; + expect(emitEventName).toBe( + ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + mockUuid + ); + emitCb(clientData); + const revelRequestEventName = + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + mockUuid; + const data = [ + { + token: "1815-6223-1073-1425", + redaction: RedactionType.MASKED, + }, + ]; + const data1 = { + type: REVEAL_TYPES.REVEAL, + records: data, + containerId: "123", + iframeName: "123", + options: { tokens: false }, + }; + const emitterCb = jest.fn(); + const onCbName = on.mock.calls[2][0]; + expect(onCbName).toBe(revelRequestEventName); + const onCb = on.mock.calls[2][1]; + onCb(data1, emitterCb); + }); +}); From 99ff0255aec9ecae1d7e9f321c9ab3504b256f3a Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Mon, 20 Oct 2025 08:14:06 +0000 Subject: [PATCH 21/47] [AUTOMATED] Release - 2.4.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d03b0994..41c443e7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.10-dev.8bf4fef", + "version": "2.4.4", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 09e6781fca72328df8b5bb69b780b6059952df97 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow <156889717+saileshwar-skyflow@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:59:40 +0530 Subject: [PATCH 22/47] SK-2219: Revert public release version (#635) * SK-2219: revert public release version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41c443e7..f2b8d2ba 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.4", + "version": "2.4.4-dev.a5c1e49", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 16f56460b288c82a8790cd796d39a95252762cfe Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Mon, 20 Oct 2025 10:32:09 +0000 Subject: [PATCH 23/47] [AUTOMATED] Release - 2.4.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2b8d2ba..41c443e7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.4-dev.a5c1e49", + "version": "2.4.4", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 32d8779bf20330650f6f958fc5946cdff8b3e0e6 Mon Sep 17 00:00:00 2001 From: skyflow-bharti <118584001+skyflow-bharti@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:26:10 +0530 Subject: [PATCH 24/47] Release/25.7.8.2 (#615) --- src/core-utils/collect.ts | 147 +++- src/core-utils/reveal.ts | 109 +++ src/core/constants.ts | 24 + src/core/external/collect/collect-element.ts | 250 ++++--- .../collect/compose-collect-container.ts | 372 +++++++---- .../collect/compose-collect-element.ts | 57 +- .../reveal/composable-reveal-container.ts | 474 +++++++++++++ .../reveal/composable-reveal-element.ts | 66 ++ .../reveal/composable-reveal-internal.ts | 610 +++++++++++++++++ .../internal/composable-frame-element-init.ts | 369 +++++++++++ src/core/internal/frame-element-init.ts | 625 +++++++++++++++++- src/core/internal/iframe-form/index.ts | 181 ++++- src/core/internal/index.ts | 39 +- src/core/internal/internal-types/index.ts | 1 + src/core/internal/reveal/reveal-frame.ts | 299 +++++++-- .../skyflow-frame/skyflow-frame-controller.ts | 8 +- src/index-internal.ts | 5 + src/libs/element-options.ts | 4 +- src/skyflow.ts | 82 ++- src/utils/common/index.ts | 12 + src/utils/constants.ts | 6 + src/utils/logs.ts | 6 + 22 files changed, 3451 insertions(+), 295 deletions(-) create mode 100644 src/core/external/reveal/composable-reveal-container.ts create mode 100644 src/core/external/reveal/composable-reveal-element.ts create mode 100644 src/core/external/reveal/composable-reveal-internal.ts create mode 100644 src/core/internal/composable-frame-element-init.ts diff --git a/src/core-utils/collect.ts b/src/core-utils/collect.ts index 93181075..a0fbb98d 100644 --- a/src/core-utils/collect.ts +++ b/src/core-utils/collect.ts @@ -205,7 +205,7 @@ const updateRecordsInVault = ( options, ) => { const table = skyflowIdRecord.fields.table; - const skyflowID = skyflowIdRecord.skyflowID; + const skyflowID = skyflowIdRecord?.skyflowID; skyflowIdRecord.fields = omit(skyflowIdRecord.fields, 'table'); skyflowIdRecord.fields = omit(skyflowIdRecord.fields, 'skyflowID'); return client.request({ @@ -279,6 +279,151 @@ export const updateRecordsBySkyflowID = async ( }); }); +export const updateRecordsBySkyflowIDComposable = async ( + skyflowIdRecords, + client: Client, + options, + authToken: string, +) => new Promise((rootResolve, rootReject) => { + let updateResponseSet: Promise[]; + // eslint-disable-next-line prefer-const + updateResponseSet = skyflowIdRecords?.updateRecords?.map( + (skyflowIdRecord: IInsertRecord) => new Promise((resolve, reject) => { + updateRecordsInVault(skyflowIdRecord, client, authToken, options) + ?.then((resolvedResult: any) => { + const resp = constructFinalUpdateRecordResponse( + resolvedResult, + options?.tokens, + skyflowIdRecord, + ); + resolve(resp); + }, + (rejectedResult) => { + let errorResponse = rejectedResult; + if (rejectedResult?.error) { + errorResponse = { + error: { + code: rejectedResult?.error?.code, + description: rejectedResult?.error?.description, + }, + }; + } + printLog(rejectedResult?.error?.description ?? '', MessageType.ERROR, LogLevel.ERROR); + reject(errorResponse); + })?.catch((error) => { + reject(error); + }); + }), + ); + Promise.allSettled(updateResponseSet)?.then((resultSet: any) => { + const recordsResponse: any[] = []; + const errorsResponse: any[] = []; + resultSet?.forEach((result: { status: string; value: any; reason?: any; }) => { + if (result?.status === 'fulfilled') { + recordsResponse?.push(result?.value); + } else { + errorsResponse?.push(result?.reason); + } + }); + + if (errorsResponse?.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse?.length === 0) { + rootReject({ errors: errorsResponse }); + } else { + rootReject({ records: recordsResponse, errors: errorsResponse }); + } + }); +}); + +export const insertDataInCollect = async ( + records, + client: Client, + options, + finalInsertRecords, + authToken: string, +) => new Promise((resolve) => { + let insertResponse: any; + let insertErrorResponse: any; + client + ?.request({ + body: { + records, + }, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + ?.then((response: any) => { + insertResponse = constructInsertRecordResponse( + response, + options?.tokens, + finalInsertRecords?.records, + ); + resolve(insertResponse); + }) + ?.catch((error) => { + insertErrorResponse = { + errors: [ + { + error: { + code: error?.error?.code, + description: error?.error?.description, + }, + }, + ], + }; + resolve(insertErrorResponse); + }); +}); + +export const insertDataInMultipleFiles = async ( + records, + client: Client, + options, + finalInsertRecords, + authToken: string, +) => new Promise((resolve) => { + let insertResponse: any; + let insertErrorResponse: any; + client + ?.request({ + body: { + records, + }, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + ?.then((response: any) => { + insertResponse = constructInsertRecordResponse( + response, + options?.tokens, + finalInsertRecords?.records, + ); + resolve(insertResponse); + }) + ?.catch((error) => { + insertErrorResponse = { + errors: [ + { + error: { + code: error?.error?.code, + description: error?.error?.description, + }, + }, + ], + }; + resolve(insertErrorResponse); + }); +}); + export const checkForElementMatchRule = (validations: IValidationRule[]) => { if (!validations) return false; for (let i = 0; i < validations.length; i += 1) { diff --git a/src/core-utils/reveal.ts b/src/core-utils/reveal.ts index 7e11fc37..f2087d84 100644 --- a/src/core-utils/reveal.ts +++ b/src/core-utils/reveal.ts @@ -16,6 +16,7 @@ import { GetResponseRecord, GetByIdResponse, GetByIdResponseRecord, + IRevealRecordComposable, } from '../utils/common'; import { printLog } from '../utils/logs-helper'; import { FILE_DOWNLOAD_URL_PARAM } from '../core/constants'; @@ -182,6 +183,27 @@ export const getFileURLFromVaultBySkyflowID = ( rootReject(err); } }); + +export const getFileURLFromVaultBySkyflowIDComposable = ( + skyflowIdRecord: IRevealRecord, + client: Client, + authToken: string, +): Promise => new Promise((rootResolve, rootReject) => { + try { + getFileURLForRender( + skyflowIdRecord, client, authToken as string, + ).then((resolvedResult: IRenderResponseType) => { + rootResolve(resolvedResult); + }).catch((err: any) => { + const errorData = formatForRenderFileFailure(err, skyflowIdRecord.skyflowID as string, + skyflowIdRecord.column as string); + printLog(errorData.error?.description || '', MessageType.ERROR, LogLevel.ERROR); + rootReject(errorData); + }); + } catch (err) { + rootReject(err); + } +}); export const fetchRecordsByTokenId = ( tokenIdRecords: IRevealRecord[], client: Client, @@ -235,6 +257,65 @@ export const fetchRecordsByTokenId = ( rootReject(err); }); }); + +export const fetchRecordsByTokenIdComposable = ( + tokenIdRecords: IRevealRecordComposable[], + client: Client, + authToken: string, +): Promise => new Promise((rootResolve, rootReject) => { + const vaultResponseSet: Promise[] = tokenIdRecords?.map( + (tokenRecord) => new Promise((resolve) => { + const apiResponse: any = []; + const redaction: RedactionType = tokenRecord?.redaction ?? RedactionType.PLAIN_TEXT; + + getTokenRecordsFromVault(tokenRecord?.token ?? '', redaction, client, authToken) + ?.then( + (response: IApiSuccessResponse) => { + const fieldsData = formatForPureJsSuccess(response); + apiResponse?.push({ + ...fieldsData, + frameId: tokenRecord?.iframeName ?? '', + }); + }, + (cause: any) => { + const errorData = formatForPureJsFailure(cause, tokenRecord?.token ?? ''); + printLog(errorData?.error?.description ?? '', MessageType.ERROR, LogLevel.ERROR); + apiResponse?.push({ + ...errorData, + frameId: tokenRecord?.iframeName ?? '', + }); + }, + ) + ?.finally(() => { + resolve(apiResponse); + }); + }), + ); + + Promise.allSettled(vaultResponseSet)?.then((resultSet) => { + const recordsResponse: Record[] = []; + const errorResponse: Record[] = []; + resultSet?.forEach((result) => { + if (result?.status === 'fulfilled') { + result?.value?.forEach((res: Record) => { + if (Object.prototype.hasOwnProperty.call(res, 'error')) { + errorResponse?.push(res); + } else { + recordsResponse?.push(res); + } + }); + } + }); + if (errorResponse?.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse?.length === 0) { + rootReject({ errors: errorResponse }); + } else { + rootReject({ records: recordsResponse, errors: errorResponse }); + } + }); +}); + export const formatRecordsForIframe = (response: IRevealResponseType) => { const result: Record = {}; if (response.records) { @@ -295,6 +376,34 @@ export const formatRecordsForClient = (response: IRevealResponseType): RevealRes return revealResponse; }; +export const formatRecordsForClientComposable = (response) => { + let successRecords = []; + let errorRecords = []; + + if (response?.errors && response?.errors?.length > 0) { + errorRecords = response?.errors?.map((errors) => ({ + error: errors?.error ?? {}, + })); + } + + if (response?.records) { + successRecords = response?.records?.map((record) => ({ + token: record?.[0]?.token ?? '', + valueType: record?.[0]?.valueType ?? '', + })); + } + + if (successRecords?.length > 0 && errorRecords?.length > 0) { + return { success: successRecords, errors: errorRecords }; + } + + if (successRecords?.length > 0) { + return { success: successRecords }; + } + + return { errors: errorRecords }; +}; + export const fetchRecordsGET = async ( skyflowIdRecords: IGetRecord[], client: Client, diff --git a/src/core/constants.ts b/src/core/constants.ts index 57196408..7e2a2761 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -30,11 +30,13 @@ export const SDK_IFRAME_EVENT = 'SDK IFRAME EVENT'; export const DOMAIN = 'US2'; export const CORALOGIX_DOMAIN = 'https://cdn.rum-ingress-coralogix.com/coralogix/browser/latest/coralogix-browser-sdk.js'; export const FRAME_ELEMENT = 'element'; +export const COMPOSABLE_REVEAL = 'reveal-composable'; export const ELEMENT_TYPES = { COLLECT: 'COLLECT', REVEAL: 'REVEAL', COMPOSE: 'COMPOSABLE', + REVEAL_COMPOSE: 'REVEAL_COMPOSE', }; export const EVENT_TYPES = { @@ -104,8 +106,19 @@ export const ELEMENT_EVENTS_TO_CLIENT = { }; export const ELEMENT_EVENTS_TO_IFRAME = { + MULTIPLE_UPLOAD_FILES_RESPONSE: 'MULTIPLE_UPLOAD_FILES_RESPONSE', + RENDER_MOUNTED: 'RENDER_MOUNTED', + HEIGHT_CALLBACK: 'HEIGHT_CALLBACK', + HEIGHT_CALLBACK_COMPOSABLE: 'HEIGHT_CALLBACK_COMPOSABLE', + COMPOSABLE_REVEAL: 'COMPOSABLE_REVEAL', + MULTIPLE_UPLOAD_FILES: 'MULTIPLE_UPLOAD_FILES', COLLECT_CALL_REQUESTS: 'COLLECT_CALL_REQUESTS', + COMPOSABLE_CALL_REQUESTS: 'COMPOSABLE_CALL_REQUESTS', + COMPOSABLE_CALL_RESPONSE: 'COMPOSABLE_CALL_RESPONSE', + COMPOSABLE_FILE_CALL_RESPONSE: 'COMPOSABLE_FILE_CALL_RESPONSE', + COMPOSABLE_CONTAINER: 'COMPOSABLE_CONTAINER', REVEAL_CALL_REQUESTS: 'REVEAL_CALL_REQUESTS', + REVEAL_CALL_RESPONSE: 'REVEAL_CALL_RESPONSE', FRAME_READY: 'FRAME_READY', READY_FOR_CLIENT: 'READY_FOR_CLIENT', TOKENIZATION_REQUEST: 'TOKENIZATION_REQUEST', @@ -162,6 +175,7 @@ export enum ElementType { EXPIRATION_MONTH = 'EXPIRATION_MONTH', EXPIRATION_YEAR = 'EXPIRATION_YEAR', FILE_INPUT = 'FILE_INPUT', + MULTI_FILE_INPUT = 'MULTI_FILE_INPUT', } export enum CardType { @@ -324,6 +338,14 @@ export const ELEMENTS = { type: 'file', }, }, + [ElementType.MULTI_FILE_INPUT]: { + name: 'MULTI_FILE_INPUT', + sensitive: true, + attributes: { + type: 'file', + multiple: '', + }, + }, }; export const CARDNUMBER_INPUT_FORMAT = { @@ -637,6 +659,7 @@ export const DEFAULT_ERROR_TEXT_ELEMENT_TYPES = { [ElementType.EXPIRATION_MONTH]: 'Invalid expiration month', [ElementType.EXPIRATION_YEAR]: 'Invalid expiration year', [ElementType.FILE_INPUT]: logs.errorLogs.INVALID_COLLECT_VALUE, + [ElementType.MULTI_FILE_INPUT]: logs.errorLogs.INVALID_COLLECT_VALUE, }; export const DEFAULT_REQUIRED_TEXT_ELEMENT_TYPES = { @@ -649,6 +672,7 @@ export const DEFAULT_REQUIRED_TEXT_ELEMENT_TYPES = { [ElementType.EXPIRATION_MONTH]: 'expiration month is required', [ElementType.EXPIRATION_YEAR]: 'expiration year is required', [ElementType.FILE_INPUT]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, + [ElementType.MULTI_FILE_INPUT]: logs.errorLogs.DEFAULT_REQUIRED_COLLECT_VALUE, }; export const INPUT_KEYBOARD_EVENTS = { diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index 8dfc05de..6d73de71 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -120,7 +120,6 @@ class CollectElement extends SkyflowElement { // if (this.#isSingleElementAPI && this.#elements.length > 1) { // throw new SkyflowError(SKYFLOW_ERROR_CODE.UNKNOWN_ERROR, [], true); // } - this.#doesReturnValue = EnvOptions[this.#context.env].doesReturnValue; this.elementType = this.#isSingleElementAPI ? this.#elements[0].elementType @@ -165,18 +164,18 @@ class CollectElement extends SkyflowElement { this.#readyToMount = container.isMounted; if (container.type === ContainerType.COMPOSABLE) { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + + this.#iframe.name) { + this.#iframe.setIframeHeight(event.data.data.height); + } + }); this.#elements.forEach((element) => { - this.#bus.on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED - + formatFrameNameToId(element.elementName), (data) => { - if (data.name === element.elementName) { - updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, `${element.elementType}_${METRIC_TYPES.EVENTS.MOUNTED}`); + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + formatFrameNameToId(element.elementName)) { element.isMounted = true; this.#mounted = true; - this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT - + this.#iframe.name, - {}, (payload:any) => { - this.#iframe.setIframeHeight(payload.height); - }); } }); }); @@ -200,6 +199,7 @@ class CollectElement extends SkyflowElement { getID = () => this.#elementId; mount = (domElement: HTMLElement | string) => { + this.#mounted = true; if (!domElement) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['CollectElement'], true); } @@ -469,84 +469,168 @@ class CollectElement extends SkyflowElement { } }); this.#elements.forEach((element1) => { - this.#bus.on(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT - + element1.elementName, (data: any) => { - if ( - this.#isSingleElementAPI + const isComposableContainer = this.#elements.length > 1; + if (isComposableContainer) { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + + element1.elementName) { + const data = event.data.data; + if (data.name === element1.elementName) { + if ( + this.#isSingleElementAPI && data.event === ELEMENT_EVENTS_TO_CLIENT.READY && data.name === formatFrameNameToId(this.#iframe.name) - ) { - this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); - } else { - const isComposable = this.#elements.length > 1; - this.#elements.forEach((element, index) => { - if (data.name === element.elementName) { - let emitEvent = ''; - switch (data.event) { - case ELEMENT_EVENTS_TO_CLIENT.FOCUS: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.FOCUS; - break; - case ELEMENT_EVENTS_TO_CLIENT.BLUR: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.BLUR; - break; - case ELEMENT_EVENTS_TO_CLIENT.CHANGE: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.CHANGE; - break; - case ELEMENT_EVENTS_TO_CLIENT.READY: - emitEvent = ELEMENT_EVENTS_TO_CLIENT.READY; - break; - case ELEMENT_EVENTS_TO_CLIENT.SUBMIT: - this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.SUBMIT); - return; - // case ELEMENT_EVENTS_TO_CLIENT.CREATED: - // this.#mounted = true; - // return; - // todo: need to implement the below events - // case ELEMENT_EVENTS_TO_CLIENT.ESCAPE: - // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ESCAPE); - // break; - // case ELEMENT_EVENTS_TO_CLIENT.CLICK: - // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.CLICK); - // break; - // case ELEMENT_EVENTS_TO_CLIENT.ERROR: - // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ERROR); - // break; - - default: - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_EVENT_TYPE, [], true); - } - this.#states[index].isEmpty = data.value.isEmpty; - this.#states[index].isValid = data.value.isValid; - this.#states[index].isComplete = data.value.isComplete; - this.#states[index].isFocused = data.value.isFocused; - this.#states[index].isRequired = data.value.isRequired; - this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; - - if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; - else this.#states[index].value = undefined; - - emitEvent = isComposable ? `${emitEvent}:${data.name}` : emitEvent; - - this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT - + this.#iframe.name, - {}, (payload:any) => { - this.#iframe.setIframeHeight(payload.height); - }); - - this.#updateState(); - const emitData = { - ...this.#states[index], - elementType: element.elementType, - }; - if (isComposable && this.#groupEmitter) { - this.#groupEmitter._emit(emitEvent, emitData); + ) { + this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); } else { - this.#eventEmitter._emit(emitEvent, emitData); + const isComposable = this.#elements.length > 1; + this.#elements.forEach((element, index) => { + if (data.name === element.elementName) { + let emitEvent = ''; + switch (data.event) { + case ELEMENT_EVENTS_TO_CLIENT.FOCUS: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.FOCUS; + break; + case ELEMENT_EVENTS_TO_CLIENT.BLUR: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.BLUR; + break; + case ELEMENT_EVENTS_TO_CLIENT.CHANGE: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.CHANGE; + break; + case ELEMENT_EVENTS_TO_CLIENT.READY: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.READY; + break; + case ELEMENT_EVENTS_TO_CLIENT.SUBMIT: + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.SUBMIT); + return; + + default: + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_EVENT_TYPE, [], true); + } + this.#states[index].isEmpty = data.value.isEmpty; + this.#states[index].isValid = data.value.isValid; + this.#states[index].isComplete = data.value.isComplete; + this.#states[index].isFocused = data.value.isFocused; + this.#states[index].isRequired = data.value.isRequired; + this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; + if (element.elementType === ElementType.MULTI_FILE_INPUT) { + this.#states[index].metaData = data?.value?.metaData || []; + } + if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; + else this.#states[index].value = undefined; + + emitEvent = isComposable ? `${emitEvent}:${data.name}` : emitEvent; + this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + + this.#updateState(); + const emitData = { + ...this.#states[index], + elementType: element.elementType, + }; + if (isComposable) { + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, { + iframeName: this.#iframe.name, + }); + } + if (isComposable && this.#groupEmitter) { + this.#groupEmitter._emit(emitEvent, emitData); + } else { + this.#eventEmitter._emit(emitEvent, emitData); + } + } + }); } } - }); - } - }); + } + }); + } else { + this.#bus.on(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + + element1.elementName, (data: any) => { + if ( + this.#isSingleElementAPI + && data.event === ELEMENT_EVENTS_TO_CLIENT.READY + && data.name === formatFrameNameToId(this.#iframe.name) + ) { + this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); + } else { + const isComposable = this.#elements.length > 1; + this.#elements.forEach((element, index) => { + if (data.name === element.elementName) { + let emitEvent = ''; + switch (data.event) { + case ELEMENT_EVENTS_TO_CLIENT.FOCUS: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.FOCUS; + break; + case ELEMENT_EVENTS_TO_CLIENT.BLUR: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.BLUR; + break; + case ELEMENT_EVENTS_TO_CLIENT.CHANGE: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.CHANGE; + break; + case ELEMENT_EVENTS_TO_CLIENT.READY: + emitEvent = ELEMENT_EVENTS_TO_CLIENT.READY; + break; + case ELEMENT_EVENTS_TO_CLIENT.SUBMIT: + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.SUBMIT); + return; + // case ELEMENT_EVENTS_TO_CLIENT.CREATED: + // this.#mounted = true; + // return; + // todo: need to implement the below events + // case ELEMENT_EVENTS_TO_CLIENT.ESCAPE: + // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ESCAPE); + // break; + // case ELEMENT_EVENTS_TO_CLIENT.CLICK: + // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.CLICK); + // break; + // case ELEMENT_EVENTS_TO_CLIENT.ERROR: + // this.eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.ERROR); + // break; + + default: + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_EVENT_TYPE, [], true); + } + this.#states[index].isEmpty = data.value.isEmpty; + this.#states[index].isValid = data.value.isValid; + this.#states[index].isComplete = data.value.isComplete; + this.#states[index].isFocused = data.value.isFocused; + this.#states[index].isRequired = data.value.isRequired; + this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; + + if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; + else this.#states[index].value = undefined; + + emitEvent = isComposable ? `${emitEvent}:${data.name}` : emitEvent; + this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + + this.#updateState(); + const emitData = { + ...this.#states[index], + elementType: element.elementType, + }; + if (isComposable) { + this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, { + iframeName: this.#iframe.name, + }); + } + if (isComposable && this.#groupEmitter) { + this.#groupEmitter._emit(emitEvent, emitData); + } else { + this.#eventEmitter._emit(emitEvent, emitData); + } + } + }); + } + }); + } }); }; diff --git a/src/core/external/collect/compose-collect-container.ts b/src/core/external/collect/compose-collect-container.ts index c3f7d0ef..d63ab17e 100644 --- a/src/core/external/collect/compose-collect-container.ts +++ b/src/core/external/collect/compose-collect-container.ts @@ -24,6 +24,7 @@ import { InputStyles, ErrorTextStyles, ContainerOptions, + UploadFilesResponse, } from '../../../utils/common'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import logs from '../../../utils/logs'; @@ -43,6 +44,8 @@ import CollectElement from './collect-element'; import ComposableElement from './compose-collect-element'; import { ElementGroup, ElementGroupItem } from './collect-container'; import { Metadata, SkyflowElementProps } from '../../internal/internal-types'; +import Client from '../../../client'; +import { getAccessToken } from '../../../utils/bus-events'; export interface ComposableElementGroup extends ElementGroup { styles: InputStyles; @@ -81,7 +84,13 @@ class ComposableContainer extends Container { #clientDomain: string = ''; - #isSkyflowFrameReady: boolean = false; + #isComposableFrameReady: boolean = false; + + #shadowRoot: ShadowRoot | null = null; + + #iframeID: string = ''; + + #getSkyflowBearerToken: () => Promise | undefined; constructor( metaData: Metadata, @@ -104,8 +113,7 @@ class ComposableContainer extends Container { }, }, }; - this.#isSkyflowFrameReady = metaData.skyflowContainer.isControllerFrameReady; - + this.#getSkyflowBearerToken = metaData.getSkyflowBearerToken; this.#skyflowElements = skyflowElements; this.#context = context; this.#options = options; @@ -125,6 +133,18 @@ class ComposableContainer extends Container { this.#context.logLevel); this.#containerMounted = true; this.#updateListeners(); + bus + // .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.#containerId, (data, callback) => { + printLog(parameterizedString(logs.infoLogs.INITIALIZE_COMPOSABLE_CLIENT, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + callback({ + client: this.#metaData.clientJSON, + context, + }); + this.#isComposableFrameReady = true; + }); } create = (input: CollectElementInput, options: CollectElementOptions = { @@ -150,7 +170,11 @@ class ComposableContainer extends Container { elementName, }); const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}:${this.#containerId}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`; - return new ComposableElement(elementName, this.#eventEmitter, controllerIframeName); + this.#iframeID = controllerIframeName; + return new ComposableElement( + elementName, this.#eventEmitter, controllerIframeName, + { ...this.#metaData, type: input.type }, + ); }; #createMultipleElement = ( @@ -316,156 +340,228 @@ class ComposableContainer extends Container { this.#containerElement.mount(domElement); this.#isMounted = true; } + this.#elementsList.forEach((element) => { + this.#eventEmitter.on(`${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${element.elementName}`, (data, callback) => { + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${element.elementName}`, + { + elementName: element.name, + data: { + type: COLLECT_TYPES.FILE_UPLOAD, + containerId: this.#containerId, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + options: { + ...data.options, + }, + }, + ); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + callback(err); + }); + }); + }); + if (domElement instanceof HTMLElement + && (domElement as HTMLElement).getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = domElement.getRootNode() as ShadowRoot; + } else if (typeof domElement === 'string') { + const element = document.getElementById(domElement); + if (element && element.getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = element.getRootNode() as ShadowRoot; + } + } + if (this.#shadowRoot !== null) { + this.#eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, (data) => { + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + data.iframeName, {}); + }); + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframeID, {}); + } }; unmount = () => { this.#containerElement.unmount(); }; - collect = (options: ICollectOptions = { tokens: true }): Promise => { - this.#isSkyflowFrameReady = this.#metaData.skyflowContainer.isControllerFrameReady; - if (this.#isSkyflowFrameReady) { - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (!this.#elementsList || this.#elementsList.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); - } - if (!this.#isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); - } - const containerElements = getElements(this.#tempElements); - containerElements.forEach((element:any) => { - if (!element?.isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } - }); - const elementIds:{ frameId:string, elementId:string }[] = []; - const collectElements = Object.values(this.#elements); - collectElements.forEach((element) => { - element.isValidElement(); - }); - if (options && options.tokens && typeof options.tokens !== 'boolean') { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); - } - if (options?.additionalFields) { - validateAdditionalFieldsInCollect(options.additionalFields); - } - if (options?.upsert) { - validateUpsertOptions(options?.upsert); + collect = (options: ICollectOptions = { tokens: true }) : + Promise => new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + if (!this.#isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); + } + const containerElements = getElements(this.#tempElements); + containerElements.forEach((element:any) => { + if (!element?.isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); + } + }); + const elementIds:{ frameId:string, elementId:string }[] = []; + const collectElements = Object.values(this.#elements); + collectElements.forEach((element) => { + element.isValidElement(); + }); + if (options && options.tokens && typeof options.tokens !== 'boolean') { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); + } + if (options?.additionalFields) { + validateAdditionalFieldsInCollect(options.additionalFields); + } + if (options?.upsert) { + validateUpsertOptions(options?.upsert); + } + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: this.#tempElements.elementName, + elementId: element.elementName ?? '', + }); + }); + const client = Client.fromJSON(this.#metaData.clientJSON) as any; + const clientId = client.toJSON()?.metaData?.uuid || ''; + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + this.#containerId, { + data: { + type: COLLECT_TYPES.COLLECT, + ...options, + tokens: options?.tokens !== undefined ? options.tokens : true, + elementIds, + containerId: this.#containerId, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + window.addEventListener('message', (event) => { + if (event.data?.type + === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.#containerId) { + const data = event.data.data; + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); + reject(data?.error); + } else if (data?.records) { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + resolve(data); + } else { + printLog(`${JSON.stringify(data)}`, MessageType.ERROR, this.#context.logLevel); + reject(data); } - this.#elementsList.forEach((element) => { - elementIds.push({ - frameId: this.#tempElements.elementName, - elementId: element.elementName || '', - }); - }); - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#metaData.uuid, - { - type: COLLECT_TYPES.COLLECT, - ...options, - tokens: options?.tokens !== undefined ? options.tokens : true, - elementIds, - containerId: this.#containerId, - }, - (data: any) => { - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - - resolve(data); - } - }, - ); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), - MessageType.LOG, this.#context.logLevel); - } catch (err:any) { - printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); - reject(err); } }); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), + MessageType.LOG, this.#context.logLevel); + } catch (err:any) { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); } - return new Promise((resolve, reject) => { - try { - validateInitConfig(this.#metaData.clientJSON.config); - if (!this.#elementsList || this.#elementsList.length === 0) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); - } - if (!this.#isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); - } + }); + + #emitEvent = (eventName: string, options?: Record, callback?: any) => { + if (this.#shadowRoot) { + const iframe = this.#shadowRoot.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } else { + const iframe = document.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }; - const containerElements = getElements(this.#tempElements); - containerElements.forEach((element:any) => { - if (!element?.isMounted) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.ELEMENTS_NOT_MOUNTED, [], true); - } + uploadFiles = (options: ICollectOptions): + Promise => new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + if (!this.#isMounted) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.COMPOSABLE_CONTAINER_NOT_MOUNTED, [], true); + } + const elementIds:{ frameId:string, elementId:string }[] = []; + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: this.#tempElements.elementName, + elementId: element.elementName ?? '', }); - const elementIds:{ frameId:string, elementId:string }[] = []; - const collectElements = Object.values(this.#elements); - collectElements.forEach((element) => { - element.isValidElement(); + }); + const client = Client.fromJSON(this.#metaData.clientJSON) as any; + const clientId = client.toJSON()?.metaData?.uuid || ''; + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + this.#containerId, { + data: { + type: COLLECT_TYPES.FILE_UPLOAD, + ...options, + // tokens: options?.tokens !== undefined ? options.tokens : true, + elementIds, + containerId: this.#containerId, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, }); - - if (options && options.tokens && typeof options.tokens !== 'boolean') { - throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_TOKENS_IN_COLLECT, [], true); - } - if (options?.additionalFields) { - validateAdditionalFieldsInCollect(options.additionalFields); - } - if (options?.upsert) { - validateUpsertOptions(options?.upsert); - } - this.#elementsList.forEach((element) => { - elementIds.push({ - frameId: this.#tempElements.elementName, - elementId: element.elementName || '', - }); + window.addEventListener('message', (event) => { + if (event.data?.type + === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.#containerId) { + const data = event.data.data; + if (!data || data?.error) { + printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); + reject(data?.error); + } else if (data?.fileUploadResponse) { + printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + resolve(data); + } else { + printLog(`${JSON.stringify(data)}`, MessageType.ERROR, this.#context.logLevel); + reject(data); + } + } }); - bus - .target(properties.IFRAME_SECURE_ORIGIN) - .on(ELEMENT_EVENTS_TO_IFRAME.SKYFLOW_FRAME_CONTROLLER_READY + this.#containerId, () => { - bus - // .target(properties.IFRAME_SECURE_ORIGIN) - .emit( - ELEMENT_EVENTS_TO_IFRAME.COLLECT_CALL_REQUESTS + this.#metaData.uuid, - { - type: COLLECT_TYPES.COLLECT, - ...options, - tokens: options?.tokens !== undefined ? options.tokens : true, - elementIds, - containerId: this.#containerId, - }, - (data: any) => { - if (!data || data?.error) { - printLog(`${JSON.stringify(data?.error)}`, MessageType.ERROR, this.#context.logLevel); - reject(data?.error); - } else { - printLog(parameterizedString(logs.infoLogs.COLLECT_SUBMIT_SUCCESS, CLASS_NAME), - MessageType.LOG, - this.#context.logLevel); - resolve(data); - } - }, - ); - }); - printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, - CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.TOKENIZATION_REQUEST), - MessageType.LOG, this.#context.logLevel); - } catch (err:any) { + }).catch((err:any) => { printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); reject(err); - } - }); - }; + }); + } catch (err:any) { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + } + }); #updateListeners = () => { this.#eventEmitter.on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_UPDATE_OPTIONS, (data) => { diff --git a/src/core/external/collect/compose-collect-element.ts b/src/core/external/collect/compose-collect-element.ts index b3e7b7c3..e0696be3 100644 --- a/src/core/external/collect/compose-collect-element.ts +++ b/src/core/external/collect/compose-collect-element.ts @@ -1,10 +1,15 @@ +import { Context } from 'vm'; import EventEmitter from '../../../event-emitter'; import { formatValidations } from '../../../libs/element-options'; import SkyflowError from '../../../libs/skyflow-error'; import { ContainerType } from '../../../skyflow'; -import { CollectElementUpdateOptions, EventName } from '../../../utils/common'; +import { + CollectElementUpdateOptions, EventName, MessageType, MetaData, +} from '../../../utils/common'; import SKYFLOW_ERROR_CODE from '../../../utils/constants'; import { ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ElementType } from '../../constants'; +import { printLog } from '../../../utils/logs-helper'; +import logs from '../../../utils/logs'; class ComposableElement { #elementName: string; @@ -19,7 +24,13 @@ class ComposableElement { #isUpdateCalled = false; - constructor(name: string, eventEmitter: EventEmitter, iframeName: string) { + #metaData: any; + + #context: Context; + + #elementType: ElementType; + + constructor(name, eventEmitter, iframeName, metaData) { this.#elementName = name; this.#iframeName = iframeName; this.#eventEmitter = eventEmitter; @@ -27,6 +38,12 @@ class ComposableElement { this.#eventEmitter.on(`${EventName.READY}:${this.#elementName}`, () => { this.#isMounted = true; }); + this.#metaData = metaData; + this.#context = { + logLevel: this.#metaData.clientJSON?.config?.options?.logLevel, + env: this.#metaData.clientJSON?.config?.options?.env, + }; + this.#elementType = this.#metaData?.type as ElementType; } on(eventName: string, handler: Function) { @@ -98,6 +115,40 @@ class ComposableElement { }); } }; -} + uploadMultipleFiles = (metaData?: MetaData) => new Promise((resolve, reject) => { + try { + if (this.#elementType !== ElementType.MULTI_FILE_INPUT) { + throw new SkyflowError( + SKYFLOW_ERROR_CODE.MULTI_FILE_NOT_SUPPORTED, + [], + true, + ); + } + // eslint-disable-next-line no-underscore-dangle + this.#eventEmitter._emit(`${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${this.#elementName}`, { + options: metaData, + }, (response: any) => { + if (response.error) { + reject(response); + } + }); + window.addEventListener('message', (event) => { + if (event?.data?.type === `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${this.#elementName}`) { + if (event?.data?.data?.errorResponse || event?.data?.data?.error) { + printLog(`${event?.data?.data.errorResponse || event?.data?.data.error}`, MessageType.ERROR, this.#context.logLevel); + reject(event?.data?.data); + } else { + printLog(logs.infoLogs.MULTI_UPLOAD_FILES_SUCCESS, + MessageType.LOG, this.#context.logLevel); + resolve(event?.data?.data); + } + } + }); + } catch (error) { + printLog(`${error}`, MessageType.ERROR, this.#context.logLevel); + reject(error); + } + }); +} export default ComposableElement; diff --git a/src/core/external/reveal/composable-reveal-container.ts b/src/core/external/reveal/composable-reveal-container.ts new file mode 100644 index 00000000..2d829848 --- /dev/null +++ b/src/core/external/reveal/composable-reveal-container.ts @@ -0,0 +1,474 @@ +/* eslint-disable no-plusplus */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* +Copyright (c) 2023 Skyflow, Inc. +*/ +import bus from 'framebus'; +import sum from 'lodash/sum'; +import EventEmitter from '../../../event-emitter'; +import iframer, { setAttributes, getIframeSrc, setStyles } from '../../../iframe-libs/iframer'; +import deepClone from '../../../libs/deep-clone'; +import SkyflowError from '../../../libs/skyflow-error'; +import uuid from '../../../libs/uuid'; +import properties from '../../../properties'; +import { ContainerType } from '../../../skyflow'; +import { + Context, MessageType, +} from '../../../utils/common'; +import SKYFLOW_ERROR_CODE from '../../../utils/constants'; +import logs from '../../../utils/logs'; +import { printLog, parameterizedString } from '../../../utils/logs-helper'; +import { + validateInitConfig, + validateInputFormatOptions, + validateRevealElementRecords, +} from '../../../utils/validators'; +import { + COLLECT_FRAME_CONTROLLER, + CONTROLLER_STYLES, ELEMENT_EVENTS_TO_IFRAME, + FRAME_ELEMENT, ELEMENT_EVENTS_TO_CLIENT, + COMPOSABLE_REVEAL, + REVEAL_TYPES, +} from '../../constants'; +import Container from '../common/container'; + +import ComposableRevealElement from './composable-reveal-element'; +import { RevealElementInput, RevealResponse } from '../../../index-node'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; +import ComposableRevealInternalElement from './composable-reveal-internal'; +import { formatRevealElementOptions } from '../../../utils/helpers'; + +const CLASS_NAME = 'ComposableRevealContainer'; +class ComposableRevealContainer extends Container { + #containerId: string; + + #elements: Record = {}; + + #metaData: any; + + #elementGroup: any = { rows: [] }; + + #elementsList:any = []; + + #context:Context; + + #skyflowElements:any; + + #eventEmitter: EventEmitter; + + #isMounted: boolean = false; + + #options: any; + + #containerElement:any; + + type:string = ContainerType.COMPOSE_REVEAL; + + #containerMounted: boolean = false; + + #tempElements: any = {}; + + #clientDomain: string = ''; + + #isComposableFrameReady: boolean = false; + + #shadowRoot: ShadowRoot | null = null; + + #iframeID: string = ''; + + #revealRecords: IRevealElementInput[] = []; + + #getSkyflowBearerToken: () => Promise | undefined; + + constructor(options, metaData, skyflowElements, context) { + super(); + this.#containerId = uuid(); + this.#metaData = { + ...metaData, + clientJSON: { + ...metaData.clientJSON, + config: { + ...metaData.clientJSON.config, + options: { + ...metaData.clientJSON.config?.options, + ...options, + }, + }, + }, + }; + this.#getSkyflowBearerToken = metaData.getSkyflowBearerToken; + this.#skyflowElements = skyflowElements; + this.#context = context; + this.#options = options; + this.#eventEmitter = new EventEmitter(); + + this.#clientDomain = this.#metaData.clientDomain || ''; + const iframe = iframer({ + name: `${COLLECT_FRAME_CONTROLLER}:${this.#containerId}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`, + referrer: this.#clientDomain, + }); + setAttributes(iframe, { + src: getIframeSrc(), + }); + setStyles(iframe, { ...CONTROLLER_STYLES }); + printLog(parameterizedString(logs.infoLogs.CREATE_COLLECT_CONTAINER, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#containerMounted = true; + bus + // .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.#containerId, (data, callback) => { + printLog(parameterizedString(logs.infoLogs.INITIALIZE_COMPOSABLE_CLIENT, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + callback({ + client: this.#metaData.clientJSON, + context, + }); + this.#isComposableFrameReady = true; + }); + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + this.#containerId) { + this.#isComposableFrameReady = true; + } + }); + } + + create = (input: RevealElementInput, options?: IRevealElementOptions) => { + const elementId = uuid(); + validateInputFormatOptions(options); + + const elementName = `${COMPOSABLE_REVEAL}:${btoa(elementId)}`; + this.#elementsList?.push({ + name: elementName, + ...input, + elementName, + elementId, + ...formatRevealElementOptions(options ?? {}), + }); + const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements ?? {})}:${this.#containerId}:${this.#context?.logLevel}:${btoa(this.#clientDomain ?? '')}`; + return new ComposableRevealElement(elementName, + this.#eventEmitter, + controllerIframeName); + }; + + #createMultipleElement = ( + multipleElements: any, + isSingleElementAPI: boolean = false, + ) => { + try { + const elements: any[] = []; + this.#tempElements = deepClone(multipleElements); + this.#tempElements?.rows?.forEach((row) => { + row?.elements?.forEach((element) => { + const options = element ?? {}; + const { elementType } = options; + options.isMounted = false; + + options.label = element?.label; + options.skyflowID = element?.skyflowID; + + elements.push(options); + }); + }); + + this.#tempElements.elementName = isSingleElementAPI + ? elements[0].elementName + : `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}`; + if ( + isSingleElementAPI + && !this.#elements[elements[0].elementName] + && this.#hasElementName(elements[0].name) + ) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0].name}`], true); + } + + let element = this.#elements[this.#tempElements.elementName]; + if (element) { + if (isSingleElementAPI) { + element.update(elements[0]); + } else { + element.update(this.#tempElements); + } + } else { + const elementId = uuid(); + try { + element = new ComposableRevealInternalElement( + elementId, + this.#tempElements, + this.#metaData, + { + containerId: this.#containerId, + isMounted: this.#containerMounted, + type: this.type, + eventEmitter: this.#eventEmitter, + }, + true, + this.#context, + ); + this.#elements[this.#tempElements.elementName] = element; + this.#skyflowElements[elementId] = element; + } catch (error: any) { + printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, + MessageType.ERROR, + this.#context.logLevel); + throw error; + } + } + this.#iframeID = element.iframeName(); + return element; + } catch (error: any) { + printLog(logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT, + MessageType.ERROR, + this.#context.logLevel); + throw error; + } + }; + + #hasElementName = (name: string) => { + const tempElements = Object.keys(this.#elements); + for (let i = 0; i < tempElements.length; i += 1) { + if (atob(tempElements[i].split(':')[2]) === name) { + return true; + } + } + return false; + }; + + mount = (domElement: HTMLElement | string) => { + if (!domElement) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, + ['RevealElement'], true); + } + + const { layout } = this.#options; + if (sum(layout) !== this.#elementsList.length) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.MISMATCH_ELEMENT_COUNT_LAYOUT_SUM, [], true); + } + let count = 0; + layout.forEach((rowCount, index) => { + this.#elementGroup.rows = [ + ...this.#elementGroup.rows, + { elements: [] }, + ]; + for (let i = 0; i < rowCount; i++) { + this.#elementGroup.rows[index].elements.push( + this.#elementsList[count], + ); + count++; + } + }); + if (this.#options.styles) { + this.#elementGroup.styles = { + ...this.#options.styles, + }; + } + if (this.#options.errorTextStyles) { + this.#elementGroup.errorTextStyles = { + ...this.#options.errorTextStyles, + }; + } + if (this.#containerMounted) { + this.#containerElement = this.#createMultipleElement(this.#elementGroup, false); + this.#containerElement.mount(domElement); + this.#isMounted = true; + } + if (domElement instanceof HTMLElement + && (domElement as HTMLElement).getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = domElement.getRootNode() as ShadowRoot; + } else if (typeof domElement === 'string') { + const element = document.getElementById(domElement); + if (element && element.getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = element.getRootNode() as ShadowRoot; + } + } + if (this.#shadowRoot !== null) { + this.#eventEmitter.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, (data) => { + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + data.iframeName, {}); + }); + this.#emitEvent(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframeID, {}); + } + }; + + unmount = () => { + this.#containerElement.unmount(); + }; + + #emitEvent = (eventName: string, options?: Record, callback?: any) => { + if (this.#shadowRoot) { + const iframe = this.#shadowRoot.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } else { + const iframe = document.getElementById(this.#iframeID) as HTMLIFrameElement; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }; + + reveal(): Promise { + this.#revealRecords = []; + if (this.#isComposableFrameReady) { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + printLog(parameterizedString(logs.infoLogs.VALIDATE_REVEAL_RECORDS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#elementsList.forEach((currentElement) => { + // if (currentElement.isClientSetError()) { + // throw new SkyflowError(SKYFLOW_ERROR_CODE.REVEAL_ELEMENT_ERROR_STATE); + // } + if (!currentElement.skyflowID) { + this.#revealRecords.push(currentElement); + } + }); + validateRevealElementRecords(this.#revealRecords); + const elementIds:{ frameId:string, token:string }[] = []; + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: element.name, + token: element.token, + }); + }); + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + this.#containerId, + { + data: { + type: REVEAL_TYPES.REVEAL, + containerId: this.#containerId, + elementIds, + }, + clientConfig: { + vaultURL: this.#metaData?.clientJSON?.config?.vaultURL, + vaultID: this.#metaData?.clientJSON?.config?.vaultID, + authToken, + }, + context: this.#context, + }, + ); + + window?.addEventListener('message', (event) => { + if (event?.data?.type + === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.#containerId) { + const revealData = event?.data?.data; + if (revealData?.errors) { + printLog( + parameterizedString(logs?.errorLogs?.FAILED_REVEAL), + MessageType.ERROR, + this.#context?.logLevel, + ); + reject(revealData); + } else { + printLog( + parameterizedString(logs?.infoLogs?.REVEAL_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context?.logLevel, + ); + resolve(revealData); + } + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + } + }); + } + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + if (!this.#elementsList || this.#elementsList.length === 0) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.NO_ELEMENTS_IN_COMPOSABLE, [], true); + } + printLog(parameterizedString(logs.infoLogs.VALIDATE_REVEAL_RECORDS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#elementsList.forEach((currentElement) => { + // if (currentElement.isClientSetError()) { + // throw new SkyflowError(SKYFLOW_ERROR_CODE.REVEAL_ELEMENT_ERROR_STATE); + // } + if (!currentElement.skyflowID) { + this.#revealRecords.push(currentElement); + } + }); + validateRevealElementRecords(this.#revealRecords); + const elementIds:{ frameId:string, token:string }[] = []; + this.#elementsList.forEach((element) => { + elementIds.push({ + frameId: element.name, + token: element.token, + }); + }); + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + window.addEventListener('message', (messagEevent) => { + if (messagEevent.data.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + + this.#containerId) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + this.#containerId, { + data: { + type: REVEAL_TYPES.REVEAL, + containerId: this.#containerId, + elementIds, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + context: this.#context, + }, + ); + window.addEventListener('message', (event) => { + if (event.data.type + === ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.#containerId) { + const revealData = event.data.data; + if (revealData.errors) { + printLog(parameterizedString(logs.errorLogs.FAILED_REVEAL), + MessageType.ERROR, this.#context.logLevel); + reject(revealData); + } else { + printLog(parameterizedString(logs.infoLogs.REVEAL_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + resolve(revealData); + } + } + }); + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + } + }); + } +} +export default ComposableRevealContainer; diff --git a/src/core/external/reveal/composable-reveal-element.ts b/src/core/external/reveal/composable-reveal-element.ts new file mode 100644 index 00000000..0f3f328d --- /dev/null +++ b/src/core/external/reveal/composable-reveal-element.ts @@ -0,0 +1,66 @@ +import EventEmitter from '../../../event-emitter'; +import { ContainerType } from '../../../skyflow'; +import { EventName, RenderFileResponse } from '../../../utils/common'; +import { ELEMENT_EVENTS_TO_IFRAME, REVEAL_ELEMENT_OPTIONS_TYPES } from '../../constants'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; + +class ComposableRevealElement { + #elementName: string; + + #eventEmitter: EventEmitter; + + #iframeName: string; + + type: string = ContainerType?.COMPOSABLE ?? 'COMPOSABLE'; + + #isMounted: boolean = false; + + constructor(name, eventEmitter, iframeName) { + this.#elementName = name ?? ''; + this.#iframeName = iframeName ?? ''; + this.#eventEmitter = eventEmitter; + this.#eventEmitter?.on?.(`${EventName?.READY ?? 'READY'}:${this.#elementName}`, () => { + this.#isMounted = true; + }); + } + + iframeName(): string { + return this.#iframeName ?? ''; + } + + getID(): string { + return this.#elementName ?? ''; + } + + renderFile(): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line no-underscore-dangle + this.#eventEmitter?._emit?.( + `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST ?? ''}:${this.#elementName}`, + {}, + (response) => { + if (response?.errors) { + reject(response); + } else if (response?.error) { + reject({ errors: response?.error }); + } else { + resolve(response); + } + }, + ); + }); + } + + update = (options: IRevealElementInput | IRevealElementOptions) => { + // eslint-disable-next-line no-underscore-dangle + this.#eventEmitter?._emit?.( + `${ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS}:${this.#elementName}`, + { + options: options as IRevealElementInput | IRevealElementOptions, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + }, + ); + }; +} + +export default ComposableRevealElement; diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts new file mode 100644 index 00000000..a5584755 --- /dev/null +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -0,0 +1,610 @@ +/* +Copyright (c) 2022 Skyflow, Inc. +*/ +import bus from 'framebus'; +import SkyflowError from '../../../libs/skyflow-error'; +import uuid from '../../../libs/uuid'; +import { Context, MessageType, RenderFileResponse } from '../../../utils/common'; +import SKYFLOW_ERROR_CODE from '../../../utils/constants'; +import { + ELEMENT_EVENTS_TO_IFRAME, + ELEMENT_EVENTS_TO_CONTAINER, + REVEAL_ELEMENT_OPTIONS_TYPES, + METRIC_TYPES, + ELEMENT_EVENTS_TO_CLIENT, + EVENT_TYPES, + REVEAL_TYPES, + COMPOSABLE_REVEAL, +} from '../../constants'; +import IFrame from '../common/iframe'; +import SkyflowElement from '../common/skyflow-element'; +import { IRevealElementInput, IRevealElementOptions } from './reveal-container'; +import { + pushElementEventWithTimeout, + updateMetricObjectValue, +} from '../../../metrics'; +import logs from '../../../utils/logs'; +import { parameterizedString, printLog } from '../../../utils/logs-helper'; +import properties from '../../../properties'; +import { validateInitConfig, validateRenderElementRecord } from '../../../utils/validators'; +import EventEmitter from '../../../event-emitter'; +import { formatRevealElementOptions } from '../../../utils/helpers'; + +const CLASS_NAME = 'RevealElementInteranalElement'; + +export interface RevealComposableGroup{ + record: IRevealElementInput + options: IRevealElementOptions +} + +class ComposableRevealInternalElement extends SkyflowElement { + #iframe: IFrame; + + #metaData: any; + + #recordData: any; + + #containerId: any; + + #isMounted:boolean = false; + + #isClientSetError:boolean = false; + + #context: Context; + + #elementId: string; + + #readyToMount: boolean = false; + + #eventEmitter: EventEmitter; + + #isFrameReady: boolean; + + #domSelecter: string; + + #clientId: string; + + #isSkyflowFrameReady: boolean = false; + + #isSingleElementAPI: boolean; + + #shadowRoot: ShadowRoot | null = null; + + #getSkyflowBearerToken: () => Promise | undefined; + + #composableIframeName!: string; + + #isComposableFrameReady: boolean = false; + + constructor(elementId: string, + recordGroup: RevealComposableGroup[], + metaData: any, container: any, isSingleElementAPI: boolean = false, + context: Context) { + super(); + this.#elementId = elementId; + this.#metaData = metaData; + this.#clientId = this.#metaData?.uuid; + this.#isSingleElementAPI = isSingleElementAPI; + this.#recordData = recordGroup; + this.#containerId = container?.containerId; + this.#readyToMount = container?.isMounted ?? true; + this.#eventEmitter = container?.eventEmitter; + this.#context = context; + + this.#iframe = new IFrame( + `${COMPOSABLE_REVEAL}:${btoa(uuid())}`, + metaData, + this.#containerId, + this.#context?.logLevel, + ); + + this.#domSelecter = ''; + this.#isFrameReady = false; + this.#readyToMount = true; + this.#getSkyflowBearerToken = metaData?.getSkyflowBearerToken; + this.#isSkyflowFrameReady = metaData?.skyflowContainer?.isControllerFrameReady ?? false; + + bus?.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe?.name, (data) => { + this.#iframe?.setIframeHeight(data?.height); + }); + + window?.addEventListener('message', (event) => { + if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + this.#containerId) { + this.#isComposableFrameReady = true; + } + }); + + window?.addEventListener('message', (event) => { + if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + this.#iframe?.name) { + this.#iframe?.setIframeHeight(event?.data?.data?.height); + } + }); + + // eslint-disable-next-line max-len + if (this.#recordData?.rows) { + this.setupRenderFileEventListener(this.getRecordData()?.rows); + } + } + + private setupRenderFileEventListener(rows: any[]): void { + if (!rows?.length) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_REVEAL_COMPOSABLE_INPUT, ['COMPOSABLE_REVEAL'], true); + } + + try { + rows?.forEach((row, rowIndex) => { + row?.elements?.forEach((element: any, elementIndex: number) => { + if (!element?.name) return; + this.#eventEmitter?.on( + `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST}:${element?.name}`, + (data, callback) => { + this.renderFile(element)?.then((response) => { + callback?.(response); + })?.catch((error) => { + callback?.({ error }); + }); + }, + ); + this.#eventEmitter?.on( + `${ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS}:${element?.name}`, + (data) => { + if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS) { + // make this change in original elememt that is inside rows + const updatedElement = { + ...element, + ...data.options, + ...formatRevealElementOptions(data.options as IRevealElementOptions), + }; + + // Update element in this.#recordData.rows structure + if (this.#recordData?.rows?.[rowIndex]?.elements?.[elementIndex]) { + this.#recordData.rows[rowIndex].elements[elementIndex] = updatedElement; + } + + // Update local element reference + element = updatedElement; + + // Call update method + this.update(data.options, element); + } + }, + ); + }); + }); + } catch (error) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_REVEAL_COMPOSABLE_INPUT, ['COMPOSABLE_REVEAL'], true); + } + } + + getID() { + return this.#elementId; + } + + mount(domElementSelector: HTMLElement | string) { + if (!domElementSelector) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['RevealElement'], true); + } + updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElementSelector); + if ( + this.#metaData?.clientJSON?.config?.options?.trackMetrics + && this.#metaData.clientJSON.config?.options?.trackingKey + ) { + pushElementEventWithTimeout(this.#elementId); + } + + this.#readyToMount = true; + if (this.#readyToMount) { + this.#iframe.mount(domElementSelector, undefined, { + record: JSON.stringify({ + ...this.#metaData, + record: this.#recordData, + context: this.#context, + containerId: this.#containerId, + }), + }); + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + if (this.#recordData.skyflowID) { + bus + // .target(location.origin) + .emit( + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.#containerId, + { + skyflowID: this.#recordData.skyflowID, + containerId: this.#containerId, + }, + ); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.MOUNTED); + } else { + bus + // .target(location.origin) + .emit( + ELEMENT_EVENTS_TO_CONTAINER.ELEMENT_MOUNTED + this.#containerId, + { + id: this.#recordData.token, + containerId: this.#containerId, + }, + ); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_END_TIME, Date.now()); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.MOUNTED); + } + if (Object.prototype.hasOwnProperty.call(this.#recordData, 'skyflowID')) { + bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + } + }); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.EVENTS_KEY, EVENT_TYPES.READY); + updateMetricObjectValue(this.#elementId, METRIC_TYPES.MOUNT_START_TIME, Date.now()); + } + if (domElementSelector instanceof HTMLElement + && (domElementSelector as HTMLElement).getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = domElementSelector.getRootNode() as ShadowRoot; + } else if (typeof domElementSelector === 'string') { + const element = document.getElementById(domElementSelector); + if (element && element.getRootNode() instanceof ShadowRoot) { + this.#shadowRoot = element.getRootNode() as ShadowRoot; + } + } + } + + #emitEvent = (eventName: string, options?: Record) => { + if (this.#shadowRoot) { + const iframe = this.#shadowRoot?.getElementById(this.#iframe?.name) as HTMLIFrameElement; + iframe?.contentWindow?.postMessage({ + name: eventName, + ...options, + }, properties?.IFRAME_SECURE_ORIGIN); + } else { + const iframe = document?.getElementById(this.#iframe?.name) as HTMLIFrameElement; + iframe?.contentWindow?.postMessage({ + name: eventName, + ...options, + }, properties?.IFRAME_SECURE_ORIGIN); + } + }; + + renderFile(recordData): Promise { + let altText = ''; + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + altText = recordData.altText; + } + this.setAltText('loading...'); + const loglevel = this.#context.logLevel; + if (this.#isComposableFrameReady) { + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), + MessageType.LOG, + loglevel); + validateRenderElementRecord(recordData); + + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + recordData.name, + { + data: { + type: REVEAL_TYPES.RENDER_FILE, + containerId: this.#containerId, + iframeName: recordData.name, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + }, + ); + window?.addEventListener('message', (event) => { + if (event?.data && event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + + recordData.name) { + if (event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + const revealData = event?.data?.data?.result; + if (revealData?.error || revealData?.errors) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, + this.#context.logLevel); + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + this.setAltText(altText); + } + reject(revealData?.error || revealData?.errors); + } else { + printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, + CLASS_NAME, recordData.skyflowID), + MessageType.LOG, this.#context.logLevel); + resolve(revealData); + } + } + } + }); + }).catch((err:any) => { + printLog(`${err.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), + MessageType.LOG, loglevel); + } catch (err: any) { + printLog(`Error: ${err.message}`, MessageType.ERROR, + loglevel); + reject(err); + } + }); + } + return new Promise((resolve, reject) => { + try { + validateInitConfig(this.#metaData.clientJSON.config); + printLog(parameterizedString(logs.infoLogs.VALIDATE_RENDER_RECORDS, CLASS_NAME), + MessageType.LOG, + loglevel); + validateRenderElementRecord(recordData); + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + this.#containerId) { + this.#isMounted = true; + this.#getSkyflowBearerToken()?.then((authToken) => { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + recordData.name, + { + data: { + type: REVEAL_TYPES.RENDER_FILE, + containerId: this.#containerId, + iframeName: recordData.name, + }, + clientConfig: { + vaultURL: this.#metaData.clientJSON.config.vaultURL, + vaultID: this.#metaData.clientJSON.config.vaultID, + authToken, + }, + }, + ); + window.addEventListener('message', (event1) => { + if (event1?.data + && event1?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + + this.#iframe.name) { + if (event1?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + const revealData = event1?.data?.data?.result; + if (revealData?.error) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, + this.#context.logLevel); + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + this.setAltText(altText); + } + reject(revealData); + } else { + // eslint-disable-next-line max-len + printLog(parameterizedString(logs.infoLogs.RENDER_SUBMIT_SUCCESS, CLASS_NAME), + MessageType.LOG, + this.#context.logLevel); + printLog(parameterizedString(logs.infoLogs.FILE_RENDERED, + CLASS_NAME, recordData.skyflowID), + MessageType.LOG, this.#context.logLevel); + resolve(revealData); + } + } + } + }); + }).catch((err:any) => { + printLog(`${err?.message}`, MessageType.ERROR, this.#context.logLevel); + reject(err); + }); + } + }); + printLog(parameterizedString(logs.infoLogs.EMIT_EVENT, + CLASS_NAME, ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_REQUEST), + MessageType.LOG, loglevel); + } catch (err: any) { + printLog(`Error: ${err?.message}`, MessageType.ERROR, + loglevel); + reject(err); + } + }); + } + + iframeName(): string { + return this.#iframe.name; + } + + isMounted():boolean { + return this.#isMounted; + } + + hasToken():boolean { + if (this.#recordData.token) return true; + return false; + } + + isClientSetError():boolean { + return this.#isClientSetError; + } + + getRecordData() { + return this.#recordData; + } + + setErrorOverride(clientErrorText: string) { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: true, + clientErrorText, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: true, + clientErrorText, + }); + }); + } + this.#isClientSetError = true; + } + + setError(clientErrorText:string) { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: true, + clientErrorText, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: true, + clientErrorText, + }); + }); + } + this.#isClientSetError = true; + } + + resetError() { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: false, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_SET_ERROR + this.#iframe.name, { + name: this.#iframe.name, + isTriggerError: false, + }); + }); + } + this.#isClientSetError = false; + } + + setAltText(altText:string) { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: altText, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: altText, + }); + }); + } + } + + clearAltText() { + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: null, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ALT_TEXT, + updatedValue: null, + }); + }); + } + } + + setToken(token:string) { + this.#recordData = { + ...this.#recordData, + token, + }; + if (this.#isMounted) { + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.TOKEN, + updatedValue: token, + }); + } else { + bus + .target(properties.IFRAME_SECURE_ORIGIN) + .on(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#iframe.name, () => { + this.#isMounted = true; + bus.emit(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#iframe.name, { + name: this.#iframe.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.TOKEN, + updatedValue: token, + }); + }); + } + } + + unmount() { + if (this.#recordData.skyflowID) { + this.#isMounted = false; + this.#iframe.container?.remove(); + } + this.#isMounted = false; + this.#iframe.unmount(); + } + + update(options: IRevealElementInput | IRevealElementOptions, record) { + if (this.#isComposableFrameReady) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: options, + }, + ); + } else { + window.addEventListener('message', (event) => { + if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + this.#containerId) { + this.#emitEvent( + ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + { + name: ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, + updateType: REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS, + updatedValue: options, + }, + ); + } + }); + } + } +} + +export default ComposableRevealInternalElement; diff --git a/src/core/internal/composable-frame-element-init.ts b/src/core/internal/composable-frame-element-init.ts new file mode 100644 index 00000000..51210e5a --- /dev/null +++ b/src/core/internal/composable-frame-element-init.ts @@ -0,0 +1,369 @@ +import injectStylesheet from 'inject-stylesheet'; +import bus from 'framebus'; +import { getValueAndItsUnit } from '../../libs/element-options'; +import { getFlexGridStyles } from '../../libs/styles'; +import { ContainerType } from '../../skyflow'; +import { + Context, IRevealRecordComposable, +} from '../../utils/common'; +import { + getContainerType, +} from '../../utils/helpers'; +import { + ALLOWED_MULTIPLE_FIELDS_STYLES, + ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ERROR_TEXT_STYLES, REVEAL_TYPES, STYLE_TYPE, +} from '../constants'; +import IFrameFormElement from './iframe-form'; +import getCssClassesFromJss, { generateCssWithoutClass } from '../../libs/jss-styles'; +import FrameElement from '.'; +import Client from '../../client'; +import RevealFrame from './reveal/reveal-frame'; +import { + fetchRecordsByTokenIdComposable, formatRecordsForClientComposable, +} from '../../core-utils/reveal'; + +export default class RevealComposableFrameElementInit { + iframeFormElement: IFrameFormElement | undefined; + + clientMetaData: any; + + #domForm: HTMLFormElement; + + frameElement!: FrameElement; + + private static frameEle?: any; + + containerId: string; + + group: any; + + frameList: FrameElement[] = []; + + iframeFormList: IFrameFormElement[] = []; + + #client!: Client; + + #context!: Context; + + revealFrameList: any[] = []; + + rootDiv: HTMLDivElement; + + constructor() { + this.containerId = ''; + this.#domForm = document?.createElement('form'); + this.#domForm.action = '#'; + this.#domForm.onsubmit = (event) => { + event?.preventDefault(); + }; + + this.rootDiv = document?.createElement('div'); + this.updateGroupData(); + this.createContainerDiv(this.group); + + window?.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_REVEAL + + this.containerId && event?.data?.data?.type === REVEAL_TYPES.REVEAL) { + this.#context = event?.data?.context; + const data = event?.data?.data ?? {}; + const elementIds = data?.elementIds ?? []; + const revealDataInput: IRevealRecordComposable[] = []; + this.#client = new Client(event?.data?.clientConfig ?? {}, {}); + + elementIds?.forEach((element) => { + this.revealFrameList?.forEach((revealFrame) => { + const data2 = revealFrame?.getData?.(); + if (data2?.name === element?.frameId) { + if (data2 && !data2?.skyflowID) { + const revealRecord: IRevealRecordComposable = { + token: data2?.token ?? '', + redaction: data2?.redaction, + iframeName: data2?.name ?? '', + }; + revealDataInput?.push(revealRecord); + } + } + }); + }); + + this.revealData(revealDataInput, this.containerId, event?.data?.clientConfig?.authToken) + ?.then((revealResponse: any) => { + if (revealResponse?.records?.length > 0) { + const formattedRecord = formatRecordsForClientComposable(revealResponse); + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.containerId, + data: formattedRecord, + }, + this.clientMetaData?.clientDomain, + ); + + revealResponse?.records?.forEach((record: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === record?.frameId) { + revealFrame?.responseUpdate?.(record); + } + }); + }); + } + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + }) + ?.catch((error) => { + const formattedRecord = formatRecordsForClientComposable(error); + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_RESPONSE_READY + this.containerId, + data: formattedRecord, + }, + this.clientMetaData?.clientDomain, + ); + + error?.records?.forEach((record: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === record?.frameId) { + revealFrame?.responseUpdate?.(record); + } + }); + }); + + error?.errors?.forEach((error1: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === error1?.frameId) { + revealFrame?.responseUpdate?.(error1); + } + }); + }); + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + }); + } + }); + + bus?.emit(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId, {}, (data: any) => { + this.#context = data?.context; + if (data?.client?.config) { + data.client.config = { + ...data?.client?.config, + }; + } + this.#client = Client?.fromJSON?.(data?.client); + }); + } + + updateGroupData = () => { + const url = window?.location?.href ?? ''; + const configIndex = url?.indexOf('?') ?? -1; + const encodedString = configIndex !== -1 ? decodeURIComponent(url?.substring(configIndex + 1)) : ''; + const parsedRecord = encodedString ? JSON.parse(atob(encodedString)) : {}; + this.clientMetaData = parsedRecord?.clientJSON?.metaData; + this.group = parsedRecord?.record; + this.containerId = parsedRecord?.containerId ?? ''; + this.#context = parsedRecord?.context; + }; + + static startFrameElement = () => { + RevealComposableFrameElementInit.frameEle = new RevealComposableFrameElementInit(); + }; + + revealData(revealRecords: IRevealRecordComposable[], containerId, authToken) { + return new Promise((resolve, reject) => { + fetchRecordsByTokenIdComposable(revealRecords, this.#client, authToken)?.then( + (resolvedResult) => { + resolve(resolvedResult); + }, + (rejectedResult) => { + reject(rejectedResult); + }, + ); + }); + } + + createContainerDiv = (newGroup) => { + this.group = newGroup; + const { + rows = [], + styles, + errorTextStyles, + } = this.group ?? {}; + + const isComposableContainer = getContainerType(window?.name) === ContainerType?.COMPOSABLE; + this.group.spacing = getValueAndItsUnit(this.group?.spacing)?.join('') ?? ''; + this.rootDiv = document?.createElement('div'); + this.rootDiv.className = 'container'; + + const containerStylesByClassName = getFlexGridStyles({ + 'align-items': this.group?.alignItems ?? 'stretch', + 'justify-content': this.group?.justifyContent ?? 'flex-start', + spacing: this.group?.spacing, + }); + + injectStylesheet?.injectWithAllowlist( + { + [`.${this.rootDiv?.className}`]: containerStylesByClassName, + }, + ALLOWED_MULTIPLE_FIELDS_STYLES, + ); + + let count = 0; + rows?.forEach((row, rowIndex) => { + row.spacing = getValueAndItsUnit(row?.spacing)?.join('') ?? ''; + const rowDiv = document?.createElement('div'); + rowDiv.id = `row-${rowIndex}`; + + const intialRowStyles = { + 'align-items': row?.alignItems ?? 'stretch', + 'justify-content': row?.justifyContent ?? 'flex-start', + spacing: row?.spacing, + padding: this.group?.spacing, + }; + + const rowStylesByClassName = getFlexGridStyles(intialRowStyles); + let errorTextElement; + + if (isComposableContainer) { + rowDiv.className = `${rowDiv?.id} SkyflowElement-${rowDiv?.id}-base`; + const rowStyles = { + [STYLE_TYPE?.BASE]: { + ...(styles?.[STYLE_TYPE?.BASE] ?? {}), + }, + }; + + getCssClassesFromJss?.(rowStyles, `${rowDiv?.id}`); + + errorTextElement = document?.createElement('span'); + errorTextElement.id = `${rowDiv?.id}-error`; + errorTextElement.className = 'SkyflowElement-row-error-base'; + + const errorStyles = { + [STYLE_TYPE?.BASE]: { + ...ERROR_TEXT_STYLES, + ...(errorTextStyles?.[STYLE_TYPE?.BASE] ?? {}), + }, + }; + + getCssClassesFromJss?.(errorStyles, 'row-error'); + if (errorTextStyles?.[STYLE_TYPE?.GLOBAL]) { + generateCssWithoutClass?.(errorTextStyles?.[STYLE_TYPE?.GLOBAL]); + } + } else { + rowDiv.className = `row-${rowIndex}`; + injectStylesheet?.injectWithAllowlist( + { + [`.${rowDiv?.className}`]: rowStylesByClassName, + }, + ALLOWED_MULTIPLE_FIELDS_STYLES, + ); + } + + row?.elements?.forEach((element) => { + const elementDiv = document?.createElement('div'); + elementDiv.className = `element-${count}`; + elementDiv.id = `${rowDiv?.id}:element-${count}`; + count += 1; + + const elementStylesByClassName = { + padding: row?.spacing, + }; + + injectStylesheet?.injectWithAllowlist( + { + [`.${elementDiv?.className}`]: elementStylesByClassName, + }, + ALLOWED_MULTIPLE_FIELDS_STYLES, + ); + + const revealFrame = new RevealFrame(element, this.#context, + this.containerId, elementDiv); + this.revealFrameList?.push(revealFrame); + rowDiv?.append(elementDiv); + }); + + this.rootDiv?.append(rowDiv); + if (isComposableContainer) { + this.rootDiv?.append(errorTextElement); + } + }); + + if (this.#domForm) { + this.#domForm.innerHTML = ''; + document.body.innerHTML = ''; + this.#domForm?.append(this.rootDiv); + document?.body?.append(this.#domForm); + } + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.containerId, + data: { + name: window?.name, + }, + }, + this.clientMetaData.clientDomain, + ); + + bus?.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window?.name, (data, callback) => { + callback?.({ + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }); + }); + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData.clientDomain, + ); + + window?.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window?.name) { + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData?.clientDomain, + ); + } + if (event?.data?.type + === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name) { + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window?.name, + data: { + height: this.rootDiv?.scrollHeight ?? 0, + name: window?.name, + }, + }, + this.clientMetaData.clientDomain, + ); + } + }); + }; +} diff --git a/src/core/internal/frame-element-init.ts b/src/core/internal/frame-element-init.ts index d2c35e6b..13c89945 100644 --- a/src/core/internal/frame-element-init.ts +++ b/src/core/internal/frame-element-init.ts @@ -1,17 +1,35 @@ import injectStylesheet from 'inject-stylesheet'; import bus from 'framebus'; +import get from 'lodash/get'; import { getValueAndItsUnit, validateAndSetupGroupOptions } from '../../libs/element-options'; import { getFlexGridStyles } from '../../libs/styles'; import { ContainerType } from '../../skyflow'; -import { Context, Env, LogLevel } from '../../utils/common'; -import { getContainerType } from '../../utils/helpers'; +import { + Context, Env, LogLevel, + MessageType, +} from '../../utils/common'; +import { + fileValidation, generateUploadFileName, getContainerType, vaildateFileName, +} from '../../utils/helpers'; import { ALLOWED_MULTIPLE_FIELDS_STYLES, - ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ERROR_TEXT_STYLES, STYLE_TYPE, + COLLECT_TYPES, + ELEMENT_EVENTS_TO_CLIENT, ELEMENT_EVENTS_TO_IFRAME, ELEMENTS, ERROR_TEXT_STYLES, STYLE_TYPE, } from '../constants'; import IFrameFormElement from './iframe-form'; import getCssClassesFromJss, { generateCssWithoutClass } from '../../libs/jss-styles'; import FrameElement from '.'; +import { + checkForElementMatchRule, checkForValueMatch, constructElementsInsertReq, + constructInsertRecordRequest, insertDataInCollect, + updateRecordsBySkyflowIDComposable, +} from '../../core-utils/collect'; +import SkyflowError from '../../libs/skyflow-error'; +import SKYFLOW_ERROR_CODE from '../../utils/constants'; +import Client from '../../client'; +import { printLog } from '../../utils/logs-helper'; + +const set = require('set-value'); export default class FrameElementInit { iframeFormElement: IFrameFormElement | undefined; @@ -30,9 +48,15 @@ export default class FrameElementInit { group: any; + frameList: FrameElement[] = []; + + iframeFormList: IFrameFormElement[] = []; + + #client!: Client; + constructor() { // this.createIframeElement(frameName, label, skyflowID, isRequired); - this.context = { logLevel: LogLevel.ERROR, env: Env.PROD }; // client level + this.context = { logLevel: LogLevel.INFO, env: Env.PROD }; // client level this.containerId = ''; this.#domForm = document.createElement('form'); this.#domForm.action = '#'; @@ -41,8 +65,571 @@ export default class FrameElementInit { }; this.updateGroupData(); this.createContainerDiv(this.group); + bus + // .target(this.clientMetaData.clientDomain) + .emit(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId, {}, (data: any) => { + data.client.config = { + ...data.client.config, + }; + this.#client = Client.fromJSON(data.client) as any; + }); + + window.addEventListener('message', this.handleCollectCall); } + private handleCollectCall = (event: MessageEvent) => { + this.iframeFormList.forEach((inputElement) => { + if (inputElement) { + if (inputElement.fieldType + === ELEMENTS.MULTI_FILE_INPUT.name) { + if (event?.data && event?.data?.name === `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES}:${inputElement.iFrameName}`) { + this.#client = Client.fromJSON(event?.data?.clientConfig); + this.multipleUploadFiles(inputElement, event?.data?.clientConfig, event?.data?.options) + ?.then((response: any) => { + window?.parent.postMessage({ + type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${inputElement.iFrameName}`, + data: response, + }, this.clientMetaData?.clientDomain); + }).catch((error) => { + window?.parent.postMessage({ + type: `${ELEMENT_EVENTS_TO_IFRAME.MULTIPLE_UPLOAD_FILES_RESPONSE}:${inputElement.iFrameName}`, + data: error, + }, this.clientMetaData?.clientDomain); + }); + } + } + } + }); + + // if (event.origin === this.clientMetaData.clientDomain) { + if (event.data && event.data.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_REQUESTS + + this.containerId) { + if (event.data.data && event.data.data.type === COLLECT_TYPES.COLLECT) { + this.tokenize(event.data.data, event.data.clientConfig) + .then((response: any) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.containerId, + data: response, + }, this.clientMetaData.clientDomain); + }) + .catch((error) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CALL_RESPONSE + this.containerId, + data: error, + }, this.clientMetaData.clientDomain); + }); + } else if (event.data.data && event.data.data.type === COLLECT_TYPES.FILE_UPLOAD) { + this.parallelUploadFiles(event.data.data, event.data.clientConfig) + .then((response: any) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId, + data: response, + }, this.clientMetaData.clientDomain); + }) + .catch((error) => { + window?.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_FILE_CALL_RESPONSE + this.containerId, + data: error, + }, this.clientMetaData.clientDomain); + }); + } + } + if (event.data.name === ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_CONTAINER + this.containerId) { + const data = event.data; + data.client.config = { + ...data.client.config, + }; + this.#client = Client.fromJSON(data.client) as any; + } + // } + }; + + private parallelUploadFiles = (options, config) => new Promise((rootResolve, rootReject) => { + const promises: Promise[] = []; + this.iframeFormList.forEach((inputElement) => { + let res: Promise; + if (inputElement) { + if ( + inputElement.fieldType + === ELEMENTS.FILE_INPUT.name + ) { + res = this.uploadFiles(inputElement, config); + promises.push(res); + } + } + }); + Promise.allSettled( + promises, + ).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + resultSet.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value !== undefined && result.value !== null) { + if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { + errorResponse.push(result.value); + } else { + const response = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response); + } + } + } else if (result.status === 'rejected') { + errorResponse.push(result.reason); + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }); + + uploadFiles = (fileElement, clientConfig) => { + this.#client = new Client(clientConfig, {}); + if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); + const fileUploadObject: any = {}; + + const { + state, tableName, skyflowID, onFocusChange, preserveFileName, + } = fileElement; + + if (state.isRequired) { + onFocusChange(false); + } + try { + fileValidation(state.value, state.isRequired, fileElement); + } catch (err) { + return Promise.reject(err); + } + + const validatedFileState = fileValidation(state.value, state.isRequired, fileElement); + + if (!validatedFileState) { + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true)); + } + fileUploadObject[state.name] = state.value; + + const formData = new FormData(); + + const column = Object.keys(fileUploadObject)[0]; + + const value: Blob = Object.values(fileUploadObject)[0] as Blob; + + if (preserveFileName) { + const isValidFileName = vaildateFileName(state.value.name); + if (!isValidFileName) { + return Promise.reject( + new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true), + ); + } + formData.append(column, value); + } else { + const generatedFileName = generateUploadFileName(state.value.name); + formData.append(column, new File([value], generatedFileName, { type: state.value.type })); + } + + const client = this.#client; + const sendRequest = () => new Promise((rootResolve, rootReject) => { + client + .request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}/${skyflowID}/files`, + headers: { + authorization: `Bearer ${clientConfig.authToken}`, + 'content-type': 'multipart/form-data', + }, + }) + .then((response: any) => { + rootResolve(response); + }) + .catch((error) => { + rootReject(error); + }); + }); + + return new Promise((resolve, reject) => { + sendRequest() + .then((res) => resolve(res)) + .catch((err) => { + reject(err); + }); + }); + }; + + private tokenize = (options, clientConfig: any) => { + let errorMessage = ''; + const insertRequestObject: any = {}; + const updateRequestObject: any = {}; + + this.iframeFormList.forEach((inputElement) => { + if (inputElement) { + if (inputElement) { + if ( + inputElement.fieldType + !== ELEMENTS.FILE_INPUT.name && inputElement.fieldType + !== ELEMENTS.MULTI_FILE_INPUT.name + ) { + const { + // eslint-disable-next-line max-len + state, doesClientHasError, clientErrorText, errorText, onFocusChange, validations, + setValue, + } = inputElement; + if (state.isRequired || !state.isValid) { + onFocusChange(false); + } + if (validations + && checkForElementMatchRule(validations) + && checkForValueMatch(validations, inputElement)) { + setValue(state.value); + onFocusChange(false); + } + if (!state.isValid || !state.isComplete) { + if (doesClientHasError) { + errorMessage += `${state.name}:${clientErrorText}`; + } else { errorMessage += `${state.name}:${errorText} `; } + } + } + } + } + }); + + // return for error + if (errorMessage.length > 0) { + // eslint-disable-next-line max-len + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.COMPLETE_AND_VALID_INPUTS, [`${errorMessage}`], true)); + } + // eslint-disable-next-line consistent-return + this.iframeFormList.forEach((inputElement) => { + if (inputElement) { + const { + state, tableName, validations, skyflowID, + } = inputElement; + if (tableName) { + if ( + inputElement.fieldType + !== ELEMENTS.FILE_INPUT.name && inputElement.fieldType + !== ELEMENTS.MULTI_FILE_INPUT.name + ) { + if ( + inputElement.fieldType + === ELEMENTS.checkbox.name + ) { + if (insertRequestObject[state.name]) { + insertRequestObject[state.name] = `${insertRequestObject[state.name]},${state.value + }`; + } else { + insertRequestObject[state.name] = state.value; + } + } else if (insertRequestObject[tableName] && !(skyflowID === '') && skyflowID === undefined) { + if (get(insertRequestObject[tableName], state.name) + && !(validations && checkForElementMatchRule(validations))) { + return Promise.reject(new SkyflowError(SKYFLOW_ERROR_CODE.DUPLICATE_ELEMENT, + [state.name, tableName], true)); + } + set( + insertRequestObject[tableName], + state.name, + inputElement.getUnformattedValue(), + ); + } else if (skyflowID || skyflowID === '') { + if (skyflowID === '' || skyflowID === null) { + return Promise.reject(new SkyflowError( + SKYFLOW_ERROR_CODE.EMPTY_SKYFLOW_ID_IN_ADDITIONAL_FIELDS, + )); + } + if (updateRequestObject[skyflowID]) { + set( + updateRequestObject[skyflowID], + state.name, + inputElement.getUnformattedValue(), + ); + } else { + updateRequestObject[skyflowID] = {}; + set( + updateRequestObject[skyflowID], + state.name, + inputElement.getUnformattedValue(), + ); + set( + updateRequestObject[skyflowID], + 'table', + tableName, + ); + } + } else { + insertRequestObject[tableName] = {}; + set( + insertRequestObject[tableName], + state.name, + inputElement.getUnformattedValue(), + ); + } + } + } + } + }); + let finalInsertRequest; + let finalInsertRecords; + let finalUpdateRecords; + try { + [finalInsertRecords, finalUpdateRecords] = constructElementsInsertReq( + insertRequestObject, updateRequestObject, options.options, + ); + finalInsertRequest = constructInsertRecordRequest(finalInsertRecords, options.options); + } catch (error:any) { + return Promise.reject({ + error: error?.message, + }); + } + this.#client = new Client(clientConfig, {}); + const client = this.#client; + const sendRequest = () => new Promise((rootResolve, rootReject) => { + const insertPromiseSet: Promise[] = []; + + // const clientId = client.toJSON()?.metaData?.uuid || ''; + // getAccessToken(clientId).then((authToken) => { + if (finalInsertRequest.length !== 0) { + insertPromiseSet.push( + insertDataInCollect(finalInsertRequest, + client, options, finalInsertRecords, clientConfig.authToken as string), + ); + } + if (finalUpdateRecords.updateRecords.length !== 0) { + insertPromiseSet.push( + updateRecordsBySkyflowIDComposable( + finalUpdateRecords, client, options, clientConfig.authToken as string, + ), + ); + } + if (insertPromiseSet.length !== 0) { + Promise.allSettled(insertPromiseSet).then((resultSet: any) => { + const recordsResponse: any[] = []; + const errorsResponse: any[] = []; + + resultSet.forEach((result: + { status: string; value: any; reason?: any; }) => { + if (result.status === 'fulfilled') { + if (result.value.records !== undefined && Array.isArray(result.value.records)) { + result.value.records.forEach((record) => { + recordsResponse.push(record); + }); + } + if (result.value.errors !== undefined && Array.isArray(result.value.errors)) { + result.value.errors.forEach((error) => { + errorsResponse.push(error); + }); + } + } else { + if (result.reason?.records !== undefined && Array.isArray(result.reason?.records)) { + result.reason.records.forEach((record) => { + recordsResponse.push(record); + }); + } + if (result.reason?.errors !== undefined && Array.isArray(result.reason?.errors)) { + result.reason.errors.forEach((error) => { + errorsResponse.push(error); + }); + } + } + }); + if (errorsResponse.length === 0) { + rootResolve({ records: recordsResponse }); + } else if (recordsResponse.length === 0) rootReject({ errors: errorsResponse }); + else rootReject({ records: recordsResponse, errors: errorsResponse }); + }); + } + // }).catch((err) => { + // rootReject({ + // error: err, + // }); + // }); + }); + + return new Promise((resolve, reject) => { + sendRequest() + .then((res) => resolve(res)) + .catch((err) => reject(err)); + }); + }; + + // eslint-disable-next-line consistent-return + private multipleUploadFiles = + (fileElement: IFrameFormElement, + clientConfig, metaData) => new Promise((rootResolve, rootReject) => { + this.#client = new Client(clientConfig, {}); + if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); + const { + state, tableName, onFocusChange, preserveFileName, + } = fileElement; + if (state.isRequired) { + onFocusChange(false); + } + + if (fileElement.state.value === undefined || fileElement.state.value === null || fileElement.state.value === '') { + rootReject({ error: 'No files selected' }); + return; + } + const files = state.value instanceof FileList + ? Array.from(state.value) + : [state.value]; + + this.validateFiles(files, state, fileElement); + const insertRequest = this.createInsertRequest(files.length, metaData); + this.insertDataCallInMultiFiles( + insertRequest, this.#client, tableName as string, clientConfig.authToken as string, + ).then((response: any) => { + const skyflowIDs = this.extractSkyflowIDs(response); + if (skyflowIDs.length === 0) { + rootReject({ error: 'No skyflow IDs returned from insert data' }); + return; + } + const promises: Promise[] = []; + + files.forEach((file, index) => { + const fileUploadObject: any = {}; + fileUploadObject[state.name] = file; + const formData = new FormData(); + const column = Object.keys(fileUploadObject)[0]; + const value: Blob = Object.values(fileUploadObject)[0] as Blob; + if (preserveFileName) { + formData.append(column, value); + } else { + const generatedFileName = generateUploadFileName(file.name); + formData.append(column, new File([value], generatedFileName, { type: file.type })); + } + const client = this.#client; + const promise1 = new Promise((resolve, reject) => { + client + .request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}/${skyflowIDs[index]}/files`, + headers: { + authorization: `Bearer ${clientConfig.authToken}`, + 'content-type': 'multipart/form-data', + }, + }) + .then((response1) => { + resolve(response1); + }) + .catch((error) => { + reject(error); + }); + }); + promises.push(promise1); + }); + Promise.allSettled( + promises, + ).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + resultSet.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value !== undefined && result.value !== null) { + if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { + errorResponse.push(result.value); + } else { + const response1 = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response1); + } + } + } else if (result.status === 'rejected') { + errorResponse.push({ error: result.reason }); + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }).catch((error) => { + printLog(`${error}`, MessageType.LOG, this.context?.logLevel); + rootReject({ + error: error?.error || error, + }); + }); + }); + + private validateFiles = (files: File[], state: any, fileElement: IFrameFormElement) => { + files.forEach((file) => { + // Check file validation + const validatedFileState = fileValidation(file, state.isRequired, fileElement); + if (!validatedFileState) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_TYPE, [], true); + } + + // Check filename validation + const isValidFileName = vaildateFileName(file.name); + if (!isValidFileName) { + throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true); + } + }); + return true; + }; + + private createInsertRequest = (numberOfRequests: number, options = {}) => { + // Create basic request structure + const request = { + records: [] as Array<{ fields: Record }>, + tokenization: false, + }; + + // Add empty field objects based on number of requests + for (let i = 0; i < numberOfRequests; i += 1) { + request.records.push({ + fields: options === undefined ? {} : options, + }); + } + + return request; + }; + + private extractSkyflowIDs = (response: { records: Array<{ skyflow_id: string }> }): string[] => { + if (!response?.records || !Array.isArray(response.records)) { + return []; + } + + return response.records + .map((record) => record.skyflow_id) + .filter((id) => id !== undefined && id !== null); + }; + + private insertDataCallInMultiFiles = ( + insertRequest, + client: Client, + tableName: string, + authToken: string, + ) => new Promise((rootResolve, rootReject) => { + client + .request({ + body: { + ...insertRequest, + }, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}`, + headers: { + authorization: `Bearer ${authToken}`, + 'content-type': 'application/json', + }, + }) + .then((response: any) => { + // Extract skyflow IDs from response + const skyflowIDs = this.extractSkyflowIDs(response); + rootResolve({ + ...response, + skyflowIDs, // Add extracted IDs to response + }); + }) + .catch((error) => { + rootReject(error); + }); + }); + updateGroupData = () => { const frameName = window.name; const url = window.location?.href; @@ -56,7 +643,6 @@ export default class FrameElementInit { }; this.group = parsedRecord.record; this.containerId = parsedRecord.containerId; - bus .target(this.clientMetaData.clientDomain) .on(ELEMENT_EVENTS_TO_IFRAME.SET_VALUE + frameName, (data) => { @@ -73,6 +659,7 @@ export default class FrameElementInit { ...this.clientMetaData, isRequired, }, this.context, skyflowID); + this.iframeFormList.push(this.iframeFormElement); return this.iframeFormElement; }; @@ -184,11 +771,21 @@ export default class FrameElementInit { iFrameFormElement, element, elementDiv, + this.clientMetaData.clientDomain, ); + this.frameList.push(this.frameElement); + if (isComposableContainer && errorTextElement) { iFrameFormElement.on(ELEMENT_EVENTS_TO_CLIENT.BLUR, (state) => { errorTextMap[element.elementName] = state.error; this.#updateCombinedErrorText(errorTextElement.id, errorTextMap); + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, + data: { height: rootDiv.scrollHeight, name: window.name }, + }, + this.clientMetaData.clientDomain, + ); }); } @@ -208,6 +805,24 @@ export default class FrameElementInit { bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window.name, (data, callback) => { callback({ height: rootDiv.scrollHeight, name: window.name }); }); + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, + data: { height: rootDiv.scrollHeight, name: window.name }, + }, + this.clientMetaData.clientDomain, + ); + window.addEventListener('message', (event) => { + if (event.data.name === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + window.name) { + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + window.name, + data: { height: rootDiv.scrollHeight, name: window.name }, + }, + this.clientMetaData.clientDomain, + ); + } + }); }; #updateCombinedErrorText = (elementId, errorMessages) => { diff --git a/src/core/internal/iframe-form/index.ts b/src/core/internal/iframe-form/index.ts index 2ae70aac..de56c27f 100644 --- a/src/core/internal/iframe-form/index.ts +++ b/src/core/internal/iframe-form/index.ts @@ -219,6 +219,35 @@ export default class IFrameFormElement extends EventEmitter { : ELEMENT_EVENTS_TO_CLIENT.BLUR, value: { ...this.getStatus() }, }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: focus + ? ELEMENT_EVENTS_TO_CLIENT.FOCUS + : ELEMENT_EVENTS_TO_CLIENT.BLUR, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: focus + ? ELEMENT_EVENTS_TO_CLIENT.FOCUS + : ELEMENT_EVENTS_TO_CLIENT.BLUR, + name: this.iFrameName, + value: { ...this.getStatus() }, + }, + }, this.metaData.clientDomain); + } + } if (!focus) { bus.emit(ELEMENT_EVENTS_TO_CLIENT.BLUR + this.iFrameName); this._emit(ELEMENT_EVENTS_TO_CLIENT.BLUR, { @@ -282,6 +311,40 @@ export default class IFrameFormElement extends EventEmitter { this.mask = newMask; } + getFileDetails = (value: FileList | File | null): Array<{ + name: string; + size: number; + type: string; + }> => { + // Return empty array if no value + if (!value) return []; + + try { + // Handle FileList + if (value instanceof FileList) { + return Array.from(value).map((file) => ({ + name: file.name, + size: file.size, + type: file.type, + })); + } + + // Handle single File + if (value instanceof File) { + return [{ + name: value.name, + size: value.size, + type: value.type, + }]; + } + + // Return empty array for invalid input + return []; + } catch (error) { + return []; + } + }; + setValidation(validations: IValidationRule[] | undefined) { if (ELEMENTS[this.fieldType].regex) { this.regex = ELEMENTS[this.fieldType].regex; @@ -469,6 +532,21 @@ export default class IFrameFormElement extends EventEmitter { resp = false; } if (this.preserveFileName) vaildateFileNames = vaildateFileName(value.name); + } else if (this.fieldType === ElementType.MULTI_FILE_INPUT) { + const files = this.state.value instanceof FileList + ? Array.from(this.state.value) + : [this.state.value]; + for (let i = 0; i < files.length; i += 1) { + try { + resp = fileValidation(files[i], this.state.isRequired, { + allowedFileType: this.allowedFileType, + blockEmptyFiles: this.blockEmptyFiles, + }); + } catch (err) { + resp = false; + } + if (this.preserveFileName) vaildateFileNames = vaildateFileName(files[i].name); + } } else { // eslint-disable-next-line no-lonely-if if (this.regex && value) { @@ -591,6 +669,31 @@ export default class IFrameFormElement extends EventEmitter { event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } } else if ( data.options !== undefined @@ -673,23 +776,99 @@ export default class IFrameFormElement extends EventEmitter { event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } else if ( this.state.value && (this.fieldType === ELEMENTS.EXPIRATION_DATE.name || this.fieldType === ELEMENTS.EXPIRATION_MONTH.name - || this.fieldType === ELEMENTS.FILE_INPUT.name) + || this.fieldType === ELEMENTS.FILE_INPUT.name + || this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) ) { bus.emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, { name: this.iFrameName, event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } else if (!this.state.isEmpty) { bus.emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, { name: this.iFrameName, event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, value: this.getStatus(), }); + if (this.containerType === ContainerType.COMPOSABLE) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: { + ...this.getStatus(), + value: '', + metaData: this.getFileDetails(this.getStatus().value), + }, + }, + }, this.metaData.clientDomain); + } else { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, + data: { + event: ELEMENT_EVENTS_TO_CLIENT.CHANGE, + name: this.iFrameName, + value: this.getStatus(), + }, + }, this.metaData.clientDomain); + } + } } this._emit(ELEMENT_EVENTS_TO_CLIENT.CHANGE, { diff --git a/src/core/internal/index.ts b/src/core/internal/index.ts index 8648eb8c..aca89ed3 100644 --- a/src/core/internal/index.ts +++ b/src/core/internal/index.ts @@ -89,15 +89,20 @@ export default class FrameElement { private selectedData?: number = undefined; + private clientDomain: string; + constructor( iFrameFormElement: IFrameFormElement, options: any, htmlDivElement: HTMLDivElement, + clientDomain: string = '', ) { + this.clientDomain = clientDomain; this.iFrameFormElement = iFrameFormElement; this.options = options; this.htmlDivElement = htmlDivElement; this.hasError = false; + this.mount(); this.iFrameFormElement.fieldName = options.column; this.iFrameFormElement.tableName = options.table; @@ -138,7 +143,6 @@ export default class FrameElement { this.inputParent = document.createElement('div'); this.inputParent.style.position = 'relative'; - const inputElement = document.createElement(type); this.domInput = inputElement; this.domInput.iFrameFormElement = this.iFrameFormElement; @@ -217,6 +221,9 @@ export default class FrameElement { if (state.value && this.iFrameFormElement.fieldType === ELEMENTS.FILE_INPUT.name) { this.focusChange(false); } + if (state.value && this.iFrameFormElement.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + this.focusChange(false); + } this.focusChange(false); if (state.error && this.domError) { @@ -454,6 +461,12 @@ export default class FrameElement { .emit(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iFrameFormElement.iFrameName, { name: this.iFrameFormElement.iFrameName, }); + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.iFrameFormElement.iFrameName, + data: { + name: this.iFrameFormElement.iFrameName, + }, + }, this.clientDomain); this.updateStyleClasses(this.iFrameFormElement.getStatus()); }; @@ -567,6 +580,10 @@ export default class FrameElement { const target = event.target as HTMLFormElement; this.iFrameFormElement.setValue(target.files[0], target.checkValidity()); this.focusChange(true); + } else if (this.iFrameFormElement.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + const target = event.target as HTMLFormElement; + this.iFrameFormElement.setValue(target.files, target.checkValidity()); + this.focusChange(true); } else { const target = event.target as HTMLInputElement; const { mask } = this.iFrameFormElement; @@ -817,11 +834,21 @@ export default class FrameElement { }; onSubmit = () => { - bus - .emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameFormElement.iFrameName, { - name: this.iFrameFormElement.iFrameName, - event: ELEMENT_EVENTS_TO_CLIENT.SUBMIT, - }); + if (this.iFrameFormElement.containerType === ContainerType.COMPOSABLE) { + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameFormElement.iFrameName, + data: { + name: this.iFrameFormElement.iFrameName, + event: ELEMENT_EVENTS_TO_CLIENT.SUBMIT, + }, + }, this.clientDomain); + } else { + bus + .emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameFormElement.iFrameName, { + name: this.iFrameFormElement.iFrameName, + event: ELEMENT_EVENTS_TO_CLIENT.SUBMIT, + }); + } }; onArrowKeys = (keyBoardEvent: KeyboardEvent) => { diff --git a/src/core/internal/internal-types/index.ts b/src/core/internal/internal-types/index.ts index bb236b3f..5cec82cf 100644 --- a/src/core/internal/internal-types/index.ts +++ b/src/core/internal/internal-types/index.ts @@ -77,6 +77,7 @@ export interface ClientMetadata { } export interface Metadata extends ClientMetadata { + getSkyflowBearerToken: () => Promise | undefined | unknown; clientJSON: ClientToJSON; containerType: ContainerType; skyflowContainer: SkyflowContainer; diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index 5611c51b..7fac88e1 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -15,17 +15,25 @@ import { RENDER_ELEMENT_IMAGE_STYLES, DEFAULT_FILE_RENDER_ERROR, ELEMENT_EVENTS_TO_CLIENT, + REVEAL_TYPES, } from '../../constants'; import getCssClassesFromJss, { generateCssWithoutClass } from '../../../libs/jss-styles'; import { printLog, parameterizedString, } from '../../../utils/logs-helper'; import logs from '../../../utils/logs'; -import { Context, MessageType } from '../../../utils/common'; +import { + Context, IRenderResponseType, IRevealRecord, MessageType, +} from '../../../utils/common'; import { constructMaskTranslation, - getAtobValue, getMaskedOutput, getValueFromName, handleCopyIconClick, styleToString, + formatRevealElementOptions, + getAtobValue, + getMaskedOutput, getValueFromName, handleCopyIconClick, styleToString, } from '../../../utils/helpers'; +import { formatForRenderClient, getFileURLFromVaultBySkyflowIDComposable } from '../../../core-utils/reveal'; +import Client from '../../../client'; +import properties from '../../../properties'; const { getType } = require('mime'); @@ -65,6 +73,8 @@ class RevealFrame { #skyflowContainerId: string = ''; + #client!: Client; + static init() { const url = window.location?.href; const configIndex = url.indexOf('?'); @@ -75,9 +85,9 @@ class RevealFrame { parsedRecord.context, skyflowContainerId); } - constructor(record, context, id) { + constructor(record, context, id, rootDiv?) { this.#skyflowContainerId = id; - this.#name = window.name; + this.#name = rootDiv ? record?.name : window.name; this.#containerId = getValueFromName(this.#name, 2); const encodedClientDomain = getValueFromName(this.#name, 4); const clientDomain = getAtobValue(encodedClientDomain); @@ -91,14 +101,14 @@ class RevealFrame { getCssClassesFromJss(REVEAL_ELEMENT_DIV_STYLE, 'div'); this.#labelElement = document.createElement('span'); - this.#labelElement.className = `SkyflowElement-label-${STYLE_TYPE.BASE}`; + this.#labelElement.className = `SkyflowElement-${this.#name}-label-${STYLE_TYPE.BASE}`; this.#dataElememt = document.createElement('span'); - this.#dataElememt.className = `SkyflowElement-content-${STYLE_TYPE.BASE}`; + this.#dataElememt.className = `SkyflowElement-${this.#name}-content-${STYLE_TYPE.BASE}`; this.#dataElememt.id = this.#name; this.#errorElement = document.createElement('span'); - this.#errorElement.className = `SkyflowElement-error-${STYLE_TYPE.BASE}`; + this.#errorElement.className = `SkyflowElement-${this.#name}-error-${STYLE_TYPE.BASE}`; if (this.#record.enableCopy) { this.domCopy = document.createElement('img'); @@ -125,13 +135,14 @@ class RevealFrame { ...REVEAL_ELEMENT_LABEL_DEFAULT_STYLES[STYLE_TYPE.BASE], ...this.#record.labelStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#labelStyles, 'label'); + // getCssClassesFromJss(this.#labelStyles, 'label'); + getCssClassesFromJss(this.#labelStyles, `${this.#name}-label`); if (this.#record.labelStyles[STYLE_TYPE.GLOBAL]) { generateCssWithoutClass(this.#record.labelStyles[STYLE_TYPE.GLOBAL]); } } else { - getCssClassesFromJss(REVEAL_ELEMENT_LABEL_DEFAULT_STYLES, 'label'); + getCssClassesFromJss(REVEAL_ELEMENT_LABEL_DEFAULT_STYLES, `${this.#name}-label`); } } this.updateDataView(); @@ -140,7 +151,7 @@ class RevealFrame { this.#inputStyles[STYLE_TYPE.BASE] = { ...this.#record.inputStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#inputStyles, 'content'); + getCssClassesFromJss(this.#inputStyles, `${this.#name}-content`); if (this.#record.inputStyles[STYLE_TYPE.GLOBAL]) { generateCssWithoutClass(this.#record.inputStyles[STYLE_TYPE.GLOBAL]); } @@ -155,26 +166,68 @@ class RevealFrame { ...REVEAL_ELEMENT_ERROR_TEXT_DEFAULT_STYLES[STYLE_TYPE.BASE], ...this.#record.errorTextStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#errorTextStyles, 'error'); + getCssClassesFromJss(this.#errorTextStyles, `${this.#name}-error`); if (this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]) { generateCssWithoutClass(this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]); } } else { getCssClassesFromJss( REVEAL_ELEMENT_ERROR_TEXT_DEFAULT_STYLES, - 'error', + `${this.#name}-error`, ); } this.#elementContainer.appendChild(this.#dataElememt); - document.body.append(this.#elementContainer); + if (rootDiv) rootDiv.append(this.#elementContainer); + else document.body.append(this.#elementContainer); + // document.body.append(this.#elementContainer); bus.emit(ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#name, { name: this.#name }); - + if (rootDiv) { + this.getConfig(); + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#name, + data: { + name: this.#name, + }, + }, this.#clientDomain); + + window.parent.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + data: { height: this.#elementContainer.scrollHeight, name: this.#name }, + }, this.#clientDomain); + } bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, (_, callback) => { callback({ height: this.#elementContainer.scrollHeight, name: this.#name }); }); + const sub2 = (responseUrl) => { + if (responseUrl.iframeName === this.#name) { + if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { + this.setRevealError(DEFAULT_FILE_RENDER_ERROR); + if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { + this.#dataElememt.innerText = this.#record.altText; + } + bus + .emit( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + { + height: this.#elementContainer.scrollHeight, + }, () => { + }, + ); + } else { + const ext = this.getExtension(responseUrl.url); + this.addFileRender(responseUrl.url, ext); + } + } + }; + bus + .target(window.location.origin) + .on( + ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + this.#name, + sub2, + ); const sub = (data) => { if (Object.prototype.hasOwnProperty.call(data, this.#record.token)) { @@ -189,7 +242,7 @@ class RevealFrame { this.#dataElememt.innerText = formattedOutput; } printLog(parameterizedString(logs.infoLogs.ELEMENT_REVEALED, - CLASS_NAME, this.#record.token), MessageType.LOG, this.#context.logLevel); + CLASS_NAME, this.#record.token), MessageType.LOG, this.#context?.logLevel); // bus // .target(window.location.origin) @@ -220,35 +273,172 @@ class RevealFrame { if (data.isTriggerError) { this.setRevealError(data.clientErrorText as string); } else { this.setRevealError(''); } } }); + window.parent.postMessage( + { + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + this.#containerId, + data: { + name: window.name, + }, + }, this.#clientDomain, + ); this.updateRevealElementOptions(); - - const sub2 = (responseUrl) => { - if (responseUrl.iframeName === this.#name) { - if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { - this.setRevealError(DEFAULT_FILE_RENDER_ERROR); - if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { - this.#dataElememt.innerText = this.#record.altText; - } - bus - .emit( - ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, - { - height: this.#elementContainer.scrollHeight, - }, () => { - }, + window.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_REQUESTS + this.#name) { + if (event?.data?.data?.iframeName === this.#name + && event?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { + this.renderFile(this.#record, event?.data?.clientConfig)?.then((resolvedResult) => { + const result = formatForRenderClient( + resolvedResult as IRenderResponseType, + this.#record?.column, ); - } else { - const ext = this.getExtension(responseUrl.url); - this.addFileRender(responseUrl.url, ext); + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, + data: { + type: REVEAL_TYPES.RENDER_FILE, + result, + }, + }, this.#clientDomain); + + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + })?.catch((error) => { + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, + data: { + type: REVEAL_TYPES.RENDER_FILE, + result: { + errors: error, + }, + }, + }, this.#clientDomain); + + window?.postMessage({ + type: ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK_COMPOSABLE + window?.name, + }, properties?.IFRAME_SECURE_ORIGIN); + }); } } - }; - bus - .target(window.location.origin) - .on( - ELEMENT_EVENTS_TO_IFRAME.RENDER_FILE_RESPONSE_READY + this.#name, - sub2, + + if (event?.data?.type === ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name) { + if (event?.data?.data?.height) { + window?.parent?.postMessage({ + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + data: { + height: this.#elementContainer?.scrollHeight ?? 0, + name: this.#name, + }, + }, this.#clientDomain); + } + } + }); + } + + responseUpdate = (data) => { + if (data?.frameId === this.#record?.name && data?.error) { + if (!Object.prototype.hasOwnProperty.call(this.#record, 'skyflowID')) { + this.setRevealError(REVEAL_ELEMENT_ERROR_TEXT); + } + } else if (data?.frameId === this.#record?.name + && data?.[0]?.token + && this.#record?.token === data?.[0]?.token) { + const responseValue = data?.[0]?.value as string ?? ''; + this.#revealedValue = responseValue; + this.isRevealCalled = true; + this.#dataElememt.innerText = responseValue; + + if (this.#record?.mask) { + const { formattedOutput } = getMaskedOutput( + this.#dataElememt?.innerText ?? '', + this.#record?.mask?.[0], + constructMaskTranslation(this.#record?.mask), + ); + this.#dataElememt.innerText = formattedOutput ?? ''; + } + + printLog( + parameterizedString( + logs?.infoLogs?.ELEMENT_REVEALED, + CLASS_NAME, + this.#record?.token, + ), + MessageType.LOG, + this.#context?.logLevel, ); + } + + window?.parent?.postMessage( + { + type: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + data: { + height: this.#elementContainer?.scrollHeight ?? 0, + name: this.#name, + }, + }, + this.#clientDomain, + ); + }; + + getConfig = () => { + const url = window.location?.href; + const configIndex = url.indexOf('?'); + const encodedString = configIndex !== -1 ? decodeURIComponent(url.substring(configIndex + 1)) : ''; + const parsedRecord = encodedString ? JSON.parse(atob(encodedString)) : {}; + this.#clientDomain = parsedRecord.clientDomain || ''; + this.#containerId = parsedRecord.containerId; + }; + + getData = () => this.#record; + + private sub2 = (responseUrl) => { + if (responseUrl.iframeName === this.#name) { + if (Object.prototype.hasOwnProperty.call(responseUrl, 'error') && responseUrl.error === DEFAULT_FILE_RENDER_ERROR) { + this.setRevealError(DEFAULT_FILE_RENDER_ERROR); + if (Object.prototype.hasOwnProperty.call(this.#record, 'altText')) { + this.#dataElememt.innerText = this.#record.altText; + } + bus + .emit( + ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#name, + { + height: this.#elementContainer.scrollHeight, + }, () => { + }, + ); + } else { + const ext = this.getExtension(responseUrl.url); + this.addFileRender(responseUrl.url, ext); + } + } + }; + + private renderFile(data: IRevealRecord, clientConfig) { + this.#client = new Client(clientConfig, {}); + return new Promise((resolve, reject) => { + try { + getFileURLFromVaultBySkyflowIDComposable(data, this.#client, clientConfig.authToken) + .then((resolvedResult) => { + let url = ''; + if (resolvedResult.fields && data.column) { + url = resolvedResult.fields[data.column]; + } + this.sub2({ + url, + iframeName: this.#name, + }); + resolve(resolvedResult); + }, + (rejectedResult) => { + this.sub2({ + error: DEFAULT_FILE_RENDER_ERROR, + iframeName: this.#name, + }); + reject(rejectedResult); + }); + } catch (err) { + reject(err); + } + }); } // eslint-disable-next-line class-methods-use-this @@ -312,6 +502,31 @@ class RevealFrame { } private updateRevealElementOptions() { + window.addEventListener('message', (event) => { + if (event?.data?.name === ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + + this.#name) { + const data = event?.data; + if (data.updateType === REVEAL_ELEMENT_OPTIONS_TYPES.ELEMENT_PROPS) { + const updatedValue = data.updatedValue as object; + this.#record = { + ...this.#record, + ...updatedValue, + ...formatRevealElementOptions(updatedValue), + }; + this.updateElementProps(); + if (this.isRevealCalled) { + if (this.#record?.mask) { + const { formattedOutput } = getMaskedOutput( + this.#revealedValue ?? '', + this.#record?.mask?.[0], + constructMaskTranslation(this.#record?.mask), + ); + this.#dataElememt.innerText = formattedOutput ?? ''; + } + } + } + } + }); bus .target(this.#clientDomain) .on(ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + this.#name, (data) => { @@ -355,7 +570,7 @@ class RevealFrame { ...this.#inputStyles, ...this.#record.inputStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#inputStyles, 'content'); + getCssClassesFromJss(this.#inputStyles, `${this.#name}-content`); if (this.#record.inputStyles[STYLE_TYPE.GLOBAL]) { const newInputGlobalStyles = { ...this.#inputStyles[STYLE_TYPE.GLOBAL], @@ -370,7 +585,7 @@ class RevealFrame { ...REVEAL_ELEMENT_LABEL_DEFAULT_STYLES[STYLE_TYPE.BASE], ...this.#record.labelStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#labelStyles, 'label'); + getCssClassesFromJss(this.#labelStyles, `${this.#name}-label`); if (this.#record.labelStyles[STYLE_TYPE.GLOBAL]) { const newLabelGlobalStyles = { @@ -386,7 +601,7 @@ class RevealFrame { ...this.#errorTextStyles[STYLE_TYPE.BASE], ...this.#record.errorTextStyles[STYLE_TYPE.BASE], }; - getCssClassesFromJss(this.#errorTextStyles, 'error'); + getCssClassesFromJss(this.#errorTextStyles, `${this.#name}-error`); if (this.#record.errorTextStyles[STYLE_TYPE.GLOBAL]) { const newErrorTextGlobalStyles = { ...this.#errorTextStyles[STYLE_TYPE.GLOBAL], diff --git a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts index 7b44335a..02b8fe72 100644 --- a/src/core/internal/skyflow-frame/skyflow-frame-controller.ts +++ b/src/core/internal/skyflow-frame/skyflow-frame-controller.ts @@ -106,11 +106,11 @@ class SkyflowFrameController { try { window.CoralogixRum.info(SDK_IFRAME_EVENT, data.event); printLog(parameterizedString(logs.infoLogs.METRIC_CAPTURE_EVENT), - MessageType.LOG, this.#context.logLevel); + MessageType.LOG, this.#context?.logLevel); } catch (err: any) { printLog(parameterizedString(logs.infoLogs.UNKNOWN_METRIC_CAPTURE_EVENT, err.toString()), - MessageType.LOG, this.#context.logLevel); + MessageType.LOG, this.#context?.logLevel); } } }, @@ -512,7 +512,8 @@ class SkyflowFrameController { if (inputElement) { if ( inputElement.iFrameFormElement.fieldType - !== ELEMENTS.FILE_INPUT.name + !== ELEMENTS.FILE_INPUT.name && inputElement.iFrameFormElement.fieldType + !== ELEMENTS.MULTI_FILE_INPUT.name ) { const { state, doesClientHasError, clientErrorText, errorText, onFocusChange, validations, @@ -552,6 +553,7 @@ class SkyflowFrameController { if ( inputElement.iFrameFormElement.fieldType !== ELEMENTS.FILE_INPUT.name + && inputElement.iFrameFormElement.fieldType !== ELEMENTS.MULTI_FILE_INPUT.name ) { if ( inputElement.iFrameFormElement.fieldType diff --git a/src/index-internal.ts b/src/index-internal.ts index 31274800..1cf7049f 100644 --- a/src/index-internal.ts +++ b/src/index-internal.ts @@ -4,6 +4,7 @@ Copyright (c) 2022 Skyflow, Inc. import 'core-js/stable'; import RevealFrame from './core/internal/reveal/reveal-frame'; import { + COMPOSABLE_REVEAL, FRAME_ELEMENT, FRAME_REVEAL, SKYFLOW_FRAME_CONTROLLER, @@ -18,6 +19,7 @@ import { } from './utils/logs-helper'; import { getAtobValue, getValueFromName } from './utils/helpers'; import FrameElementInit from './core/internal/frame-element-init'; +import RevealComposableFrameElementInit from './core/internal/composable-frame-element-init'; (function init(root: any) { try { @@ -26,6 +28,9 @@ import FrameElementInit from './core/internal/frame-element-init'; const frameId = getValueFromName(frameName, 1); if (frameType === SKYFLOW_FRAME_CONTROLLER) { SkyflowFrameController.init(frameId); + } else if (frameType === COMPOSABLE_REVEAL) { + root.Skyflow = RevealComposableFrameElementInit; + RevealComposableFrameElementInit.startFrameElement(); } else if (frameType === FRAME_ELEMENT) { const logLevel = getValueFromName(frameName, 4) || LogLevel.ERROR; printLog( diff --git a/src/libs/element-options.ts b/src/libs/element-options.ts index 360c6258..df162c82 100644 --- a/src/libs/element-options.ts +++ b/src/libs/element-options.ts @@ -380,7 +380,7 @@ export const formatOptions = ( break; } - case ELEMENTS.FILE_INPUT.name: { + case ELEMENTS.FILE_INPUT.name || ELEMENTS.MULTI_FILE_INPUT.name: { if (!Object.prototype.hasOwnProperty.call(formattedOptions, 'preserveFileName')) { formattedOptions = { ...formattedOptions, preserveFileName: true }; } @@ -402,7 +402,7 @@ export const formatOptions = ( if (Object.prototype.hasOwnProperty.call(formattedOptions, 'preserveFileName') && !validateBooleanOptions(formattedOptions.preserveFileName)) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_BOOLEAN_OPTIONS, ['preserveFileName'], true); } - if (elementType === ELEMENTS.FILE_INPUT.name) { + if (elementType === ELEMENTS.FILE_INPUT.name || elementType === ELEMENTS.MULTI_FILE_INPUT.name) { if (options.allowedFileType) { if (!Array.isArray(options.allowedFileType)) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_ALLOWED_OPTIONS, [], true); diff --git a/src/skyflow.ts b/src/skyflow.ts index 447dff8c..456509f0 100644 --- a/src/skyflow.ts +++ b/src/skyflow.ts @@ -48,11 +48,13 @@ import ComposableContainer from './core/external/collect/compose-collect-contain import { validateComposableContainerOptions } from './utils/validators'; import ThreeDS from './core/external/threeds/threeds'; import { ClientMetadata, SkyflowElementProps } from './core/internal/internal-types'; +import ComposableRevealContainer from './core/external/reveal/composable-reveal-container'; export enum ContainerType { COLLECT = 'COLLECT', REVEAL = 'REVEAL', COMPOSABLE = 'COMPOSABLE', + COMPOSE_REVEAL = 'COMPOSABLE_REVEAL', } export interface SkyflowConfigOptions { logLevel?: LogLevel; @@ -173,9 +175,50 @@ class Skyflow { return skyflow; } + #getSkyflowBearerToken = () => new Promise((resolve, reject) => { + if ( + this.#client.config.getBearerToken + && (!this.#bearerToken || !isTokenValid(this.#bearerToken)) + ) { + this.#client.config + .getBearerToken() + .then((bearerToken) => { + if (isTokenValid(bearerToken)) { + printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), + MessageType.LOG, + this.#logLevel); + this.#bearerToken = bearerToken; + resolve(this.#bearerToken); + } else { + printLog(parameterizedString( + logs.errorLogs.INVALID_BEARER_TOKEN, + ), MessageType.ERROR, this.#logLevel); + reject({ + error: parameterizedString( + logs.errorLogs.INVALID_BEARER_TOKEN, + ), + }); + } + }) + .catch((err) => { + printLog(parameterizedString(logs.errorLogs.BEARER_TOKEN_REJECTED), MessageType.ERROR, + this.#logLevel); + reject({ error: err }); + }); + } else { + printLog(parameterizedString(logs.infoLogs.REUSE_BEARER_TOKEN, CLASS_NAME), + MessageType.LOG, + this.#logLevel); + resolve(this.#bearerToken); + } + }); + container(type: ContainerType.COLLECT, options?: ContainerOptions): CollectContainer; container(type: ContainerType.COMPOSABLE, options?: ContainerOptions): ComposableContainer; container(type: ContainerType.REVEAL, options?: ContainerOptions): RevealContainer; + container(type: ContainerType.COMPOSE_REVEAL, + options?: ContainerOptions) + : ComposableRevealContainer; container(type: ContainerType, options?: ContainerOptions) { switch (type) { case ContainerType.COLLECT: { @@ -184,6 +227,7 @@ class Skyflow { clientJSON: this.#client.toJSON(), containerType: type, skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, }, this.#skyflowElements, { logLevel: this.#logLevel, env: this.#env }, options); @@ -198,6 +242,7 @@ class Skyflow { clientJSON: this.#client.toJSON(), containerType: type, skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, }, this.#skyflowElements, { logLevel: this.#logLevel, env: this.#env }, options); @@ -208,23 +253,38 @@ class Skyflow { } case ContainerType.COMPOSABLE: { validateComposableContainerOptions(options!); - const composableContainer = new ComposableContainer( - { - ...this.#metadata, - clientJSON: this.#client.toJSON(), - containerType: type, - skyflowContainer: this.#skyflowContainer, - }, - this.#skyflowElements, - { logLevel: this.#logLevel, env: this.#env }, - options!, - ); + const composableContainer = new ComposableContainer({ + ...this.#metadata, + clientJSON: this.#client.toJSON(), + containerType: type, + skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, + }, + this.#skyflowElements, + { logLevel: this.#logLevel, env: this.#env }, options!); printLog(parameterizedString(logs.infoLogs.COLLECT_CONTAINER_CREATED, CLASS_NAME), MessageType.LOG, this.#logLevel); return composableContainer; } + case ContainerType.COMPOSE_REVEAL: { + validateComposableContainerOptions(options!); + const revealComposableContainer = new ComposableRevealContainer(options, { + ...this.#metadata, + clientJSON: this.#client.toJSON(), + containerType: type, + skyflowContainer: this.#skyflowContainer, + getSkyflowBearerToken: this.#getSkyflowBearerToken, + }, + this.#skyflowElements, + { logLevel: this.#logLevel, env: this.#env }); + printLog(parameterizedString(logs.infoLogs.REVEAL_CONTAINER_CREATED, CLASS_NAME), + MessageType.LOG, + this.#logLevel); + return revealComposableContainer; + } + default: if (!type) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_CONTAINER_TYPE, [], true); diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index 6736d4a7..dec98386 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -75,6 +75,15 @@ export interface IRevealRecord { table?: string; } +export interface IRevealRecordComposable { + token?: string; + redaction?: RedactionType; + column?: string; + skyflowID?: string; + table?: string; + iframeName?: string; +} + export interface IInsertResponse { records: IInsertResponseReocrds[]; } @@ -309,6 +318,9 @@ export interface ICollectOptions { additionalFields?: IInsertRecordInput, upsert?: Array, } +export interface MetaData { + [key: string]: any, +} export interface UploadFilesResponse { fileUploadResponse?: Record, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e80fcf2c..539ae099 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -4,6 +4,12 @@ Copyright (c) 2022 Skyflow, Inc. import logs from './logs'; const SKYFLOW_ERROR_CODE = { + MULTI_FILE_NOT_SUPPORTED: { + code: 400, + description: logs.errorLogs.MULTI_FILE_NOT_SUPPORTED, + }, + INVALID_REVEAL_COMPOSABLE_INPUT: + { code: 400, description: logs.errorLogs.INVALID_REVEAL_COMPOSABLE_INPUT }, INVALID_FILE_NAME: { code: 400, description: logs.errorLogs.INVALID_FILE_NAME }, INVALID_FIELD: { code: 400, description: logs.errorLogs.INVALID_FIELD }, VAULTID_IS_REQUIRED: { code: 400, description: logs.errorLogs.VAULTID_IS_REQUIRED }, diff --git a/src/utils/logs.ts b/src/utils/logs.ts index 367ae2a8..8b5a75c0 100644 --- a/src/utils/logs.ts +++ b/src/utils/logs.ts @@ -10,6 +10,8 @@ const logs = { CREATE_COLLECT_CONTAINER: '%s1 - Creating Collect container.', COLLECT_CONTAINER_CREATED: '%s1 - Created Collect container successfully.', + INITIALIZE_COMPOSABLE_CLIENT: '%s1 - Initializing Composable container.', + CREATE_REVEAL_CONTAINER: '%s1 - Creating Reveal container.', REVEAL_CONTAINER_CREATED: '%s1 - Created Reveal container successfully.', @@ -26,6 +28,8 @@ const logs = { ELEMENT_REVEALED: '%s1 - %s2 Element revealed.', FILE_RENDERED: '%s1 - %s2 File rendered.', COLLECT_SUBMIT_SUCCESS: '%s1 - Data has been collected successfully.', + UPLOAD_FILES_SUCCESS: '%s1 - Files uploaded successfully.', + MULTI_UPLOAD_FILES_SUCCESS: '%s1 - Multiple files uploaded successfully.', REVEAL_SUBMIT_SUCCESS: '%s1 - Data has been revealed successfully.', RENDER_SUBMIT_SUCCESS: '%s1 - File download URL has been fetched successfully.', INSERT_DATA_SUCCESS: '%s1 - Data has been inserted successfully.', @@ -91,6 +95,8 @@ const logs = { VALIDATE_GET_BY_ID_INPUT: '%s1 - Validating getByID input.', }, errorLogs: { + MULTI_FILE_NOT_SUPPORTED: 'Multi file upload is only supported in MULT_FILE_INPUT element in composable container. Please use MULT_FILE_INPUT element for multi file upload.', + INVALID_REVEAL_COMPOSABLE_INPUT: 'Reveal composable input is invalid. Please provide a valid input.', NO_ELEMENTS_IN_COLLECT: 'Validation error. No elements found in collect container', NO_ELEMENTS_IN_COMPOSABLE: 'Validation error. No elements found in composable container', NO_ELEMENTS_IN_REVEAL: 'Validation error. No elements found in reveal container', From de5826b340856d9263668c687fc5ce5a9bfb0bee Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Tue, 29 Jul 2025 09:10:59 +0000 Subject: [PATCH 25/47] [AUTOMATED] Release - 2.5.0-beta.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41c443e7..3bb4392e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.4", + "version": "2.5.0-beta.8", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 612083a33a5ad97745acc4a51a0030380f93eed1 Mon Sep 17 00:00:00 2001 From: skyflow-bharti <118584001+skyflow-bharti@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:50:44 +0530 Subject: [PATCH 26/47] Release/25.7.8.2 (#616) * SK-2177 support composable container in shadow-dom and normal dom * SK-2177 internal release * [AUTOMATED] Release - 2.5.0-beta.5-dev.3d22e02 * SK-2177 fix promises * [AUTOMATED] Release - 2.5.0-beta.5-dev.c97c0f5 * SK-2177 mounted * [AUTOMATED] Release - 2.5.0-beta.5-dev.1a3ba57 * SK-2177 mounted * [AUTOMATED] Release - 2.5.0-beta.5-dev.4f9c03b * SK-2177 mounted * [AUTOMATED] Release - 2.5.0-beta.5-dev.410308a * SK-2177 comment mount * [AUTOMATED] Release - 2.5.0-beta.5-dev.06519d4 * SK-2177 comment mounted * [AUTOMATED] Release - 2.5.0-beta.5-dev.9d975d3 * SK-2177 client initialise * [AUTOMATED] Release - 2.5.0-beta.5-dev.1c981ce * SK-2177 add target * [AUTOMATED] Release - 2.5.0-beta.5-dev.deb34bf * SK-2177 add target * [AUTOMATED] Release - 2.5.0-beta.5-dev.7dda8b1 * SK-2177 add target * [AUTOMATED] Release - 2.5.0-beta.5-dev.a9e2143 * SK-2177 client initialise * [AUTOMATED] Release - 2.5.0-beta.5-dev.d5616a2 * SK-2177 client initialise * [AUTOMATED] Release - 2.5.0-beta.5-dev.a0c77c9 * SK-2177 client initialise * [AUTOMATED] Release - 2.5.0-beta.5-dev.69a8f50 * SK-2177 client fix * [AUTOMATED] Release - 2.5.0-beta.5-dev.9812099 * [AUTOMATED] Release - 2.5.0-beta.5-dev.0ee2970 * [AUTOMATED] Release - 2.5.0-beta.5-dev.ef404d4 * SK-2177 target test * [AUTOMATED] Release - 2.5.0-beta.5-dev.5f64ad2 * SK-2177 target tests * [AUTOMATED] Release - 2.5.0-beta.5-dev.25da8a5 * SK-2177 target tests * SK-2177 fix height * [AUTOMATED] Release - 2.5.0-beta.5-dev.36a43c5 * SK-2177 file render * [AUTOMATED] Release - 2.5.0-beta.5-dev.fc93609 * SKS-2117 WIP reveal composable changes. * SKS-2177: WIP added reveal composble element styles. * SK-2177 added event for reveal composable * SK-2177 added event for reveal composable * SK-2177 add reveal element input validations. * SK-2177 file render changes * SK-2177 file render changes * [AUTOMATED] Release - 2.5.0-beta.5-dev.50ad902 * SK-2177 fix height * SK-2177 fix render changes * [AUTOMATED] Release - 2.5.0-beta.5-dev.8fd34b7 * SK-2177 added error handling * SK-2177 trigger release internal * [AUTOMATED] Release - 2.5.0-beta.5-dev.de7a4f1 * SK-2177 add optional checks * [AUTOMATED] Release - 2.5.0-beta.5-dev.f8a1528 * SK-2191 add support for multiple file upload * [AUTOMATED] Release - 2.5.0-beta.6-dev.97db0e3 * SK-2191 fix drag and drop * [AUTOMATED] Release - 2.5.0-beta.6-dev.e034fae * SK-2191 fix drag and drop * [AUTOMATED] Release - 2.5.0-beta.6-dev.9003510 * SK-2191 fix drag and drop * [AUTOMATED] Release - 2.5.0-beta.6-dev.b79aa08 * [AUTOMATED] Release - 2.5.0-beta.6-dev.5974a3a * SK-2191 fix drag and drop * SK-2197 removed commented code * [AUTOMATED] Release - 2.5.0-beta.6-dev.19a5b44 * SK-2191 fix height * [AUTOMATED] Release - 2.5.0-beta.6-dev.7c41e89 * [AUTOMATED] Release - 2.5.0-beta.6-dev.a29ea92 * SK-2202 fix the listeners * [AUTOMATED] Release - 2.4.2-dev.eb6dbb3 * SK-2202 fix the submit * [AUTOMATED] Release - 2.4.2-dev.47ea8b2 * SK-2202 add optional check * [AUTOMATED] Release - 2.4.2-dev.141ab05 * [AUTOMATED] Release - 2.4.2-dev.6c87190 * SK-2202 event listener * [AUTOMATED] Release - 2.5.0-beta.8-dev.d79138c * [AUTOMATED] Release - 2.5.0-beta.8-dev.cc9dc5a --------- Co-authored-by: skyflow-bharti Co-authored-by: yaswanth-pula-skyflow --- package.json | 2 +- src/core/external/collect/collect-element.ts | 3 ++- src/core/internal/iframe-form/index.ts | 20 +++++++++++++------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 3bb4392e..4ce21b38 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.8", + "version": "2.5.0-beta.8-dev.cc9dc5a", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index 6d73de71..f97794b5 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -513,7 +513,8 @@ class CollectElement extends SkyflowElement { this.#states[index].isFocused = data.value.isFocused; this.#states[index].isRequired = data.value.isRequired; this.#states[index].selectedCardScheme = data?.value?.selectedCardScheme || ''; - if (element.elementType === ElementType.MULTI_FILE_INPUT) { + if (element.elementType === ElementType.MULTI_FILE_INPUT + || element.elementType === ElementType.FILE_INPUT) { this.#states[index].metaData = data?.value?.metaData || []; } if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; diff --git a/src/core/internal/iframe-form/index.ts b/src/core/internal/iframe-form/index.ts index de56c27f..6da0eebb 100644 --- a/src/core/internal/iframe-form/index.ts +++ b/src/core/internal/iframe-form/index.ts @@ -220,7 +220,8 @@ export default class IFrameFormElement extends EventEmitter { value: { ...this.getStatus() }, }); if (this.containerType === ContainerType.COMPOSABLE) { - if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { window.parent.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, data: { @@ -670,7 +671,8 @@ export default class IFrameFormElement extends EventEmitter { value: this.getStatus(), }); if (this.containerType === ContainerType.COMPOSABLE) { - if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { window.parent.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, data: { @@ -777,7 +779,8 @@ export default class IFrameFormElement extends EventEmitter { value: this.getStatus(), }); if (this.containerType === ContainerType.COMPOSABLE) { - if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { window.parent.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, data: { @@ -806,7 +809,8 @@ export default class IFrameFormElement extends EventEmitter { && (this.fieldType === ELEMENTS.EXPIRATION_DATE.name || this.fieldType === ELEMENTS.EXPIRATION_MONTH.name || this.fieldType === ELEMENTS.FILE_INPUT.name - || this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) + || this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + ) ) { bus.emit(ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, { name: this.iFrameName, @@ -814,7 +818,8 @@ export default class IFrameFormElement extends EventEmitter { value: this.getStatus(), }); if (this.containerType === ContainerType.COMPOSABLE) { - if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { window.parent.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, data: { @@ -845,7 +850,8 @@ export default class IFrameFormElement extends EventEmitter { value: this.getStatus(), }); if (this.containerType === ContainerType.COMPOSABLE) { - if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name) { + if (this.fieldType === ELEMENTS.MULTI_FILE_INPUT.name + || this.fieldType === ELEMENTS.FILE_INPUT.name) { window.parent.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT + this.iFrameName, data: { @@ -905,4 +911,4 @@ export default class IFrameFormElement extends EventEmitter { this.resetData(); this.resetEvents(); } -} +} \ No newline at end of file From fd4c85f31475d2c45b7e494858cabf9277caa49c Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 14 Aug 2025 16:57:33 +0530 Subject: [PATCH 27/47] SK-2239: add file metadata in file render and wcag keyboard actions --- package.json | 2 +- src/core-utils/reveal.ts | 3 +- src/core/external/collect/collect-element.ts | 9 +++--- src/core/external/common/iframe.ts | 1 + .../reveal/composable-reveal-container.ts | 4 +-- .../reveal/composable-reveal-internal.ts | 20 ++++++------- src/core/internal/iframe-form/index.ts | 20 ++++++------- src/core/internal/index.ts | 30 ++++++++++++++++++- src/core/internal/reveal/reveal-frame.ts | 4 ++- src/utils/common/index.ts | 1 + tests/utils/jwt-utils.test.js | 4 +-- 11 files changed, 65 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 4ce21b38..3c308b17 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.8-dev.cc9dc5a", + "version": "2.4.3-dev.b8e2236", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", diff --git a/src/core-utils/reveal.ts b/src/core-utils/reveal.ts index f2087d84..bb334494 100644 --- a/src/core-utils/reveal.ts +++ b/src/core-utils/reveal.ts @@ -146,7 +146,7 @@ export const getFileURLForRender = ( paramList += `${skyflowIdRecord.skyflowID}?`; - paramList += `fields=${skyflowIdRecord.column}&${FILE_DOWNLOAD_URL_PARAM}`; + paramList += `fields=${skyflowIdRecord.column}&${FILE_DOWNLOAD_URL_PARAM}&returnFileMetadata=true`; const vaultEndPointurl: string = `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${skyflowIdRecord.table}/${paramList}`; return client.request({ @@ -345,6 +345,7 @@ export const formatForRenderClient = (response: IRenderResponseType, column: str const successRecord = { skyflow_id: response.fields.skyflow_id, column, + fileMetadata: response.fileMetadata, }; formattedResponse.success = successRecord; } else if (response.errors) { diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index f97794b5..df18953c 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -469,7 +469,7 @@ class CollectElement extends SkyflowElement { } }); this.#elements.forEach((element1) => { - const isComposableContainer = this.#elements.length > 1; + const isComposableContainer = this.#metaData?.containerType === 'COMPOSABLE'; if (isComposableContainer) { window.addEventListener('message', (event) => { if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.INPUT_EVENT @@ -483,7 +483,6 @@ class CollectElement extends SkyflowElement { ) { this.#eventEmitter._emit(ELEMENT_EVENTS_TO_CLIENT.READY); } else { - const isComposable = this.#elements.length > 1; this.#elements.forEach((element, index) => { if (data.name === element.elementName) { let emitEvent = ''; @@ -520,7 +519,7 @@ class CollectElement extends SkyflowElement { if (Object.prototype.hasOwnProperty.call(data.value, 'value')) this.#states[index].value = data.value.value; else this.#states[index].value = undefined; - emitEvent = isComposable ? `${emitEvent}:${data.name}` : emitEvent; + emitEvent = isComposableContainer ? `${emitEvent}:${data.name}` : emitEvent; this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, {}, (payload:any) => { @@ -532,12 +531,12 @@ class CollectElement extends SkyflowElement { ...this.#states[index], elementType: element.elementType, }; - if (isComposable) { + if (isComposableContainer) { this.#groupEmitter?._emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT, { iframeName: this.#iframe.name, }); } - if (isComposable && this.#groupEmitter) { + if (isComposableContainer && this.#groupEmitter) { this.#groupEmitter._emit(emitEvent, emitData); } else { this.#eventEmitter._emit(emitEvent, emitData); diff --git a/src/core/external/common/iframe.ts b/src/core/external/common/iframe.ts index defcf630..cde296f5 100644 --- a/src/core/external/common/iframe.ts +++ b/src/core/external/common/iframe.ts @@ -28,6 +28,7 @@ export default class IFrame { this.iframe = iframer({ name: this.name, referrer: clientDomain, + title: name.match(/^element:([^:]+):/)?.[1] ?? name, }); } diff --git a/src/core/external/reveal/composable-reveal-container.ts b/src/core/external/reveal/composable-reveal-container.ts index 2d829848..bf635096 100644 --- a/src/core/external/reveal/composable-reveal-container.ts +++ b/src/core/external/reveal/composable-reveal-container.ts @@ -187,9 +187,9 @@ class ComposableRevealContainer extends Container { let element = this.#elements[this.#tempElements.elementName]; if (element) { if (isSingleElementAPI) { - element.update(elements[0]); + // element.update(elements[0]); } else { - element.update(this.#tempElements); + // element.update(this.#tempElements); } } else { const elementId = uuid(); diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts index a5584755..618f753e 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -108,12 +108,6 @@ class ComposableRevealInternalElement extends SkyflowElement { this.#iframe?.setIframeHeight(data?.height); }); - window?.addEventListener('message', (event) => { - if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED - + this.#containerId) { - this.#isComposableFrameReady = true; - } - }); window?.addEventListener('message', (event) => { if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + this.#iframe?.name) { @@ -136,6 +130,12 @@ class ComposableRevealInternalElement extends SkyflowElement { rows?.forEach((row, rowIndex) => { row?.elements?.forEach((element: any, elementIndex: number) => { if (!element?.name) return; + window?.addEventListener('message', (event) => { + if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + element?.name) { + this.#isComposableFrameReady = true; + } + }); this.#eventEmitter?.on( `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST}:${element?.name}`, (data, callback) => { @@ -351,9 +351,9 @@ class ComposableRevealInternalElement extends SkyflowElement { MessageType.LOG, loglevel); validateRenderElementRecord(recordData); - window.addEventListener('message', (event) => { + window.addEventListener('message', (event) => { if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED - + this.#containerId) { + + recordData?.name) { this.#isMounted = true; this.#getSkyflowBearerToken()?.then((authToken) => { printLog(parameterizedString(logs.infoLogs.BEARER_TOKEN_RESOLVED, CLASS_NAME), @@ -377,7 +377,7 @@ class ComposableRevealInternalElement extends SkyflowElement { window.addEventListener('message', (event1) => { if (event1?.data && event1?.data?.type === ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE - + this.#iframe.name) { + + recordData.name) { if (event1?.data?.data?.type === REVEAL_TYPES.RENDER_FILE) { const revealData = event1?.data?.data?.result; if (revealData?.error) { @@ -592,7 +592,7 @@ class ComposableRevealInternalElement extends SkyflowElement { } else { window.addEventListener('message', (event) => { if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED - + this.#containerId) { + + record?.name) { this.#emitEvent( ELEMENT_EVENTS_TO_IFRAME.REVEAL_ELEMENT_UPDATE_OPTIONS + record.name, { diff --git a/src/core/internal/iframe-form/index.ts b/src/core/internal/iframe-form/index.ts index 6da0eebb..32d29010 100644 --- a/src/core/internal/iframe-form/index.ts +++ b/src/core/internal/iframe-form/index.ts @@ -313,9 +313,9 @@ export default class IFrameFormElement extends EventEmitter { } getFileDetails = (value: FileList | File | null): Array<{ - name: string; - size: number; - type: string; + fileName: string; + fileSizeKB: number; + fileType: string; }> => { // Return empty array if no value if (!value) return []; @@ -324,18 +324,18 @@ export default class IFrameFormElement extends EventEmitter { // Handle FileList if (value instanceof FileList) { return Array.from(value).map((file) => ({ - name: file.name, - size: file.size, - type: file.type, + fileName: file.name, + fileSizeKB: Math.ceil(file.size / 1024), + fileType: file.type, })); } // Handle single File if (value instanceof File) { return [{ - name: value.name, - size: value.size, - type: value.type, + fileName: value.name, + fileSizeKB: Math.ceil(value.size / 1024), + fileType: value.type, }]; } @@ -911,4 +911,4 @@ export default class IFrameFormElement extends EventEmitter { this.resetData(); this.resetEvents(); } -} \ No newline at end of file +} diff --git a/src/core/internal/index.ts b/src/core/internal/index.ts index aca89ed3..01878a53 100644 --- a/src/core/internal/index.ts +++ b/src/core/internal/index.ts @@ -145,6 +145,14 @@ export default class FrameElement { this.inputParent.style.position = 'relative'; const inputElement = document.createElement(type); this.domInput = inputElement; + inputElement.addEventListener('keydown', (event) => { + const keyboardEvent = event as KeyboardEvent; + if ((keyboardEvent.ctrlKey || keyboardEvent.metaKey) && keyboardEvent.key.toLowerCase() === 'z') { + keyboardEvent.preventDefault(); + this.setValue(''); + this.iFrameFormElement.setValue('', true); + } + }); this.domInput.iFrameFormElement = this.iFrameFormElement; inputElement.setAttribute(CUSTOM_ROW_ID_ATTRIBUTE, this.htmlDivElement?.id?.split(':')[0] || ''); this.inputParent.append(inputElement); @@ -164,6 +172,15 @@ export default class FrameElement { this.dropdownSelect = document.createElement('select'); this.dropdownSelect.setAttribute('style', this.options?.inputStyles?.dropdown ? (DROPDOWN_STYLES + styleToString(this.options.inputStyles.dropdown)) : DROPDOWN_STYLES); + this.dropdownSelect.addEventListener('focus', () => { + if (this.options?.inputStyles?.dropdownIcon?.focus) { + this.setDropdownIconStyle(this.options?.inputStyles?.dropdownIcon?.focus); + } + }); + + this.dropdownSelect.addEventListener('blur', () => { + this.setDropdownIconStyle(this.options?.inputStyles?.dropdownIcon); + }); this.dropdownSelect.addEventListener('change', (event:any) => { event.preventDefault(); @@ -888,7 +905,6 @@ export default class FrameElement { case INPUT_KEYBOARD_EVENTS.ENTER: this.onSubmit(); - keyBoardEvent.preventDefault(); break; default: break; @@ -1165,4 +1181,16 @@ export default class FrameElement { } } } + + private setDropdownIconStyle(styleObj?: any) { + if (this.dropdownIcon?.style.display === 'block') { + this.dropdownIcon.setAttribute( + 'style', + styleObj + ? DROPDOWN_ICON_STYLES + styleToString(styleObj) + : DROPDOWN_ICON_STYLES, + ); + this.dropdownIcon.style.display = 'block'; + } + } } diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index 7fac88e1..71a268d3 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -275,7 +275,7 @@ class RevealFrame { }); window.parent.postMessage( { - type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + this.#containerId, + type: ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + this.#name, data: { name: window.name, }, @@ -291,6 +291,8 @@ class RevealFrame { resolvedResult as IRenderResponseType, this.#record?.column, ); + console.log("event name : ",ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name) + console.log("resultt : ", result); window?.parent?.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, data: { diff --git a/src/utils/common/index.ts b/src/utils/common/index.ts index dec98386..57737439 100644 --- a/src/utils/common/index.ts +++ b/src/utils/common/index.ts @@ -99,6 +99,7 @@ export interface IRevealResponseType { export interface IRenderResponseType { fields?: Record errors?: Record + fileMetadata?: Record } export interface IDetokenizeInput { diff --git a/tests/utils/jwt-utils.test.js b/tests/utils/jwt-utils.test.js index e6c4f52e..332e11c5 100644 --- a/tests/utils/jwt-utils.test.js +++ b/tests/utils/jwt-utils.test.js @@ -5,7 +5,7 @@ import isTokenValid from "../../src/utils/jwt-utils"; jest.mock('jwt-decode', () => () => ({exp: 123})) describe('Validation token', () => { - + test('empty token', () => { const res = isTokenValid("") expect(res).toBe(false) @@ -20,4 +20,4 @@ describe('Validation token', () => { const res = isTokenValid("token") expect(res).toBe(false) }) -}); \ No newline at end of file +}); From df2f10b975f474299b2b178116a5fa20d2f85447 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 14 Aug 2025 11:28:22 +0000 Subject: [PATCH 28/47] [AUTOMATED] Release - 2.4.3-dev.c482a14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c308b17..991e2df5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.3-dev.b8e2236", + "version": "2.4.3-dev.c482a14", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From b4ad37424053da881745eb88d4e45a27df4a65a9 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 14 Aug 2025 17:22:25 +0530 Subject: [PATCH 29/47] SK-2239: remove console log --- src/core/internal/reveal/reveal-frame.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index 71a268d3..4476eded 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -291,8 +291,6 @@ class RevealFrame { resolvedResult as IRenderResponseType, this.#record?.column, ); - console.log("event name : ",ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name) - console.log("resultt : ", result); window?.parent?.postMessage({ type: ELEMENT_EVENTS_TO_IFRAME.REVEAL_CALL_RESPONSE + this.#name, data: { From e149568fb0810adab06596933ea6db533ed1c2f7 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 14 Aug 2025 11:53:08 +0000 Subject: [PATCH 30/47] [AUTOMATED] Release - 2.4.3-dev.15b2419 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 991e2df5..ee1cee60 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.3-dev.c482a14", + "version": "2.4.3-dev.15b2419", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From c41bd4617706fc123c452302de5d90c2bb99d4f0 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Thu, 14 Aug 2025 12:14:13 +0000 Subject: [PATCH 31/47] [AUTOMATED] Release - 2.5.0-beta.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee1cee60..55bd50d4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.3-dev.15b2419", + "version": "2.5.0-beta.9", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 05841007b8a92db5ac6998fe67dc3d699bca9715 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 13:15:45 +0530 Subject: [PATCH 32/47] SK-2254: fix 0px height in composable container --- src/core/external/collect/collect-element.ts | 29 +++- src/core/internal/frame-element-init.ts | 141 +++++++++++-------- src/core/internal/internal-types/index.ts | 1 + src/utils/validators/index.ts | 4 - 4 files changed, 104 insertions(+), 71 deletions(-) diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index df18953c..84d76c88 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -42,6 +42,7 @@ import { updateMetricObjectValue, } from '../../../metrics'; import { Metadata, ContainerProps, InternalState } from '../../internal/internal-types'; +import properties from '../../../properties'; const CLASS_NAME = 'Element'; class CollectElement extends SkyflowElement { @@ -139,6 +140,7 @@ class CollectElement extends SkyflowElement { name: element.elementName, isRequired: element.required, selectedCardScheme: '', + metaData: undefined, }); }); if (this.#elements && this.#elements.length && this.#elements.length > 1) { @@ -203,12 +205,27 @@ class CollectElement extends SkyflowElement { if (!domElement) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['CollectElement'], true); } - this.resizeObserver = new ResizeObserver(() => { - this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - {}, (payload:any) => { - this.#iframe.setIframeHeight(payload.height); - }); - }); + + if (domElement instanceof HTMLElement) { + this.resizeObserver = new ResizeObserver(() => { + if (domElement.getElementsByTagName('iframe')[0]?.contentWindow) { + const iframeElement = domElement.getElementsByTagName('iframe')[0]; + if (iframeElement.name === this.#iframe.name) { + iframeElement?.contentWindow?.postMessage({ + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }); + } else if (typeof domElement === 'string') { + this.resizeObserver = new ResizeObserver(() => { + this.#bus.emit(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + {}, (payload:any) => { + this.#iframe.setIframeHeight(payload.height); + }); + }); + } + updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElement); if ( this.#metaData?.clientJSON?.config?.options?.trackMetrics diff --git a/src/core/internal/frame-element-init.ts b/src/core/internal/frame-element-init.ts index 13c89945..a3b033ca 100644 --- a/src/core/internal/frame-element-init.ts +++ b/src/core/internal/frame-element-init.ts @@ -217,6 +217,9 @@ export default class FrameElementInit { const value: Blob = Object.values(fileUploadObject)[0] as Blob; + formData.append('columnName', column); + formData.append('tableName', tableName); + if (preserveFileName) { const isValidFileName = vaildateFileName(state.value.name); if (!isValidFileName) { @@ -224,10 +227,14 @@ export default class FrameElementInit { new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_FILE_NAME, [], true), ); } - formData.append(column, value); + formData.append('file', value); } else { const generatedFileName = generateUploadFileName(state.value.name); - formData.append(column, new File([value], generatedFileName, { type: state.value.type })); + formData.append('file', new File([value], generatedFileName, { type: state.value.type })); + } + + if (skyflowID) { + formData.append('skyflowID', skyflowID); } const client = this.#client; @@ -236,7 +243,7 @@ export default class FrameElementInit { .request({ body: formData, requestMethod: 'POST', - url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}/${skyflowID}/files`, + url: `${client.config.vaultURL}/v2/vaults/${client.config.vaultID}/files/upload`, headers: { authorization: `Bearer ${clientConfig.authToken}`, 'content-type': 'multipart/form-data', @@ -457,73 +464,90 @@ export default class FrameElementInit { // eslint-disable-next-line consistent-return private multipleUploadFiles = - (fileElement: IFrameFormElement, - clientConfig, metaData) => new Promise((rootResolve, rootReject) => { + (fileElement: IFrameFormElement, clientConfig, metaData) => new Promise((rootResolve, rootReject) => { this.#client = new Client(clientConfig, {}); if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); - const { - state, tableName, onFocusChange, preserveFileName, - } = fileElement; + + const { state, tableName, onFocusChange, preserveFileName } = fileElement; if (state.isRequired) { onFocusChange(false); } - if (fileElement.state.value === undefined || fileElement.state.value === null || fileElement.state.value === '') { + if (state.value === undefined || state.value === null || state.value === '') { rootReject({ error: 'No files selected' }); return; } - const files = state.value instanceof FileList - ? Array.from(state.value) - : [state.value]; + const files = state.value instanceof FileList ? Array.from(state.value) : [state.value]; this.validateFiles(files, state, fileElement); - const insertRequest = this.createInsertRequest(files.length, metaData); - this.insertDataCallInMultiFiles( - insertRequest, this.#client, tableName as string, clientConfig.authToken as string, - ).then((response: any) => { - const skyflowIDs = this.extractSkyflowIDs(response); - if (skyflowIDs.length === 0) { - rootReject({ error: 'No skyflow IDs returned from insert data' }); - return; + + const uploadFile = (file: File, skyflowID?: string) => { + const formData = new FormData(); + formData.append('columnName', state.name); + if (tableName) formData.append('tableName', tableName); + if (preserveFileName) { + formData.append('file', file); + } else { + const generatedFileName = generateUploadFileName(file.name); + formData.append('file', new File([file], generatedFileName, { type: file.type })); } - const promises: Promise[] = []; - - files.forEach((file, index) => { - const fileUploadObject: any = {}; - fileUploadObject[state.name] = file; - const formData = new FormData(); - const column = Object.keys(fileUploadObject)[0]; - const value: Blob = Object.values(fileUploadObject)[0] as Blob; - if (preserveFileName) { - formData.append(column, value); - } else { - const generatedFileName = generateUploadFileName(file.name); - formData.append(column, new File([value], generatedFileName, { type: file.type })); + if (skyflowID) formData.append('skyflowID', skyflowID); + const client = this.#client; + return this.#client.request({ + body: formData, + requestMethod: 'POST', + url: `${client.config.vaultURL}/v2/vaults/${this.#client.config.vaultID}/files/upload`, + headers: { + authorization: `Bearer ${clientConfig.authToken}`, + 'content-type': 'multipart/form-data', + }, + }); + }; + + if (metaData && Object.keys(metaData).length > 0) { + const insertRequest = this.createInsertRequest(files.length, metaData); + this.insertDataCallInMultiFiles( + insertRequest, this.#client, tableName as string, clientConfig.authToken as string, + ).then((response: any) => { + const skyflowIDs = this.extractSkyflowIDs(response); + if (skyflowIDs.length === 0) { + rootReject({ error: 'No skyflow IDs returned from insert data' }); + return; } - const client = this.#client; - const promise1 = new Promise((resolve, reject) => { - client - .request({ - body: formData, - requestMethod: 'POST', - url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}/${tableName}/${skyflowIDs[index]}/files`, - headers: { - authorization: `Bearer ${clientConfig.authToken}`, - 'content-type': 'multipart/form-data', - }, - }) - .then((response1) => { - resolve(response1); - }) - .catch((error) => { - reject(error); - }); + const promises = files.map((file, idx) => uploadFile(file, skyflowIDs[idx])); + Promise.allSettled(promises).then((resultSet) => { + const fileUploadResponse: any[] = []; + const errorResponse: any[] = []; + resultSet.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value !== undefined && result.value !== null) { + if (Object.prototype.hasOwnProperty.call(result.value, 'error')) { + errorResponse.push(result.value); + } else { + const response1 = typeof result.value === 'string' + ? JSON.parse(result.value) + : result.value; + fileUploadResponse.push(response1); + } + } + } else if (result.status === 'rejected') { + errorResponse.push({ error: result.reason }); + } + }); + if (errorResponse.length === 0) { + rootResolve({ fileUploadResponse }); + } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); + else rootReject({ fileUploadResponse, errorResponse }); + }); + }).catch((error) => { + printLog(`${error}`, MessageType.LOG, this.context?.logLevel); + rootReject({ + error: error?.error || error, }); - promises.push(promise1); }); - Promise.allSettled( - promises, - ).then((resultSet) => { + } else { + const promises = files.map((file) => uploadFile(file)); + Promise.allSettled(promises).then((resultSet) => { const fileUploadResponse: any[] = []; const errorResponse: any[] = []; resultSet.forEach((result) => { @@ -547,12 +571,7 @@ export default class FrameElementInit { } else if (fileUploadResponse.length === 0) rootReject({ errorResponse }); else rootReject({ fileUploadResponse, errorResponse }); }); - }).catch((error) => { - printLog(`${error}`, MessageType.LOG, this.context?.logLevel); - rootReject({ - error: error?.error || error, - }); - }); + } }); private validateFiles = (files: File[], state: any, fileElement: IFrameFormElement) => { diff --git a/src/core/internal/internal-types/index.ts b/src/core/internal/internal-types/index.ts index 5cec82cf..96b90abd 100644 --- a/src/core/internal/internal-types/index.ts +++ b/src/core/internal/internal-types/index.ts @@ -47,6 +47,7 @@ export interface RevealContainerProps { } export interface InternalState { + metaData: any; isEmpty: boolean, isValid: boolean, isFocused: boolean, diff --git a/src/utils/validators/index.ts b/src/utils/validators/index.ts index 5630f9d7..3d181ec8 100644 --- a/src/utils/validators/index.ts +++ b/src/utils/validators/index.ts @@ -562,10 +562,6 @@ export const validateCollectElementInput = (input: CollectElementInput, logLevel if (Object.prototype.hasOwnProperty.call(input, 'skyflowID') && !(typeof input.skyflowID === 'string')) { throw new SkyflowError(SKYFLOW_ERROR_CODE.INVALID_SKYFLOWID_IN_COLLECT, [], true); } - if (input.type === ElementType.FILE_INPUT - && !Object.keys(input).includes('skyflowID')) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.MISSING_SKYFLOWID_IN_COLLECT, [], true); - } }; export const validateUpsertOptions = (upsertOptions) => { From ad9d4ce6d87159c1198741a0f5431f5d87878d0b Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 07:46:37 +0000 Subject: [PATCH 33/47] [AUTOMATED] Release - 2.5.0-beta.9-dev.99fa815 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55bd50d4..c086c343 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.9", + "version": "2.5.0-beta.9-dev.99fa815", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From bbaf520b429daaf3f29005b26adb6040adb994c7 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 14:33:13 +0530 Subject: [PATCH 34/47] SK-2254: fix 0px height in composable reveal --- .../reveal/composable-reveal-internal.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts index 618f753e..2e5a2cbf 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -54,6 +54,8 @@ class ComposableRevealInternalElement extends SkyflowElement { #elementId: string; + resizeObserver: ResizeObserver | null; + #readyToMount: boolean = false; #eventEmitter: EventEmitter; @@ -83,6 +85,7 @@ class ComposableRevealInternalElement extends SkyflowElement { super(); this.#elementId = elementId; this.#metaData = metaData; + this.resizeObserver = null; this.#clientId = this.#metaData?.uuid; this.#isSingleElementAPI = isSingleElementAPI; this.#recordData = recordGroup; @@ -185,6 +188,20 @@ class ComposableRevealInternalElement extends SkyflowElement { if (!domElementSelector) { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['RevealElement'], true); } + + if(domElementSelector instanceof HTMLElement){ + this.resizeObserver = new ResizeObserver(() => { + if (domElementSelector.getElementsByTagName('iframe')[0]?.contentWindow) { + const iframeElement = domElementSelector.getElementsByTagName('iframe')[0] + if(iframeElement.name === this.#iframe.name){ + iframeElement?.contentWindow?.postMessage({ + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, properties.IFRAME_SECURE_ORIGIN); + } + } + }); + } + updateMetricObjectValue(this.#elementId, METRIC_TYPES.DIV_ID, domElementSelector); if ( this.#metaData?.clientJSON?.config?.options?.trackMetrics @@ -193,6 +210,15 @@ class ComposableRevealInternalElement extends SkyflowElement { pushElementEventWithTimeout(this.#elementId); } + if (typeof domElementSelector === 'string') { + const targetElement = document.querySelector(domElementSelector); + if (targetElement) { + this.resizeObserver?.observe(targetElement); + } + } else if (domElementSelector instanceof HTMLElement) { + this.resizeObserver?.observe(domElementSelector); + } + this.#readyToMount = true; if (this.#readyToMount) { this.#iframe.mount(domElementSelector, undefined, { From 159dda9097e050a3a700de405f269c593362fdc6 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 09:04:49 +0000 Subject: [PATCH 35/47] [AUTOMATED] Release - 2.5.0-beta.9-dev.fcfbd99 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c086c343..325b02da 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.9-dev.99fa815", + "version": "2.5.0-beta.9-dev.fcfbd99", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 9a7a6cc2184b8ad7f3c9d374a719621c42993baf Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 16:13:36 +0530 Subject: [PATCH 36/47] SK-2254: add resize observer for multiple iframes --- src/core/external/collect/collect-element.ts | 20 +++++++++++++------ .../reveal/composable-reveal-internal.ts | 19 ++++++++++++------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index 84d76c88..5cc87639 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -208,12 +208,20 @@ class CollectElement extends SkyflowElement { if (domElement instanceof HTMLElement) { this.resizeObserver = new ResizeObserver(() => { - if (domElement.getElementsByTagName('iframe')[0]?.contentWindow) { - const iframeElement = domElement.getElementsByTagName('iframe')[0]; - if (iframeElement.name === this.#iframe.name) { - iframeElement?.contentWindow?.postMessage({ - name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - }, properties.IFRAME_SECURE_ORIGIN); + const iframeElements = domElement.getElementsByTagName('iframe'); + // eslint-disable-next-line no-plusplus + for (let i = 0; i < iframeElements.length; i++) { + const iframeElement = iframeElements[i]; + if ( + iframeElement.name === this.#iframe.name + && iframeElement.contentWindow + ) { + iframeElement?.contentWindow?.postMessage( + { + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, + properties.IFRAME_SECURE_ORIGIN, + ); } } }); diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts index 2e5a2cbf..f9b26bc6 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -191,12 +191,19 @@ class ComposableRevealInternalElement extends SkyflowElement { if(domElementSelector instanceof HTMLElement){ this.resizeObserver = new ResizeObserver(() => { - if (domElementSelector.getElementsByTagName('iframe')[0]?.contentWindow) { - const iframeElement = domElementSelector.getElementsByTagName('iframe')[0] - if(iframeElement.name === this.#iframe.name){ - iframeElement?.contentWindow?.postMessage({ - name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - }, properties.IFRAME_SECURE_ORIGIN); + const iframeElements = domElementSelector.getElementsByTagName('iframe'); + for (let i = 0; i < iframeElements.length; i++) { + const iframeElement = iframeElements[i]; + if ( + iframeElement.name === this.#iframe.name && + iframeElement.contentWindow + ) { + iframeElement?.contentWindow?.postMessage( + { + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, + properties.IFRAME_SECURE_ORIGIN + ); } } }); From 1170930ae81585d65620ebb8154996792ebb8324 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 10:44:46 +0000 Subject: [PATCH 37/47] [AUTOMATED] Release - 2.5.0-beta.9-dev.ff71850 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 325b02da..a818b825 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.9-dev.fcfbd99", + "version": "2.5.0-beta.9-dev.ff71850", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 2328f56387dc6a0a7f24157b6d9a9057aa781dd8 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 16:57:19 +0530 Subject: [PATCH 38/47] SK-2254: add undefined check for resize observer for multiple iframes --- src/core/external/collect/collect-element.ts | 28 ++++++++++--------- .../reveal/composable-reveal-internal.ts | 26 +++++++++-------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index 5cc87639..7856b62c 100644 --- a/src/core/external/collect/collect-element.ts +++ b/src/core/external/collect/collect-element.ts @@ -209,19 +209,21 @@ class CollectElement extends SkyflowElement { if (domElement instanceof HTMLElement) { this.resizeObserver = new ResizeObserver(() => { const iframeElements = domElement.getElementsByTagName('iframe'); - // eslint-disable-next-line no-plusplus - for (let i = 0; i < iframeElements.length; i++) { - const iframeElement = iframeElements[i]; - if ( - iframeElement.name === this.#iframe.name - && iframeElement.contentWindow - ) { - iframeElement?.contentWindow?.postMessage( - { - name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - }, - properties.IFRAME_SECURE_ORIGIN, - ); + if (iframeElements && iframeElements.length > 0) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < iframeElements.length; i++) { + const iframeElement = iframeElements[i]; + if ( + iframeElement.name === this.#iframe.name + && iframeElement.contentWindow + ) { + iframeElement?.contentWindow?.postMessage( + { + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, + properties.IFRAME_SECURE_ORIGIN, + ); + } } } }); diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts index f9b26bc6..9453a06a 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -192,18 +192,20 @@ class ComposableRevealInternalElement extends SkyflowElement { if(domElementSelector instanceof HTMLElement){ this.resizeObserver = new ResizeObserver(() => { const iframeElements = domElementSelector.getElementsByTagName('iframe'); - for (let i = 0; i < iframeElements.length; i++) { - const iframeElement = iframeElements[i]; - if ( - iframeElement.name === this.#iframe.name && - iframeElement.contentWindow - ) { - iframeElement?.contentWindow?.postMessage( - { - name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, - }, - properties.IFRAME_SECURE_ORIGIN - ); + if (iframeElements && iframeElements.length > 0) { + for (let i = 0; i < iframeElements.length; i++) { + const iframeElement = iframeElements[i]; + if ( + iframeElement.name === this.#iframe.name && + iframeElement.contentWindow + ) { + iframeElement?.contentWindow?.postMessage( + { + name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, + }, + properties.IFRAME_SECURE_ORIGIN + ); + } } } }); From 844a1517ebdccf78dbd0c65a8fa305a5115a6d04 Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 11:28:25 +0000 Subject: [PATCH 39/47] [AUTOMATED] Release - 2.5.0-beta.9-dev.3d1ed74 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a818b825..d677e338 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.9-dev.ff71850", + "version": "2.5.0-beta.9-dev.3d1ed74", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From df87b1a098d69df3093a02ecb4be6b6e6d19da4d Mon Sep 17 00:00:00 2001 From: saileshwar-skyflow Date: Tue, 19 Aug 2025 18:25:59 +0000 Subject: [PATCH 40/47] [AUTOMATED] Release - 2.5.0-beta.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d677e338..6d849e7a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.9-dev.3d1ed74", + "version": "2.5.0-beta.10", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 31f06b9fa2b099d76af31266d2fc4221be55d3fc Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 16 Oct 2025 18:39:26 +0530 Subject: [PATCH 41/47] SK-2330 fix the lint error --- src/core/internal/frame-element-init.ts | 7 +++++-- tests/core/external/collect/composable-container.test.js | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/internal/frame-element-init.ts b/src/core/internal/frame-element-init.ts index a3b033ca..c615d1e9 100644 --- a/src/core/internal/frame-element-init.ts +++ b/src/core/internal/frame-element-init.ts @@ -464,11 +464,14 @@ export default class FrameElementInit { // eslint-disable-next-line consistent-return private multipleUploadFiles = - (fileElement: IFrameFormElement, clientConfig, metaData) => new Promise((rootResolve, rootReject) => { + (fileElement: IFrameFormElement, + clientConfig, metaData) => new Promise((rootResolve, rootReject) => { this.#client = new Client(clientConfig, {}); if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); - const { state, tableName, onFocusChange, preserveFileName } = fileElement; + const { + state, tableName, onFocusChange, preserveFileName, + } = fileElement; if (state.isRequired) { onFocusChange(false); } diff --git a/tests/core/external/collect/composable-container.test.js b/tests/core/external/collect/composable-container.test.js index c77561f2..2977ccf1 100644 --- a/tests/core/external/collect/composable-container.test.js +++ b/tests/core/external/collect/composable-container.test.js @@ -18,7 +18,7 @@ const bus = require('framebus'); iframerUtils.getIframeSrc = jest.fn(() => ('https://google.com')); -const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve()); +const getBearerToken = jest.fn().mockImplementation(() => Promise.resolve('token')); const mockUuid = '1234'; jest.mock('../../../../src/libs/uuid', () => ({ @@ -57,6 +57,7 @@ EventEmitter.mockImplementation(()=>({ const metaData = { + getSkyflowBearerToken: getBearerToken, skyflowContainer:{ isControllerFrameReady: true }, @@ -78,6 +79,7 @@ const metaData = { }, }; const metaData2 = { + getSkyflowBearerToken: getBearerToken, skyflowContainer:{ isControllerFrameReady: false }, From 17347101a103f81d8937a8afe4722ada5ec8b78f Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 16 Oct 2025 13:10:13 +0000 Subject: [PATCH 42/47] [AUTOMATED] Release - 2.5.0-beta.10-dev.08ce547 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d849e7a..5f39e47d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.10", + "version": "2.5.0-beta.10-dev.08ce547", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 379940c1fdcb6cbd3e45f624c043d7eccd44add9 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 30 Oct 2025 04:24:12 +0000 Subject: [PATCH 43/47] [AUTOMATED] Release - 2.4.4-dev.9309abf --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f39e47d..63e8e772 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.5.0-beta.10-dev.08ce547", + "version": "2.4.4-dev.9309abf", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From af7492239304d500781c81843d1b99952801a9fd Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 30 Oct 2025 10:15:07 +0530 Subject: [PATCH 44/47] SK-2360 fix errors --- .../external/reveal/composable-reveal-internal.ts | 14 +++++++------- src/core/internal/internal-types/index.ts | 2 +- src/skyflow.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/core/external/reveal/composable-reveal-internal.ts b/src/core/external/reveal/composable-reveal-internal.ts index 9453a06a..d2e65238 100644 --- a/src/core/external/reveal/composable-reveal-internal.ts +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -111,7 +111,6 @@ class ComposableRevealInternalElement extends SkyflowElement { this.#iframe?.setIframeHeight(data?.height); }); - window?.addEventListener('message', (event) => { if (event?.data?.type === ELEMENT_EVENTS_TO_IFRAME.HEIGHT_CALLBACK + this.#iframe?.name) { this.#iframe?.setIframeHeight(event?.data?.data?.height); @@ -189,21 +188,22 @@ class ComposableRevealInternalElement extends SkyflowElement { throw new SkyflowError(SKYFLOW_ERROR_CODE.EMPTY_ELEMENT_IN_MOUNT, ['RevealElement'], true); } - if(domElementSelector instanceof HTMLElement){ + if (domElementSelector instanceof HTMLElement) { this.resizeObserver = new ResizeObserver(() => { const iframeElements = domElementSelector.getElementsByTagName('iframe'); if (iframeElements && iframeElements.length > 0) { - for (let i = 0; i < iframeElements.length; i++) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < iframeElements.length; i++) { const iframeElement = iframeElements[i]; if ( - iframeElement.name === this.#iframe.name && - iframeElement.contentWindow + iframeElement.name === this.#iframe.name + && iframeElement.contentWindow ) { iframeElement?.contentWindow?.postMessage( { name: ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, }, - properties.IFRAME_SECURE_ORIGIN + properties.IFRAME_SECURE_ORIGIN, ); } } @@ -386,7 +386,7 @@ class ComposableRevealInternalElement extends SkyflowElement { MessageType.LOG, loglevel); validateRenderElementRecord(recordData); - window.addEventListener('message', (event) => { + window.addEventListener('message', (event) => { if (event.data.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + recordData?.name) { this.#isMounted = true; diff --git a/src/core/internal/internal-types/index.ts b/src/core/internal/internal-types/index.ts index 0b71577f..fd594d78 100644 --- a/src/core/internal/internal-types/index.ts +++ b/src/core/internal/internal-types/index.ts @@ -78,7 +78,7 @@ export interface ClientMetadata { } export interface Metadata extends ClientMetadata { - getSkyflowBearerToken: () => Promise | undefined; + getSkyflowBearerToken: () => Promise; clientJSON: ClientToJSON; containerType: ContainerType; skyflowContainer: SkyflowContainer; diff --git a/src/skyflow.ts b/src/skyflow.ts index 456509f0..92e63db7 100644 --- a/src/skyflow.ts +++ b/src/skyflow.ts @@ -175,7 +175,7 @@ class Skyflow { return skyflow; } - #getSkyflowBearerToken = () => new Promise((resolve, reject) => { + #getSkyflowBearerToken: () => Promise = () => new Promise((resolve, reject) => { if ( this.#client.config.getBearerToken && (!this.#bearerToken || !isTokenValid(this.#bearerToken)) From e37a584e45147b563802fa2442230ea6879cd2a5 Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 30 Oct 2025 04:45:47 +0000 Subject: [PATCH 45/47] [AUTOMATED] Release - 2.4.4-dev.af74922 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63e8e772..aaf05319 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.4-dev.9309abf", + "version": "2.4.4-dev.af74922", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js", From 761cf54e36c44eef29dd86f14fe23df02fc48e2c Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 30 Oct 2025 10:28:57 +0530 Subject: [PATCH 46/47] SK-2360 fix errors --- src/core-utils/collect.ts | 8 ++++---- .../internal/composable-frame-element-init.ts | 5 ++++- src/core/internal/frame-element-init.ts | 15 ++++++++++++--- src/core/internal/internal-types/index.ts | 2 +- src/core/internal/reveal/reveal-frame.ts | 5 ++++- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/core-utils/collect.ts b/src/core-utils/collect.ts index a0fbb98d..47384540 100644 --- a/src/core-utils/collect.ts +++ b/src/core-utils/collect.ts @@ -347,9 +347,9 @@ export const insertDataInCollect = async ( let insertErrorResponse: any; client ?.request({ - body: { + body: JSON.stringify({ records, - }, + }), requestMethod: 'POST', url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, headers: { @@ -391,9 +391,9 @@ export const insertDataInMultipleFiles = async ( let insertErrorResponse: any; client ?.request({ - body: { + body: JSON.stringify({ records, - }, + }), requestMethod: 'POST', url: `${client.config.vaultURL}/v1/vaults/${client.config.vaultID}`, headers: { diff --git a/src/core/internal/composable-frame-element-init.ts b/src/core/internal/composable-frame-element-init.ts index 51210e5a..9f97a0d9 100644 --- a/src/core/internal/composable-frame-element-init.ts +++ b/src/core/internal/composable-frame-element-init.ts @@ -68,7 +68,10 @@ export default class RevealComposableFrameElementInit { const data = event?.data?.data ?? {}; const elementIds = data?.elementIds ?? []; const revealDataInput: IRevealRecordComposable[] = []; - this.#client = new Client(event?.data?.clientConfig ?? {}, {}); + this.#client = new Client(event?.data?.clientConfig ?? {}, { + uuid: '', + clientDomain: '', + }); elementIds?.forEach((element) => { this.revealFrameList?.forEach((revealFrame) => { diff --git a/src/core/internal/frame-element-init.ts b/src/core/internal/frame-element-init.ts index c615d1e9..342b05f8 100644 --- a/src/core/internal/frame-element-init.ts +++ b/src/core/internal/frame-element-init.ts @@ -187,7 +187,10 @@ export default class FrameElementInit { }); uploadFiles = (fileElement, clientConfig) => { - this.#client = new Client(clientConfig, {}); + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); const fileUploadObject: any = {}; @@ -391,7 +394,10 @@ export default class FrameElementInit { error: error?.message, }); } - this.#client = new Client(clientConfig, {}); + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); const client = this.#client; const sendRequest = () => new Promise((rootResolve, rootReject) => { const insertPromiseSet: Promise[] = []; @@ -466,7 +472,10 @@ export default class FrameElementInit { private multipleUploadFiles = (fileElement: IFrameFormElement, clientConfig, metaData) => new Promise((rootResolve, rootReject) => { - this.#client = new Client(clientConfig, {}); + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); if (!this.#client) throw new SkyflowError(SKYFLOW_ERROR_CODE.CLIENT_CONNECTION, [], true); const { diff --git a/src/core/internal/internal-types/index.ts b/src/core/internal/internal-types/index.ts index fd594d78..2dbcb7ac 100644 --- a/src/core/internal/internal-types/index.ts +++ b/src/core/internal/internal-types/index.ts @@ -78,8 +78,8 @@ export interface ClientMetadata { } export interface Metadata extends ClientMetadata { - getSkyflowBearerToken: () => Promise; clientJSON: ClientToJSON; containerType: ContainerType; skyflowContainer: SkyflowContainer; + getSkyflowBearerToken: () => Promise; } diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index 4476eded..b45b3d78 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -413,7 +413,10 @@ class RevealFrame { }; private renderFile(data: IRevealRecord, clientConfig) { - this.#client = new Client(clientConfig, {}); + this.#client = new Client(clientConfig, { + uuid: '', + clientDomain: '', + }); return new Promise((resolve, reject) => { try { getFileURLFromVaultBySkyflowIDComposable(data, this.#client, clientConfig.authToken) From 6083e84cd0d4977667e17e893f1d9e595652addf Mon Sep 17 00:00:00 2001 From: skyflow-bharti Date: Thu, 30 Oct 2025 04:59:41 +0000 Subject: [PATCH 47/47] [AUTOMATED] Release - 2.4.4-dev.761cf54 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aaf05319..c9b178f0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "skyflow-js", "preferGlobal": true, "analyze": false, - "version": "2.4.4-dev.af74922", + "version": "2.4.4-dev.761cf54", "author": "Skyflow", "description": "Skyflow JavaScript SDK", "homepage": "https://github.com/skyflowapi/skyflow-js",