Skip to content

Commit 33d2bc6

Browse files
Add Support for action:// Links (#99)
* adds support for action:// links with tests * update type name
1 parent 671ff6f commit 33d2bc6

File tree

3 files changed

+159
-12
lines changed

3 files changed

+159
-12
lines changed

src/inapp/inapp.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -370,13 +370,12 @@ export function getInAppMessages(
370370
const clickedUrl = link.getAttribute('href') || '';
371371
const openInNewTab = link.getAttribute('target') === '_blank';
372372
const isIterableKeywordLink = !!clickedUrl.match(
373-
/itbl:\/\/|iterable:\/\/|action:\/\//gim
374-
);
375-
const isDismissNode = !!clickedUrl.match(
376-
/(itbl:\/\/|iterable:\/\/)dismiss/gim
373+
/iterable:\/\/|action:\/\//gim
377374
);
375+
const isDismissNode = !!clickedUrl.match(/iterable:\/\/dismiss/gim);
376+
const isActionLink = !!clickedUrl.match(/action:\/\//gim);
378377

379-
if (isDismissNode) {
378+
if (isDismissNode || isActionLink) {
380379
/*
381380
give the close anchor tag properties that make it
382381
behave more like a button with a logical aria label
@@ -402,7 +401,7 @@ export function getInAppMessages(
402401
/* swallow the network error */
403402
}).catch((e) => e);
404403

405-
if (isDismissNode) {
404+
if (isDismissNode || isActionLink) {
406405
dismissMessage(activeIframe, clickedUrl);
407406
overlay.remove();
408407
document.removeEventListener(
@@ -418,6 +417,21 @@ export function getInAppMessages(
418417
global.removeEventListener('resize', throttledResize);
419418
}
420419

420+
if (isActionLink) {
421+
const filteredMatch = (new RegExp(
422+
/^.*action:\/\/(.*)$/,
423+
'gmi'
424+
)?.exec(clickedUrl) || [])?.[1];
425+
/*
426+
just post the message to the window when clicking
427+
action:// links and early return
428+
*/
429+
return global.postMessage(
430+
{ type: 'iterable-action-link', data: filteredMatch },
431+
'*'
432+
);
433+
}
434+
421435
/*
422436
finally (since we're in an iframe), programatically click the link
423437
and send the user to where they need to go, only if it's not one

src/inapp/tests/inapp.test.ts

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ describe('getInAppMessages', () => {
355355
const secondCloseLink = (
356356
document.getElementById('iterable-iframe') as HTMLIFrameElement
357357
)?.contentWindow?.document.body?.querySelector(
358-
'a[href="action://close-second-iframe"]'
358+
'a[data-qa-original-link="action://close-second-iframe"]'
359359
);
360360
expect(secondCloseLink).not.toBe(null);
361361
expect(secondCloseLink).not.toBeUndefined();
@@ -683,5 +683,137 @@ describe('getInAppMessages', () => {
683683
);
684684
expect(secondCloseLink).toBeUndefined();
685685
});
686+
687+
it('should call global.postMessage when action:// link is clicked', async () => {
688+
const { request } = getInAppMessages(
689+
{ count: 10, packageName: 'my-lil-website' },
690+
true
691+
);
692+
await request();
693+
694+
const mockHandler = jest.fn();
695+
global.postMessage = mockHandler;
696+
697+
const iframe = document.getElementById(
698+
'iterable-iframe'
699+
) as HTMLIFrameElement;
700+
const element = iframe?.contentWindow?.document.body?.querySelector(
701+
'a[data-qa-original-link="action://close-first-iframe"]'
702+
) as Element;
703+
704+
const clickEvent = new MouseEvent('click');
705+
await element.dispatchEvent(clickEvent);
706+
expect(mockHandler).toHaveBeenCalledWith(
707+
{
708+
data: 'close-first-iframe',
709+
type: 'iterable-action-link'
710+
},
711+
'*'
712+
);
713+
expect(document.getElementById('iterable-iframe')).toBe(null);
714+
expect(
715+
JSON.parse(
716+
mockRequest.history.post.filter(
717+
(e) => !!e.url?.match(/InAppClick/gim)
718+
)[0].data
719+
).clickedUrl
720+
).toBe('action://close-first-iframe');
721+
});
722+
723+
it('should do nothing upon clicking itbl:// links', async () => {
724+
mockRequest.onGet('/inApp/getMessages').reply(200, {
725+
inAppMessages: [
726+
{
727+
...messages[0],
728+
content: {
729+
...messages[0].content,
730+
html: '<a href="itbl://whatever">profile</a>'
731+
}
732+
}
733+
]
734+
});
735+
736+
const { request } = getInAppMessages(
737+
{ count: 10, packageName: 'my-lil-website' },
738+
true
739+
);
740+
await request();
741+
742+
const iframe = document.getElementById(
743+
'iterable-iframe'
744+
) as HTMLIFrameElement;
745+
const element = iframe?.contentWindow?.document.body?.querySelector(
746+
'a[href="itbl://whatever"]'
747+
) as Element;
748+
749+
const clickEvent = new MouseEvent('click');
750+
await element.dispatchEvent(clickEvent);
751+
expect(document.getElementById('iterable-iframe')).not.toBe(null);
752+
expect(document.getElementById('iterable-iframe')).not.toBeUndefined();
753+
});
754+
755+
it('should do nothing upon clicking itbl://dismiss links', async () => {
756+
mockRequest.onGet('/inApp/getMessages').reply(200, {
757+
inAppMessages: [
758+
{
759+
...messages[0],
760+
content: {
761+
...messages[0].content,
762+
html: '<a href="itbl://dismiss">profile</a>'
763+
}
764+
}
765+
]
766+
});
767+
768+
const { request } = getInAppMessages(
769+
{ count: 10, packageName: 'my-lil-website' },
770+
true
771+
);
772+
await request();
773+
774+
const iframe = document.getElementById(
775+
'iterable-iframe'
776+
) as HTMLIFrameElement;
777+
const element = iframe?.contentWindow?.document.body?.querySelector(
778+
'a[href="itbl://dismiss"]'
779+
) as Element;
780+
781+
const clickEvent = new MouseEvent('click');
782+
await element.dispatchEvent(clickEvent);
783+
expect(document.getElementById('iterable-iframe')).not.toBe(null);
784+
expect(document.getElementById('iterable-iframe')).not.toBeUndefined();
785+
});
786+
787+
it('should do nothing upon clicking iterable:// non-dismiss links', async () => {
788+
mockRequest.onGet('/inApp/getMessages').reply(200, {
789+
inAppMessages: [
790+
{
791+
...messages[0],
792+
content: {
793+
...messages[0].content,
794+
html: '<a href="iterable://whatever">profile</a>'
795+
}
796+
}
797+
]
798+
});
799+
800+
const { request } = getInAppMessages(
801+
{ count: 10, packageName: 'my-lil-website' },
802+
true
803+
);
804+
await request();
805+
806+
const iframe = document.getElementById(
807+
'iterable-iframe'
808+
) as HTMLIFrameElement;
809+
const element = iframe?.contentWindow?.document.body?.querySelector(
810+
'a[href="iterable://whatever"]'
811+
) as Element;
812+
813+
const clickEvent = new MouseEvent('click');
814+
await element.dispatchEvent(clickEvent);
815+
expect(document.getElementById('iterable-iframe')).not.toBe(null);
816+
expect(document.getElementById('iterable-iframe')).not.toBeUndefined();
817+
});
686818
});
687819
});

src/inapp/utils.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,15 +273,15 @@ export const paintIFrame = (
273273
274274
This prevents a race condition where if we set the height before the images
275275
are loaded, we might end up with a scrolling iframe
276-
*/
276+
*/
277277
const images =
278278
html?.match(/\b(https?:\/\/\S+(?:png|jpe?g|gif)\S*)\b/gim) || [];
279279
return preloadImages(images, () => {
280280
/*
281-
set the scroll height to the content inside, but since images
282-
are going to take some time to load, we opt to preload them, THEN
283-
set the inner HTML of the iframe
284-
*/
281+
set the scroll height to the content inside, but since images
282+
are going to take some time to load, we opt to preload them, THEN
283+
set the inner HTML of the iframe
284+
*/
285285
document.body.appendChild(iframe);
286286
iframe.contentWindow?.document?.open();
287287
iframe.contentWindow?.document?.write(html);
@@ -405,6 +405,7 @@ export const paintIFrame = (
405405
export const addButtonAttrsToAnchorTag = (node: Element, ariaLabel: string) => {
406406
node.setAttribute('aria-label', ariaLabel);
407407
node.setAttribute('role', 'button');
408+
node.setAttribute('data-qa-original-link', node.getAttribute('href') || '');
408409
node.setAttribute('href', 'javascript:undefined');
409410
};
410411

0 commit comments

Comments
 (0)