Skip to content

Commit

Permalink
[MOB-7613] Remove extra height from iframe + refactor (#302)
Browse files Browse the repository at this point in the history
* [MOB-7613] Unset iframe body margin if one is not explicitly already set

* [MOB-7613] Detect img tags alongside image url paths

* [MOB-7613] Set iframe height on iframe load

* [MOB-7613] Add jwt generator url to .env.example

* [MOB-7613] Cleaup utils

* [MOB-7613] Move caching methods to separate file

* [MOB-7613] Update usages of cache

* [MOB-7613] Clean up comments in cache.ts

* [MOB-7613] Remove added return

* [MOB-7613] Clean up iframe width/height setter

* [MOB-7613] Add comment for consume variable

* [MOB-7613] Add env convenience variable for react sample app

* [MOB-7613] Suppress console.warn lint warnings

* [MOB-7613] Clean up types and remove template literals

* [MOB-7613] Fix tests

* [MOB-7613] Fix tests

* [MOB-7613] Put localhost url as fallback for jwt generator
  • Loading branch information
pauljung14 authored Feb 20, 2024
1 parent 52fed68 commit de72a62
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 342 deletions.
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
# 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
# Only set BASE_URL if developing locally, as it will take precedence over the production api urls.
# BASE_URL="https://api.iterable.com/api"

# Set this to false to prevent messages from being consumed to fetch the same message(s) when testing changes locally.
# ENABLE_INAPP_CONSUME=false

# toggle this to true if you would need to hit our EU APIs
# Toggle this to true if you would need to hit our EU APIs.
# IS_EU_ITERABLE_SERVICE=false
9 changes: 7 additions & 2 deletions react-example/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# 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
# JWT_SECRET=1234

# You can set the URL for the JWT generator here if needed
# JWT_GENERATOR=http://localhost:5000/generate

# Convenience variable to automatically set the login email during testing.
# [email protected]
2 changes: 1 addition & 1 deletion react-example/src/components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ interface Props {
}

export const LoginForm: FC<Props> = ({ setEmail, logout, refreshJwt }) => {
const [email, updateEmail] = useState<string>('');
const [email, updateEmail] = useState<string>(process.env.LOGIN_EMAIL || '');

const [isEditingUser, setEditingUser] = useState<boolean>(false);

Expand Down
2 changes: 1 addition & 1 deletion react-example/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const HomeLink = styled(Link)`
({ email }) => {
return axios
.post(
'http://localhost:5000/generate',
process.env.JWT_GENERATOR || 'http://localhost:5000/generate',
{
exp_minutes: 2,
email,
Expand Down
6 changes: 3 additions & 3 deletions react-example/src/views/InApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import styled from 'styled-components';
import _Button from 'src/components/Button';
import { EndpointWrapper, Heading, Response } from './Components.styled';
import { useUser } from 'src/context/Users';
import { getInAppMessages } from '@iterable/web-sdk';
import { DisplayOptions, getInAppMessages } from '@iterable/web-sdk';

const Button = styled(_Button)`
width: 100%;
Expand Down Expand Up @@ -35,7 +35,7 @@ const { request, pauseMessageStream, resumeMessageStream } = getInAppMessages(
closeButton: {},
displayInterval: 1000
},
{ display: 'immediate' }
{ display: DisplayOptions.Immediate }
);

export const InApp: FC<{}> = () => {
Expand All @@ -57,7 +57,7 @@ export const InApp: FC<{}> = () => {

return getInAppMessages(
{ count: 20, packageName: 'my-website' },
{ display: 'deferred' }
{ display: DisplayOptions.Deferred }
)
.request()
.then((response) => {
Expand Down
8 changes: 4 additions & 4 deletions src/__data__/inAppMessages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InAppMessage } from '../inapp/types';
import { DisplayPosition, InAppMessage } from '../inapp/types';

const normalMessage: InAppMessage = {
messageId: 'normalMessage!',
Expand Down Expand Up @@ -34,7 +34,7 @@ const normalMessage: InAppMessage = {
}
},
webInAppDisplaySettings: {
position: 'Center'
position: DisplayPosition.Center
}
},
customPayload: {},
Expand Down Expand Up @@ -88,7 +88,7 @@ const expiredMessage: InAppMessage = {
}
},
webInAppDisplaySettings: {
position: 'Center'
position: DisplayPosition.Center
}
},
customPayload: {
Expand Down Expand Up @@ -147,7 +147,7 @@ const previouslyCachedMessage: InAppMessage = {
shouldAnimate: true
},
webInAppDisplaySettings: {
position: 'Center'
position: DisplayPosition.Center
}
},
customPayload: {},
Expand Down
123 changes: 123 additions & 0 deletions src/inapp/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { setMany } from 'idb-keyval';
import { BrowserStorageEstimate, CachedMessage, InAppMessage } from './types';

/**
* Detect amount of local storage remaining (quota) and used (usage).
* If usageDetails exist (not supported in Safari), use this instead of usage.
*/
export const determineRemainingStorageQuota = async () => {
try {
if (!('indexedDB' in window)) return 0;

const storage: BrowserStorageEstimate | undefined =
'storage' in navigator && 'estimate' in navigator.storage
? await navigator.storage.estimate()
: undefined;

/** 50 MB is the lower common denominator on modern mobile browser caches. */
const mobileBrowserQuota = 52428800;
/** Max quota of browser storage that in-apps will potentially fill */
const estimatedBrowserQuota = storage?.quota;
/**
* Determine lower max quota that can be used for message cache, set to
* 60% of quota to leave space for other caching needs on that domain.
*/
const messageQuota =
((estimatedBrowserQuota &&
Math.min(estimatedBrowserQuota, mobileBrowserQuota)) ??
mobileBrowserQuota) * 0.6;

/** How much local storage is being used. */
const usage = storage?.usageDetails?.indexedDB ?? storage?.usage;
const remainingQuota = usage && messageQuota - usage;

return remainingQuota ? remainingQuota : 0;
} catch (err: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
// eslint-disable-next-line no-console
console.warn(
'Error determining remaining storage quota',
err?.response?.data?.clientErrors ?? err
);
}
/** Do not try to add to cache if we cannot determine storage space. */
return 0;
};

/**
* Deletes cached messages not present in latest getMessages fetch.
* @param cachedMessages
* @param fetchedMessages
*/
export const getCachedMessagesToDelete = (
cachedMessages: CachedMessage[],
fetchedMessages: Partial<InAppMessage>[]
) =>
cachedMessages.reduce((deleteQueue: string[], [cachedMessageId]) => {
const isCachedMessageInFetch = fetchedMessages.reduce(
(isFound, { messageId }) => {
if (messageId === cachedMessageId) isFound = true;
return isFound;
},
false
);

if (!isCachedMessageInFetch) deleteQueue.push(cachedMessageId);
return deleteQueue;
}, []);

/**
* Adds messages to cache only if they fit within the quota, starting with
* oldest messages since newer messages can still be easily retrieved via
* new requests while passing in latestCachedMessageId param.
* @param messages
* @param quota
*/
export const addNewMessagesToCache = async (
messages: { messageId: string; message: InAppMessage }[]
) => {
const quota = await determineRemainingStorageQuota();
if (quota > 0) {
/**
* Determine total size (in bytes) of new messages to be added to cache
* sorted oldest to newest (ascending createdAt property).
*/
const messagesWithSizes: {
messageId: string;
message: InAppMessage;
createdAt: number;
size: number;
}[] = messages
.map(({ messageId, message }) => {
const sizeInBytes = new Blob([
JSON.stringify(message).replace(/\[\[\],"\]/g, '')
]).size;
return {
messageId,
message,
createdAt: message.createdAt,
size: sizeInBytes
};
})
.sort((a, b) => a.createdAt - b.createdAt);

/** Only add messages that fit in cache, starting from oldest messages. */
let remainingQuota = quota;
const messagesToAddToCache: [string, InAppMessage][] = [];
messagesWithSizes.every(({ messageId, message, size }) => {
if (remainingQuota - size < 0) return false;
remainingQuota -= size;
messagesToAddToCache.push([messageId, message]);
return true;
});

try {
await setMany(messagesToAddToCache);
} catch (err: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
// eslint-disable-next-line no-console
console.warn(
'Error adding new messages to the browser cache',
err?.response?.data?.clientErrors ?? err
);
}
}
};
14 changes: 6 additions & 8 deletions src/inapp/inapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ import {
import { IterablePromise } from '../types';
import { requestMessages } from './request';
import {
DISPLAY_OPTIONS,
DisplayOptions,
GetInAppMessagesResponse,
HANDLE_LINKS,
HandleLinks,
InAppMessage,
InAppMessageResponse,
InAppMessagesRequestParams
Expand Down Expand Up @@ -426,9 +425,8 @@ export function getInAppMessages(
) => {
if (typeof handleLinks === 'string') {
if (
handleLinks === HANDLE_LINKS.OpenAllSameTab ||
(isInternalLink &&
handleLinks === HANDLE_LINKS.ExternalNewTab)
handleLinks === HandleLinks.OpenAllSameTab ||
(isInternalLink && handleLinks === HandleLinks.ExternalNewTab)
) {
sameTabAction();
} else {
Expand Down Expand Up @@ -481,9 +479,9 @@ export function getInAppMessages(
if (clickedUrl) {
const isOpeningLinkInSameTab =
(!handleLinks && !openInNewTab) ||
handleLinks === HANDLE_LINKS.OpenAllSameTab ||
handleLinks === HandleLinks.OpenAllSameTab ||
(isInternalLink &&
handleLinks === HANDLE_LINKS.ExternalNewTab);
handleLinks === HandleLinks.ExternalNewTab);

trackInAppClick(
{
Expand Down Expand Up @@ -574,7 +572,7 @@ export function getInAppMessages(
return Promise.resolve('');
};

const isDeferred = options.display === DISPLAY_OPTIONS.deferred;
const isDeferred = options.display === DisplayOptions.Deferred;

const triggerDisplayFn = isDeferred
? {
Expand Down
8 changes: 5 additions & 3 deletions src/inapp/request.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { delMany, entries } from 'idb-keyval';
import { GETMESSAGES_PATH, SDK_VERSION, WEB_PLATFORM } from 'src/constants';
import { baseIterableRequest } from 'src/request';
import { addNewMessagesToCache, getCachedMessagesToDelete } from './cache';
import schema from './inapp.schema';
import {
CachedMessage,
InAppMessage,
InAppMessageResponse,
InAppMessagesRequestParams
} from './types';
import { addNewMessagesToCache, getCachedMessagesToDelete } from './utils';

type RequestInAppMessagesProps = {
latestCachedMessageId?: string;
Expand Down Expand Up @@ -107,7 +107,8 @@ export const requestMessages = async ({ payload }: RequestMessagesProps) => {
);
try {
await delMany(cachedMessagesToDelete);
} catch (err: any) {
} catch (err: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
// eslint-disable-next-line no-console
console.warn(
'Error deleting messages from the browser cache',
err?.response?.data?.clientErrors ?? err
Expand All @@ -124,7 +125,8 @@ export const requestMessages = async ({ payload }: RequestMessagesProps) => {
inAppMessages: allMessages
}
};
} catch (err: any) {
} catch (err: any /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
// eslint-disable-next-line no-console
console.warn(
'Error requesting in-app messages',
err?.response?.data?.clientErrors ?? err
Expand Down
51 changes: 51 additions & 0 deletions src/inapp/tests/cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { messages } from '../../__data__/inAppMessages';
import { getCachedMessagesToDelete } from '../cache';
import { CachedMessage, InAppMessage } from '../types';

describe('cache.ts', () => {
describe('Caching', () => {
const now = Date.now();
const allMessages = [...messages];

const cachedMessages: CachedMessage[] = allMessages.flatMap((msg) => [
[msg.messageId, msg]
]);

it('should delete cached messages that are expired', () => {
const unexpiredMessages = allMessages.filter(
(msg) => msg.expiresAt > now
);
const expiredMessageIds = allMessages.reduce(
(allFetchedIds: string[], message) => {
if (message.expiresAt < now) allFetchedIds.push(message.messageId);
return allFetchedIds;
},
[]
);

const messagesForDeletion = getCachedMessagesToDelete(
cachedMessages,
unexpiredMessages
);
expect(messagesForDeletion).toEqual(expiredMessageIds);
});

it('should delete any cached messages not included in the fetch', () => {
const validMessages: InAppMessage[] = [];
const invalidMessages: InAppMessage[] = [];
allMessages.forEach((msg) =>
msg.messageId === 'normalMessage!'
? validMessages.push(msg)
: invalidMessages.push(msg)
);

const messagesForDeletion = getCachedMessagesToDelete(
cachedMessages,
validMessages
);
expect(messagesForDeletion).toEqual(
invalidMessages.map((msg) => msg.messageId)
);
});
});
});
Loading

0 comments on commit de72a62

Please sign in to comment.