diff --git a/integration-libs/opf/base/root/services/opf-resource-loader.service.spec.ts b/integration-libs/opf/base/root/services/opf-resource-loader.service.spec.ts index 76b77c4b9d4..b4d0c8bcda0 100644 --- a/integration-libs/opf/base/root/services/opf-resource-loader.service.spec.ts +++ b/integration-libs/opf/base/root/services/opf-resource-loader.service.spec.ts @@ -50,11 +50,13 @@ describe('OpfResourceLoaderService', () => { const mockScriptResource = { url: 'script-url', type: OpfDynamicScriptResourceType.SCRIPT, + attributes: [{ key: 'opf-load-once', value: 'true' }], }; const mockStyleResource = { url: 'style-url', type: OpfDynamicScriptResourceType.STYLES, + attributes: [{ key: 'opf-load-once', value: 'true' }], }; spyOn(opfResourceLoaderService, 'loadScript').and.callThrough(); diff --git a/integration-libs/opf/base/root/services/opf-resource-loader.service.ts b/integration-libs/opf/base/root/services/opf-resource-loader.service.ts index c9020a9d3e3..5bb3fb4e9d7 100644 --- a/integration-libs/opf/base/root/services/opf-resource-loader.service.ts +++ b/integration-libs/opf/base/root/services/opf-resource-loader.service.ts @@ -22,11 +22,12 @@ export class OpfResourceLoaderService { protected document = inject(DOCUMENT); protected platformId = inject(PLATFORM_ID); - protected readonly OPF_RESOURCE_ATTRIBUTE_KEY = 'data-opf-resource'; protected readonly CORS_DEFAULT_VALUE = 'anonymous'; + protected readonly OPF_RESOURCE_LOAD_ONCE_ATTRIBUTE_KEY = 'opf-load-once'; + protected readonly OPF_RESOURCE_ATTRIBUTE_KEY = 'data-opf-resource'; protected embedStyles(embedOptions: { - attributes?: OpfKeyValueMap[]; + attributes?: { [key: string]: string }; src: string; sri?: string; callback?: EventListener; @@ -38,22 +39,18 @@ export class OpfResourceLoaderService { link.href = src; link.rel = 'stylesheet'; link.type = 'text/css'; - link.setAttribute(this.OPF_RESOURCE_ATTRIBUTE_KEY, 'true'); if (sri) { link.integrity = sri; - const corsKeyvalue = attributes?.find( - (attr) => attr.key === 'crossorigin' && !!attr.value?.length - ); - link.crossOrigin = corsKeyvalue?.value ?? this.CORS_DEFAULT_VALUE; + link.crossOrigin = attributes?.['crossorigin'] ?? this.CORS_DEFAULT_VALUE; + delete attributes?.['crossorigin']; } - if (attributes?.length) { - attributes.forEach((attribute) => { - const { key, value } = attribute; + + attributes && + Object.keys(attributes)?.forEach((key) => { if (!(key in link)) { - link.setAttribute(key, value); + link.setAttribute(key, attributes[key as keyof object]); } }); - } if (callback) { link.addEventListener('load', callback); @@ -74,39 +71,53 @@ export class OpfResourceLoaderService { return this.scriptLoader.hasScript(src); } + /** + * Create attributes intended to script and link elements. + * + * Return attributes list including keyValueList and OPF specific attribute with below logic: + * + * 1. Resource loads only once: 'opf-load-once' key detected, no additional attribute added. + * 2. Resource deleted at page/payment change: 'data-opf-resource' attribute is added. + */ + + protected createAttributesList(keyValueList?: OpfKeyValueMap[] | undefined): { + [key: string]: string; + } { + const attributes: { [key: string]: string } = {}; + keyValueList?.forEach((keyValue: OpfKeyValueMap) => { + attributes[keyValue.key] = keyValue.value; + }); + if (attributes?.[this.OPF_RESOURCE_LOAD_ONCE_ATTRIBUTE_KEY] === 'true') { + attributes[this.OPF_RESOURCE_ATTRIBUTE_KEY] = 'true'; + } + delete attributes?.[this.OPF_RESOURCE_LOAD_ONCE_ATTRIBUTE_KEY]; + return attributes; + } + /** * Loads a script specified in the resource object. * * The returned Promise is resolved when the script is loaded or already present. * The returned Promise is rejected when a loading error occurs. */ + protected loadScript(resource: OpfDynamicScriptResource): Promise { return new Promise((resolve, reject) => { - const attributes: any = { + const attributes: { [key: string]: string } = { type: 'text/javascript', - [this.OPF_RESOURCE_ATTRIBUTE_KEY]: true, + ...this.createAttributesList(resource.attributes), }; if (resource?.sri) { attributes['integrity'] = resource.sri; - const corsKeyvalue: OpfKeyValueMap | undefined = - resource?.attributes?.find( - (attr) => attr.key === 'crossorigin' && !!attr.value?.length - ); attributes['crossOrigin'] = - corsKeyvalue?.value ?? this.CORS_DEFAULT_VALUE; - } - - if (resource.attributes) { - resource.attributes.forEach((attribute) => { - attributes[attribute.key] = attribute.value; - }); + attributes?.['crossorigin'] ?? this.CORS_DEFAULT_VALUE; + delete attributes?.['crossorigin']; } - - if (resource.url && !this.hasScript(resource.url)) { + if (resource?.url && !this.hasScript(resource.url)) { this.scriptLoader.embedScript({ + attributes, src: resource.url, - attributes: attributes, callback: () => resolve(), errorCallback: () => reject(), disableKeyRestriction: true, @@ -123,11 +134,12 @@ export class OpfResourceLoaderService { * The returned Promise is resolved when the stylesheet is loaded or already present. * The returned Promise is rejected when a loading error occurs. */ + protected loadStyles(resource: OpfDynamicScriptResource): Promise { return new Promise((resolve, reject) => { if (resource.url && !this.hasStyles(resource.url)) { this.embedStyles({ - attributes: resource?.attributes, + attributes: this.createAttributesList(resource?.attributes), src: resource.url, sri: resource?.sri, callback: () => resolve(), @@ -167,6 +179,7 @@ export class OpfResourceLoaderService { * The returned Promise is resolved when all resources are loaded. * The returned Promise is also resolved (not rejected!) immediately when any loading error occurs. */ + loadResources( scripts: OpfDynamicScriptResource[] = [], styles: OpfDynamicScriptResource[] = []