diff --git a/package.json b/package.json index 552645f6..407a89a9 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.5-dev.fc93609", "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..4cd68318 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,103 @@ 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 as string, options) + .then((resolvedResult: any) => { + const resp = constructFinalUpdateRecordResponse( + resolvedResult, options?.tokens, skyflowIdRecord, + ); + resolve(resp); + }, + (rejectedResult) => { + let errorResponse = rejectedResult; + if (rejectedResult && 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) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + 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..5c533331 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,63 @@ 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 ? tokenRecord?.redaction + : RedactionType.PLAIN_TEXT; + // eslint-disable-next-line max-len + getTokenRecordsFromVault(tokenRecord?.token as string, redaction, client, authToken as string) + .then( + (response: IApiSuccessResponse) => { + const fieldsData = formatForPureJsSuccess(response); + apiResponse.push({ + ...fieldsData, + frameId: tokenRecord?.iframeName, // Add iframeName to the response + }); + }, + (cause: any) => { + const errorData = formatForPureJsFailure(cause, tokenRecord?.token as string); + printLog(errorData.error?.description || '', MessageType.ERROR, LogLevel.ERROR); + apiResponse.push({ + ...errorData, + frameId: tokenRecord?.iframeName, // Add iframeName to the error response + }); + }, + ) + .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 +362,29 @@ 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 && response.records.length > 0) { + 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..d6ca610f 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,17 @@ export const ELEMENT_EVENTS_TO_CLIENT = { }; export const ELEMENT_EVENTS_TO_IFRAME = { + RENDER_MOUNTED: 'RENDER_MOUNTED', + HEIGHT_CALLBACK: 'HEIGHT_CALLBACK', + HEIGHT_CALLBACK_COMPOSABLE: 'HEIGHT_CALLBACK_COMPOSABLE', + COMPOSABLE_REVEAL: 'COMPOSABLE_REVEAL', 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', diff --git a/src/core/external/collect/collect-element.ts b/src/core/external/collect/collect-element.ts index 298aa5d2..87653508 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); } @@ -525,7 +525,6 @@ class CollectElement extends SkyflowElement { 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) => { @@ -537,6 +536,11 @@ class CollectElement extends SkyflowElement { ...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 { diff --git a/src/core/external/collect/compose-collect-container.ts b/src/core/external/collect/compose-collect-container.ts index 4a9f0480..01b9ed29 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; @@ -115,26 +123,22 @@ class ComposableContainer extends Container { create = (input: CollectElementInput, options: CollectElementOptions = { required: false, }) => { - validateCollectElementInput(input, this.#context.logLevel); - const validations = formatValidations(input.validations); - const formattedOptions = formatOptions(input.type, options, this.#context.logLevel); - // let elementName; - // elementName = `${input.table}.${input.column}:${btoa(uuid())}`; - // elementName = (input.table && input.column) ? `${input.type}:${btoa( - // elementName, - // )}` : ; - - const elementName = `${FRAME_ELEMENT}:${input.type}:${btoa(uuid())}`; - - this.#elementsList.push({ - elementType: input.type, - name: input.column, + validateCollectElementInput(input, this.#context?.logLevel); + const validations = formatValidations(input?.validations); + const formattedOptions = formatOptions(input?.type, options, this.#context?.logLevel); + const elementName = `${FRAME_ELEMENT}:${input?.type}:${btoa(uuid())}`; + + this.#elementsList?.push({ + elementType: input?.type, + name: input?.column, ...input, ...formattedOptions, validations, elementName, }); - const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}:${this.#containerId}:${this.#context.logLevel}:${btoa(this.#clientDomain)}`; + + const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements ?? {})}:${this.#containerId}:${this.#context?.logLevel}:${btoa(this.#clientDomain ?? '')}`; + this.#iframeID = controllerIframeName; return new ComposableElement(elementName, this.#eventEmitter, controllerIframeName); }; @@ -144,37 +148,37 @@ class ComposableContainer extends Container { ) => { const elements: any[] = []; this.#tempElements = deepClone(multipleElements); - this.#tempElements.rows.forEach((row) => { - row.elements.forEach((element) => { - const options = element; + + this.#tempElements?.rows?.forEach((row) => { + row?.elements?.forEach((element) => { + const options = element ?? {}; const { elementType } = options; validateElementOptions(elementType, options); - options.sensitive = options.sensitive || ELEMENTS[elementType].sensitive; - options.replacePattern = options.replacePattern || ELEMENTS[elementType].replacePattern; - options.mask = options.mask || ELEMENTS[elementType].mask; - + options.sensitive = options?.sensitive ?? ELEMENTS[elementType]?.sensitive; + options.replacePattern = options?.replacePattern ?? ELEMENTS[elementType]?.replacePattern; + options.mask = options?.mask ?? ELEMENTS[elementType]?.mask; options.isMounted = false; - - options.label = element.label; - options.skyflowID = element.skyflowID; + options.label = element?.label; + options.skyflowID = element?.skyflowID; elements.push(options); }); }); this.#tempElements.elementName = isSingleElementAPI - ? elements[0].elementName - : `${FRAME_ELEMENT}:group:${btoa(this.#tempElements)}`; + ? elements[0]?.elementName + : `${FRAME_ELEMENT}:group:${btoa(JSON.stringify(this.#tempElements ?? {}))}`; + if ( isSingleElementAPI - && !this.#elements[elements[0].elementName] - && this.#hasElementName(elements[0].name) + && !this.#elements?.[elements[0]?.elementName] + && this.#hasElementName(elements[0]?.name) ) { - throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0].name}`], true); + throw new SkyflowError(SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, [`${elements[0]?.name}`], true); } - let element = this.#elements[this.#tempElements.elementName]; + let element = this.#elements?.[this.#tempElements?.elementName]; if (element) { if (isSingleElementAPI) { element.update(elements[0]); @@ -301,174 +305,216 @@ class ComposableContainer extends Container { 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(); }; - 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) => { + this.#eventEmitter?.on(ELEMENT_EVENTS_TO_IFRAME.COMPOSABLE_UPDATE_OPTIONS, (data) => { let elementIndex; - const elementList = this.#elementsList.map((element, index) => { - if (element.elementName === data.elementName) { + const elementList = this.#elementsList?.map((element, index) => { + if (element?.elementName === data?.elementName) { elementIndex = index; return { - elementName: element.elementName, - ...data.elementOptions, + elementName: element?.elementName, + ...data?.elementOptions, }; } return element; }); if (this.#containerElement) { - this.#containerElement.updateElement({ - ...elementList[elementIndex], + this.#containerElement?.updateElement({ + ...elementList?.[elementIndex], }); } }); 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..a71afe34 --- /dev/null +++ b/src/core/external/reveal/composable-reveal-container.ts @@ -0,0 +1,482 @@ +/* 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'; + +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: any, metaData: any, skyflowElements: any, context: 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 }); + + this.#setupEventListeners(); + this.#initializeContainer(); + } + + #setupEventListeners(): void { + window.addEventListener('message', (event: MessageEvent) => { + if (event && event.data + && event.data?.type === ELEMENT_EVENTS_TO_CLIENT.MOUNTED + this.#containerId) { + this.#isComposableFrameReady = true; + } + }); + } + + #initializeContainer(): void { + printLog( + parameterizedString(logs.infoLogs.CREATE_COLLECT_CONTAINER, CLASS_NAME), + MessageType.LOG, + this.#context?.logLevel, + ); + this.#containerMounted = 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, + }); + + const controllerIframeName = `${FRAME_ELEMENT}:group:${btoa(this.#tempElements ?? {})}:${ + this.#containerId}:${this.#context?.logLevel}:${btoa(this.#clientDomain)}`; + + return new ComposableRevealElement( + elementName, this.#eventEmitter, controllerIframeName, this.#context, + ); + }; + + #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 + ? elements[0].elementName + : `${FRAME_ELEMENT}:group:${btoa(JSON.stringify(this.#tempElements ?? {}))}`; + + if ( + isSingleElementAPI + && !this.#elements?.[elements?.[0]?.elementName] + && elements?.[0]?.name + && this.#hasElementName(elements[0].name) + ) { + throw new SkyflowError( + SKYFLOW_ERROR_CODE.UNIQUE_ELEMENT_NAME, + [elements?.[0]?.name ?? 'unknown'], + true, + ); + } + + let element = this.#elements?.[this.#tempElements?.elementName]; + + if (element) { + if (isSingleElementAPI && elements?.[0]) { + 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, + ); + + if (this.#tempElements?.elementName) { + 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..2c4707c7 --- /dev/null +++ b/src/core/external/reveal/composable-reveal-element.ts @@ -0,0 +1,85 @@ +import EventEmitter from '../../../event-emitter'; +import { ContainerType } from '../../../skyflow'; +import { + Context, + EventName, MessageType, RenderFileResponse, +} from '../../../utils/common'; +import { printLog } from '../../../utils/logs-helper'; +import { ELEMENT_EVENTS_TO_IFRAME } from '../../constants'; + +class ComposableRevealElement { + #elementName: string; + + #eventEmitter: EventEmitter; + + #iframeName: string; + + type: string = ContainerType.COMPOSABLE; + + #isMounted: boolean = false; + + #context: Context; + + constructor(name, eventEmitter, iframeName, context: Context) { + this.#elementName = name ?? ''; + this.#iframeName = iframeName ?? ''; + this.#eventEmitter = eventEmitter; + this.#context = context; + + this.#setupEventListeners(); + } + + #setupEventListeners(): void { + try { + this.#eventEmitter?.on?.( + `${EventName?.READY}:${this.#elementName}`, + () => { + this.#isMounted = true; + }, + ); + } catch (error) { + printLog( + 'Failed to setup event listeners', + MessageType?.LOG ?? 'LOG', + this.#context?.logLevel, + ); + } + } + + iframeName(): string { + return this.#iframeName ?? ''; + } + + getID(): string { + return this.#elementName ?? ''; + } + + renderFile(): Promise { + return new Promise((resolve, reject) => { + try { + const eventName = `${ELEMENT_EVENTS_TO_IFRAME?.RENDER_FILE_REQUEST ?? ''}:${this.#elementName}`; + + // eslint-disable-next-line no-underscore-dangle + this.#eventEmitter?._emit?.( + eventName, + {}, + (response: RenderFileResponse) => { + if (response?.errors) { + reject(response); + } else { + resolve(response); + } + }, + ); + } catch (error: any) { + reject(error); + } + }); + } + + isMounted(): boolean { + return this.#isMounted; + } +} + +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..7608d189 --- /dev/null +++ b/src/core/external/reveal/composable-reveal-internal.ts @@ -0,0 +1,584 @@ +/* +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'; + +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; + 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; + bus.on(ELEMENT_EVENTS_TO_CLIENT.HEIGHT + this.#iframe.name, (data) => { + this.#iframe.setIframeHeight(data.height); + }); + window.addEventListener('message', (event) => { + if (event && event.data && event.data?.type === ELEMENT_EVENTS_TO_IFRAME.RENDER_MOUNTED + + this.#containerId) { + this.#isComposableFrameReady = true; + } + }); + window.addEventListener('message', (event) => { + if (event.data + && 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 && 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) => { + row.elements?.forEach((element: any) => { + 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 }); + }); + }, + ); + }); + }); + } 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; + if (iframe?.contentWindow) { + iframe.contentWindow.postMessage({ + name: eventName, + ...options, + }, properties.IFRAME_SECURE_ORIGIN); + } + } else { + const iframe = document.getElementById(this.#iframe.name) as HTMLIFrameElement; + if (iframe?.contentWindow) { + 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) { + printLog(parameterizedString( + logs.errorLogs.FAILED_RENDER, + ), MessageType.ERROR, + this.#context.logLevel); + if (Object.prototype.hasOwnProperty.call(recordData, 'altText')) { + this.setAltText(altText); + } + reject(revealData); + } 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) { + this.#recordData = { + ...this.#recordData, + ...options, + }; + + 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.ELEMENT_PROPS, + updatedValue: options, + }); + } 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.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..d00a4f8c --- /dev/null +++ b/src/core/internal/composable-frame-element-init.ts @@ -0,0 +1,347 @@ +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 && event.data + && 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); + } + } + }); + }); + + if (revealDataInput?.length > 0) { + 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, + ); + + if (error?.records) { + error?.records?.forEach((record: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === record?.frameId) { + revealFrame?.responseUpdate?.(record); + } + }); + }); + } + + error?.errors?.forEach((error: any) => { + this.revealFrameList?.forEach((revealFrame) => { + if (revealFrame?.getData()?.name === error?.frameId) { + revealFrame?.responseUpdate?.(error); + } + }); + }); + }); + } + } + }); + } + + 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]: { + // ...rowStylesByClassName, + // alignItems: rowStylesByClassName['align-items'], + // justifyContent: rowStylesByClassName['justify-content'], + ...(styles && 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 && errorTextStyles[STYLE_TYPE.BASE]), + }, + }; + getCssClassesFromJss(errorStyles, 'row-error'); + if (errorTextStyles && 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) => { + if (!element) return; + + 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, + }); + }); + + // Height callback event listeners + window?.addEventListener('message', (event) => { + if (event && event.data + && 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, name: window.name }, + }, + this.clientMetaData.clientDomain, + ); + } + if (event && event.data + && 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, 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..5325d5b4 100644 --- a/src/core/internal/frame-element-init.ts +++ b/src/core/internal/frame-element-init.ts @@ -1,17 +1,33 @@ 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, +} 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'; + +const set = require('set-value'); export default class FrameElementInit { iframeFormElement: IFrameFormElement | undefined; @@ -30,6 +46,14 @@ export default class FrameElementInit { group: any; + frameList: FrameElement[] = []; + + iframeFormList: IFrameFormElement[] = []; + + #client!: Client; + + #context!: Context; + constructor() { // this.createIframeElement(frameName, label, skyflowID, isRequired); this.context = { logLevel: LogLevel.ERROR, env: Env.PROD }; // client level @@ -41,8 +65,373 @@ 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) => { + this.#context = data.context; + data.client.config = { + ...data.client.config, + }; + this.#client = Client.fromJSON(data.client) as any; + }); + + window.addEventListener('message', this.handleCollectCall); } + private handleCollectCall = (event: MessageEvent) => { + // 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; + this.#context = data.context; + 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 + ) { + 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 + ) { + 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)); + }); + }; + updateGroupData = () => { const frameName = window.name; const url = window.location?.href; @@ -56,7 +445,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 +461,7 @@ export default class FrameElementInit { ...this.clientMetaData, isRequired, }, this.context, skyflowID); + this.iframeFormList.push(this.iframeFormElement); return this.iframeFormElement; }; @@ -184,7 +573,10 @@ 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; @@ -208,6 +600,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/index.ts b/src/core/internal/index.ts index 8648eb8c..2f3a5af6 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; @@ -454,6 +459,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()); }; diff --git a/src/core/internal/reveal/reveal-frame.ts b/src/core/internal/reveal/reveal-frame.ts index 5611c51b..300a9550 100644 --- a/src/core/internal/reveal/reveal-frame.ts +++ b/src/core/internal/reveal/reveal-frame.ts @@ -15,17 +15,23 @@ 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, } from '../../../utils/helpers'; +import { formatForRenderClient, getFileURLFromVaultBySkyflowIDComposable } from '../../../core-utils/reveal'; +import Client from '../../../client'; +import properties from '../../../properties'; const { getType } = require('mime'); @@ -65,6 +71,8 @@ class RevealFrame { #skyflowContainerId: string = ''; + #client!: Client; + static init() { const url = window.location?.href; const configIndex = url.indexOf('?'); @@ -75,9 +83,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 +99,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 +133,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 +149,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 +164,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)) { @@ -220,35 +271,167 @@ 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 && event.data + && 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 && event.data + && 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 @@ -355,7 +538,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 +553,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 +569,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/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/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..9ebe6ca9 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[]; } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e80fcf2c..12b866ae 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -4,6 +4,8 @@ Copyright (c) 2022 Skyflow, Inc. import logs from './logs'; const SKYFLOW_ERROR_CODE = { + 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..c53dec80 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.', @@ -91,6 +93,7 @@ const logs = { VALIDATE_GET_BY_ID_INPUT: '%s1 - Validating getByID input.', }, errorLogs: { + 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',