diff --git a/example/src/index.ts b/example/src/index.ts index b78efd7c..62ae5c06 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -71,7 +71,7 @@ import { const handleGetMessagesClick = (event: MouseEvent) => { event.preventDefault(); if (startBtn.getAttribute('aria-disabled') !== 'true') { - startBtn.innerText = `Loading...`; + startBtn.innerText = 'Loading...'; startBtn.setAttribute('aria-disabled', 'true'); startBtn.className = 'disabled'; request() @@ -94,7 +94,7 @@ import { /* login */ loginBtn.setAttribute('aria-disabled', 'true'); loginBtn.className = 'disabled'; - loginBtn.innerText = `Loading...`; + loginBtn.innerText = 'Loading...'; setEmail(email).then(() => { /* enable change email button */ changeEmailBtn.classList.remove('disabled'); @@ -123,7 +123,7 @@ import { changeEmailBtn.setAttribute('aria-disabled', 'true'); changeEmailBtn.className = 'disabled'; - changeEmailBtn.innerText = `Loading...`; + changeEmailBtn.innerText = 'Loading...'; startBtn.setAttribute('aria-disabled', 'true'); startBtn.className = 'disabled'; diff --git a/react-example/.env.example b/react-example/.env.example index 7df57e8b..1519e40a 100644 --- a/react-example/.env.example +++ b/react-example/.env.example @@ -1,10 +1,15 @@ -# To make requests from this example app make sure you first create an .env file -# and add the API key and JWT Secret to it like so (and uncomment the keys): -# API_KEY=1234 -# JWT_SECRET=1234 +# Optional environment variables for local development. Start by creating a +# called .env and add these values to it and change them appropriately. +# Remember to uncomment the variables! + +# Only set BASE_URL if developing locally, as it will take precedence over the production api urls. +# BASE_URL="https://api.iterable.com/api" # You can set the URL for the JWT generator here if needed -# JWT_GENERATOR=http://localhost:5000/generate +# JWT_GENERATOR=http://localhost:3000/generate + +# Set this to false to prevent messages from being consumed to fetch the same message(s) when testing changes locally. +# ENABLE_INAPP_CONSUME=false -# Convenience variable to automatically set the login email during testing. -# LOGIN_EMAIL=email@email.com +# Toggle this to true if you would need to hit our EU APIs. +# IS_EU_ITERABLE_SERVICE=false diff --git a/react-example/src/index.tsx b/react-example/src/index.tsx index 3a27fdc2..8a3ed57f 100644 --- a/react-example/src/index.tsx +++ b/react-example/src/index.tsx @@ -50,7 +50,7 @@ const HomeLink = styled(Link)` { exp_minutes: 2, email, - user_id: userID, + userId: userID, jwt_secret: process.env.JWT_SECRET }, { diff --git a/react-example/src/views/EmbeddedMsgs.tsx b/react-example/src/views/EmbeddedMsgs.tsx index ecabe187..90805f18 100644 --- a/react-example/src/views/EmbeddedMsgs.tsx +++ b/react-example/src/views/EmbeddedMsgs.tsx @@ -170,7 +170,7 @@ export const EmbeddedMsgs: FC = () => { parentStyle: ' margin-bottom: 10; ', primaryBtnStyle: ` background-color: #000fff; - border-radius: 8px; + border-radius: 10px; padding: 10px; color: #ffffff; `, diff --git a/react-example/src/views/EmbeddedMsgsImpressionTracker.tsx b/react-example/src/views/EmbeddedMsgsImpressionTracker.tsx index b2b0d4e3..6e2007d9 100644 --- a/react-example/src/views/EmbeddedMsgsImpressionTracker.tsx +++ b/react-example/src/views/EmbeddedMsgsImpressionTracker.tsx @@ -13,13 +13,14 @@ interface Props {} export const EmbeddedMsgsImpressionTracker: FC = () => { const elementCardRef = useRef([]); const { loggedInUser, setLoggedInUser } = useUser(); + const appPackageName = 'my-website'; const [messages, setMessages] = useState([]); const [embeddedManager] = useState( - new IterableEmbeddedManager('my-website') + new IterableEmbeddedManager(appPackageName) ); const [embeddedSessionManager] = useState( - new IterableEmbeddedSessionManager('my-website') + new IterableEmbeddedSessionManager(appPackageName) ); const getCardObserver = () => { @@ -118,7 +119,7 @@ export const EmbeddedMsgsImpressionTracker: FC = () => { messages.map((message: IterableEmbeddedMessage, index: number) => { const data = message; const card = IterableEmbeddedCard({ - embeddedManager, + appPackageName, message: data, parentStyle: ' margin-bottom: 10; ' }); diff --git a/src/components/banner/index.tsx b/src/components/banner/index.tsx index d1a993a4..4d6dfb7a 100644 --- a/src/components/banner/index.tsx +++ b/src/components/banner/index.tsx @@ -1,7 +1,9 @@ import { handleElementClick, - addButtonClickEvent -} from '../../embedded/embeddedClickEvents'; + addButtonClickEvent, + getTrimmedText, + updateButtonPadding +} from '../../embedded/embeddedUtil'; import { IterableEmbeddedButton } from 'src/embedded'; import { EmbeddedMessageData } from '../types'; @@ -36,42 +38,57 @@ export function IterableEmbeddedBanner({ margin: auto; margin-top: 10px; margin-bottom: 10px; - padding: 16px; - ${message?.elements?.defaultAction ? 'cursor: pointer;' : 'auto'} + padding: 10px 20px 15px 20px; + cursor: ${message?.elements?.defaultAction ? 'pointer' : 'auto'}; `; const defaultImageStyles = ` width: 70px; height: 70px; border-radius: 8px; margin-left: 10px; + object-fit: cover; + margin-top: 5px; `; const defaultTitleStyles = ` font-size: 20px; font-weight: bold; - margin-bottom: 4px; + margin-bottom: 6px; + color: rgb(61, 58, 59); display: block; `; const defaultTextStyles = ` - font-size: 16px; + font-size: 17px; margin-bottom: 10px; display: block; + margin-bottom: 25px; + color: rgb(120, 113, 116); `; const bannerButtons = ` margin-top: auto; + display: flex; + flex-direction: row; + flex-wrap: wrap; + row-gap: 0.2em; `; const defaultButtonStyles = ` max-width: calc(50% - 32px); - text-align: left; + text-align: center; font-size: 16px; font-weight: bold; - background-color: transparent; - color: #433d99; border: none; - border-radius: 0; + border-radius: 100px; cursor: pointer; - padding: 5px; - overflow-wrap: break-word; + padding: 8px 0px; + min-width: fit-content; + margin-right: 12px; + color: #622a6a; + background: none; `; + const defaultPrimaryButtonStyle = ` + background: #622a6a; + color: white; + padding: 8px 12px; + `; const defaultTextParentStyles = ` flex: 1; max-width: calc(100% - 80px); @@ -79,15 +96,12 @@ export function IterableEmbeddedBanner({ const mediaStyle = ` @media screen and (max-width: 800px) { .titleText { - overflow: hidden; - text-overflow: ellipsis; - max-height: 2.6em; line-height: 1.3em; } .banner { - min-height: 150px; display: flex; flex-direction: column; + min-width: fit-content; } } `; @@ -102,7 +116,7 @@ export function IterableEmbeddedBanner({ const secondaryButtonClick = document.getElementsByName( `${message?.metadata?.messageId}-banner-secondaryButton` )[0]; - if (bannerDiv) { + if (bannerDiv && message?.elements?.defaultAction) { bannerDiv.addEventListener('click', () => handleElementClick(message, appPackageName, errorCallback) ); @@ -125,6 +139,9 @@ export function IterableEmbeddedBanner({ errorCallback ); } + updateButtonPadding('.banner-button-primary-secondary'); + window.onresize = () => + updateButtonPadding('.banner-button-primary-secondary'); }, 0); const getStyleObj = (index: number) => { @@ -143,6 +160,17 @@ export function IterableEmbeddedBanner({ }; }; + const title = getTrimmedText(message?.elements?.title); + const body = getTrimmedText(message?.elements?.body); + if ( + !( + title.length || + body.length || + message?.elements?.buttons?.length || + message?.elements?.mediaUrl + ) + ) + return ''; return `
- -
`; diff --git a/src/components/card/index.tsx b/src/components/card/index.tsx index 80823ef6..ca72552f 100644 --- a/src/components/card/index.tsx +++ b/src/components/card/index.tsx @@ -1,7 +1,8 @@ import { handleElementClick, - addButtonClickEvent -} from '../../embedded/embeddedClickEvents'; + addButtonClickEvent, + getTrimmedText +} from '../../embedded/embeddedUtil'; import { IterableEmbeddedButton } from 'src/embedded'; import { EmbeddedMessageData } from '../types'; @@ -36,37 +37,40 @@ export function IterableEmbeddedCard({ margin-top: 10px; margin-bottom: 10px; padding-bottom: 10px; - ${message?.elements?.defaultAction ? 'cursor: pointer;' : 'auto'} + cursor: ${message?.elements?.defaultAction ? 'pointer' : 'auto'}; `; const defaultImageStyles = ` width: 100%; - height: auto; + aspect-ratio: 16/9; border-top-left-radius: 8px; border-top-right-radius: 8px; + object-fit: cover; `; const defaultTitleStyles = ` - font-size: 18px; + font-size: 20px; font-weight: bold; - margin-bottom: 4px; + margin-bottom: 9px; + color: rgb(61, 58, 59); display: block; `; const defaultTextStyles = ` - font-size: 14px; + font-size: 17px; margin-bottom: 10px; display: block; + color: rgb(120, 113, 116); `; const defaultButtonStyles = ` max-width: calc(50% - 32px); - text-align: left; + text-align: center; font-size: 16px; font-weight: bold; background-color: transparent; - color: ${disablePrimaryBtn ? 'grey' : '#433d99'}; + color: ${disablePrimaryBtn ? 'grey' : '#622a6a'}; border: none; border-radius: 0; cursor: pointer; padding: 5px; - overflow-wrap: break-word; + min-width: fit-content; `; const defaultTextParentStyles = ` @@ -77,18 +81,17 @@ export function IterableEmbeddedCard({ const cardButtons = ` margin-top: auto; margin-left: 5px; + display: flex; + flex-direction: row; + flex-wrap: wrap; `; const mediaStyle = ` @media screen and (max-width: 800px) { .titleText { - overflow: hidden; - text-overflow: ellipsis; - max-height: 2.6em; line-height: 1.3em; } .card { - min-height: 350px; display: flex; flex-direction: column; } @@ -105,7 +108,7 @@ export function IterableEmbeddedCard({ const secondaryButtonClick = document.getElementsByName( `${message?.metadata?.messageId}-card-secondaryButton` )[0]; - if (cardDiv) { + if (cardDiv && message?.elements?.defaultAction) { cardDiv.addEventListener('click', () => handleElementClick(message, appPackageName, errorCallback) ); @@ -146,6 +149,17 @@ export function IterableEmbeddedCard({ }; }; + const title = getTrimmedText(message?.elements?.title); + const body = getTrimmedText(message?.elements?.body); + if ( + !( + title.length || + body.length || + message?.elements?.buttons?.length || + message?.elements?.mediaUrl + ) + ) + return ''; return `
${ message?.elements?.mediaUrl - ? `` : '' } -
- - ${message?.elements?.title || 'Title Here'} - - - ${message?.elements?.body} - +
+ ${ + title.length + ? ` + ${title} + ` + : '' + } + ${ + body.length + ? ` + ${body} + ` + : '' + }
-
- ${message?.elements?.buttons - ?.map((button: IterableEmbeddedButton, index: number) => { - const buttonStyleObj = getStyleObj(index); - return ` +
+ ${ + message?.elements?.buttons + ?.map((button: IterableEmbeddedButton, index: number) => { + const buttonTitle = getTrimmedText(button.title); + if (!buttonTitle.length) { + return null; + } + const buttonStyleObj = getStyleObj(index); + return ` `; - }) - .join('')} + }) + .join('') || '' + }
`; diff --git a/src/components/notification/index.tsx b/src/components/notification/index.tsx index dfc82992..f49df095 100644 --- a/src/components/notification/index.tsx +++ b/src/components/notification/index.tsx @@ -1,13 +1,16 @@ import { EmbeddedMessageData } from '../types'; import { handleElementClick, - addButtonClickEvent -} from '../../embedded/embeddedClickEvents'; + addButtonClickEvent, + getTrimmedText, + updateButtonPadding +} from '../../embedded/embeddedUtil'; import { IterableEmbeddedButton } from 'src/embedded'; export function IterableEmbeddedNotification({ appPackageName, message, + parentStyle, disablePrimaryBtn = false, disableSecondaryBtn = false, primaryBtnStyle, @@ -25,42 +28,66 @@ export function IterableEmbeddedNotification({ textTitleDivId = 'notification-text-title-div', errorCallback }: EmbeddedMessageData): string { + const defaultNotificationStyles = ` + background: white; + border: 1px solid #ccc; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 10px 20px 15px 20px; + margin-top: 10px; + margin-bottom: 10px; + cursor: ${message?.elements?.defaultAction ? 'pointer' : 'auto'}; + `; const defaultTitleStyles = ` + margin-top: 0px; font-size: 20px; font-weight: bold; margin-bottom: 4px; + color: rgb(61, 58, 59); display: block; `; const defaultTextStyles = ` - font-size: 16px; + margin-top: 0.2em; + font-size: 17px; margin-bottom: 10px; display: block; + color: rgb(120, 113, 116); `; const defaultTextParentStyles = ` overflow-wrap: break-word; `; const defaultButtonStyles = ` max-width: calc(50% - 32px); - text-align: left; - - border-radius: 4px; - padding: 8px; - margin-right: 8px; + text-align: center; + font-weight: bold; + border-radius: 100px; + padding: 8px 0px; + margin-right: 12px; cursor: pointer; border: none; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 2px 3px rgba(0, 0, 0, 0.06); - overflow-wrap: break-word; + min-width: fit-content; + font-size: 16px; + color: #622a6a; + background: none; + `; + const notificationButtons = ` + margin-top: auto; + display:flex; + flex-direction:row; + flex-wrap: wrap; + row-gap: 0.3em; + `; + const defaultPrimaryButtonStyle = ` + background: #622a6a; + color: white; + padding: 8px 12px; `; const mediaStyle = ` @media screen and (max-width: 800px) { .titleText { - overflow: hidden; - text-overflow: ellipsis; - max-height: 2.6em; line-height: 1.3em; } .notification { - min-height: 100px; display: flex; flex-direction: column; } @@ -77,7 +104,7 @@ export function IterableEmbeddedNotification({ const secondaryButtonClick = document.getElementsByName( `${message?.metadata?.messageId}-notification-secondaryButton` )[0]; - if (notificationDiv) { + if (notificationDiv && message?.elements?.defaultAction) { notificationDiv.addEventListener('click', () => handleElementClick(message, appPackageName, errorCallback) ); @@ -100,6 +127,9 @@ export function IterableEmbeddedNotification({ errorCallback ); } + updateButtonPadding('.notification-button-primary-secondary'); + window.onresize = () => + updateButtonPadding('.notification-button-primary-secondary'); }, 0); const getStyleObj = (index: number) => { @@ -117,57 +147,71 @@ export function IterableEmbeddedNotification({ : 'enabled' }; }; - + const title = getTrimmedText(message?.elements?.title); + const body = getTrimmedText(message?.elements?.body); + if (!(title.length || body.length || message?.elements?.buttons?.length)) + return ''; return `
-
-

- ${message?.elements?.title || 'Title Here'} -

-

- ${message?.elements?.body} -

+ ${title} +

` + : '' + } + ${ + body.length + ? `

+ ${body} +

` + : '' + }
-
- ${message?.elements?.buttons - ?.map((button: IterableEmbeddedButton, index: number) => { - const buttonStyleObj = getStyleObj(index); - return ` +
+ ${ + message?.elements?.buttons + ?.map((button: IterableEmbeddedButton, index: number) => { + const buttonTitle = getTrimmedText(button.title); + if (!buttonTitle.length) { + return null; + } + const buttonStyleObj = getStyleObj(index); + return ` `; - }) - .join('')} + }) + .join('') || '' + }
`; diff --git a/src/embedded/embeddedClickEvents.ts b/src/embedded/embeddedUtil.ts similarity index 65% rename from src/embedded/embeddedClickEvents.ts rename to src/embedded/embeddedUtil.ts index 52c0c333..57a38cfa 100644 --- a/src/embedded/embeddedClickEvents.ts +++ b/src/embedded/embeddedUtil.ts @@ -118,3 +118,52 @@ const handleEmbeddedClick = (clickedUrl: string | null) => { ); } }; + +export function getTrimmedText(text: string | undefined) { + const unsafeText = text && typeof text === 'string' ? text.trim() : ''; + return escapeHtml(unsafeText); +} + +function escapeHtml(unsafe: string) { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +export function updateButtonPadding(selector = '') { + const primaryButtons = document.querySelectorAll(selector) || []; + + primaryButtons.forEach((primaryButton: any) => { + if (primaryButton.getAttribute('data-index') !== 0) return; + const computedStyle = window.getComputedStyle(primaryButton); + const fontSize = parseFloat(computedStyle.fontSize); + const height = parseFloat(computedStyle.height); + const lineHeight = parseFloat(computedStyle.lineHeight); + const computedLineHeight = fontSize * 1.2; + let numberOfLines = Math.round(height / computedLineHeight); + + if (!isNaN(lineHeight)) { + numberOfLines = Math.round(height / lineHeight); + } + if (window.innerWidth <= 650) { + // Adjust padding for smaller screens + if (numberOfLines > 3) { + primaryButton.style.padding = '1em'; + } else { + primaryButton.style.padding = '8px 12px'; + } + + if (numberOfLines > 5) { + primaryButton.style.borderRadius = '50px'; + } else { + primaryButton.style.borderRadius = '100px'; + } + } else { + primaryButton.style.padding = '8px 12px'; + primaryButton.style.borderRadius = '100px'; + } + }); +} diff --git a/webpack.config.js b/webpack.config.js index d3832a60..dae70eb5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,7 +9,7 @@ function getParsedEnv() { return { ...env.parsed, VERSION: version, - IS_EU_ITERABLE_SERVICE: process.env.IS_EU_ITERABLE_SERVICE || false, + IS_EU_ITERABLE_SERVICE: process.env.IS_EU_ITERABLE_SERVICE || false }; } diff --git a/webpack.dev.config.js b/webpack.dev.config.js index 771e95a4..a7baafd2 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -9,7 +9,7 @@ function getParsedEnv() { return { ...env.parsed, VERSION: version, - IS_EU_ITERABLE_SERVICE: process.env.IS_EU_ITERABLE_SERVICE || false, + IS_EU_ITERABLE_SERVICE: process.env.IS_EU_ITERABLE_SERVICE || false }; } diff --git a/webpack.node.config.js b/webpack.node.config.js index 5f740130..ddb15b0d 100644 --- a/webpack.node.config.js +++ b/webpack.node.config.js @@ -9,7 +9,7 @@ function getParsedEnv() { return { ...env.parsed, VERSION: version, - IS_EU_ITERABLE_SERVICE: process.env.IS_EU_ITERABLE_SERVICE || false, + IS_EU_ITERABLE_SERVICE: process.env.IS_EU_ITERABLE_SERVICE || false }; }