Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: appcues integration #11

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: appcues integratio
nachozullo committed Jul 17, 2023
commit 1e67e29af8b97ca9385b58da823e75953dd009ec
2 changes: 1 addition & 1 deletion react/package.json
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "12.0.0",
"@testing-library/user-event": "^13.5.0",
"@tiendanube/nexo": "0.1.3",
"@tiendanube/nexo": "0.3.0-rc1",
"@types/jest": "^27.5.2",
"@types/node": "^16.11.44",
"@types/react": "17.0.20",
63 changes: 33 additions & 30 deletions react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ import {
SettingsExamplePage,
SimpleListExamplePage,
} from './pages';
import { DarkModeProvider, BaseLayout } from './components';
import { DarkModeProvider, BaseLayout, Appcues } from './components';

import { ToastProvider } from '@nimbus-ds/components';

@@ -37,35 +37,38 @@ function App() {
<ToastProvider>
<BaseLayout>
<NexoSyncRoute>
<Switch>
<Route path="/" exact>
<MainPage />
</Route>
<Route path="/examples">
<ExamplesPage />
</Route>
<Route path="/examples-confirmation-modal">
<ConfirmationModalExamplePage />
</Route>
<Route path="/examples-form">
<FormExamplePage />
</Route>
<Route path="/examples-login">
<LoginExamplePage />
</Route>
<Route path="/examples-page-template">
<PageTemplateExamplePage />
</Route>
<Route path="/examples-product-list">
<ProductListExamplePage />
</Route>
<Route path="/examples-settings">
<SettingsExamplePage />
</Route>
<Route path="/examples-simple-list">
<SimpleListExamplePage />
</Route>
</Switch>
<>
<Appcues />
<Switch>
<Route path="/" exact>
<MainPage />
</Route>
<Route path="/examples">
<ExamplesPage />
</Route>
<Route path="/examples-confirmation-modal">
<ConfirmationModalExamplePage />
</Route>
<Route path="/examples-form">
<FormExamplePage />
</Route>
<Route path="/examples-login">
<LoginExamplePage />
</Route>
<Route path="/examples-page-template">
<PageTemplateExamplePage />
</Route>
<Route path="/examples-product-list">
<ProductListExamplePage />
</Route>
<Route path="/examples-settings">
<SettingsExamplePage />
</Route>
<Route path="/examples-simple-list">
<SimpleListExamplePage />
</Route>
</Switch>
</>
</NexoSyncRoute>
</BaseLayout>
</ToastProvider>
113 changes: 113 additions & 0 deletions react/src/components/Appcues/Appcues.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useEffect, useState } from 'react';
import nexo from '../../nexoClient';
import { useScript } from '../../hooks';

const APPCUES_ID = 126721;

declare global {
interface Window {
AppcuesSettings: Record<string, boolean>;
Appcues: {
identify: (email: string, config: Record<string, string>) => void;
};
}
}

/* Mock Appcues identify config */
const {
externalUserId,
storeId,
firstName,
lastName,
email,
storeName,
countryCode,
storeUrl,
createdAt,
} = {
externalUserId: '2446429',
storeId: '2349817',
firstName: 'Ignacio Zullo',
lastName: '',
email: '[email protected]',
storeName: 'TiendaNacho',
countryCode: 'AR',
storeUrl: 'https://tiendanacho3.mitiendanube.com',
createdAt: '2022-08-18T15:07:30.000Z',
};

export const ACTION_STORE_INFO = 'app/store/info';
export const ACTION_USER_INFO = 'app/user/info';

interface UserInfo {
id: string;
name: string;
firstName: string;
lastName: string;
email: string;
createdAtStore: number;
}

interface StoreInfo {
id: string;
name: string;
url: string;
language: string;
currency: string;
country: string;
createdAt: string;
}

function Appcues(): null {
const statusScript = useScript(`https://fast.appcues.com/${APPCUES_ID}.js`);
const [storeInfo, setStoreInfo] = useState<StoreInfo>();
const [userInfo, setUserInfo] = useState<UserInfo>();

useEffect(() => {
nexo.dispatch({ type: ACTION_STORE_INFO });
nexo.dispatch({ type: ACTION_USER_INFO });

nexo.suscribe(ACTION_STORE_INFO, (payload: StoreInfo) => {
setStoreInfo(payload);
});

nexo.suscribe(ACTION_USER_INFO, (payload: UserInfo) => {
setUserInfo(payload);
});
}, []);

useEffect(() => {
console.log({ storeInfo, userInfo });
if (!storeInfo || !userInfo) {
console.error('Appcues store & user info not present');
return;
}

window.AppcuesSettings = {
enableURLDetection: true,
};

if (statusScript === 'ready') {
if (!window.Appcues || !window.Appcues.identify) {
console.error('No Appcues and identify found in window');
return;
}

window.Appcues.identify(userInfo.email, {
externalUserId: userInfo.id,
storeId: storeInfo.id,
firstName: userInfo.firstName,
lastName: userInfo.lastName,
email: userInfo.email,
storeName: storeInfo.name,
countryCode: storeInfo.country,
storeUrl: storeInfo.url,
createdAt: storeInfo.createdAt,
});
}
}, [statusScript, storeInfo, userInfo]);

return null;
}

export default Appcues;
1 change: 1 addition & 0 deletions react/src/components/Appcues/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Appcues';
2 changes: 1 addition & 1 deletion react/src/components/BaseLayout/BaseLayout.tsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { ThemeProvider } from '@nimbus-ds/styles';
import { useDarkMode } from '../';

const BaseLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { darkMode, toggleDarkMode } = useDarkMode();
const { darkMode } = useDarkMode();
const currentTheme = darkMode ? 'dark' : 'base';
const [active, setActive] = useState(currentTheme === 'dark');

1 change: 1 addition & 0 deletions react/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -4,3 +4,4 @@ export { default as DarkModeProvider } from './DarkModeProvider';
export * from './DarkModeProvider';
export { default as ResponsiveComponent } from './ResponsiveComponent';
export { default as AppMenu } from './AppMenu';
export { default as Appcues } from './Appcues';
1 change: 1 addition & 0 deletions react/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as useWindowWidth } from './useWindowWidth';
export { default as useScript } from './useScript';
1 change: 1 addition & 0 deletions react/src/hooks/useScript/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './useScript';
113 changes: 113 additions & 0 deletions react/src/hooks/useScript/useScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useEffect, useState } from 'react';

export type UseScriptStatus = 'idle' | 'loading' | 'ready' | 'error';

export interface UseScriptOptions {
shouldPreventLoad?: boolean;
removeOnUnmount?: boolean;
}

// Cached script statuses
const cachedScriptStatuses: Record<string, UseScriptStatus | undefined> = {};

function getScriptNode(src: string) {
const node: HTMLScriptElement | null = document.querySelector(
`script[src="${src}"]`,
);
const status = node?.getAttribute('data-status') as
| UseScriptStatus
| undefined;

return {
node,
status,
};
}

function useScript(
src: string | null,
options?: UseScriptOptions,
): UseScriptStatus {
const [status, setStatus] = useState<UseScriptStatus>(() => {
if (!src || options?.shouldPreventLoad) {
return 'idle';
}

if (typeof window === 'undefined') {
// SSR Handling - always return 'loading'
return 'loading';
}

return cachedScriptStatuses[src] ?? 'loading';
});

useEffect(() => {
if (!src || options?.shouldPreventLoad) {
return;
}

const cachedScriptStatus = cachedScriptStatuses[src];
if (cachedScriptStatus === 'ready' || cachedScriptStatus === 'error') {
// If the script is already cached, set its status immediately
setStatus(cachedScriptStatus);
return;
}

// Fetch existing script element by src
// It may have been added by another instance of this hook
const script = getScriptNode(src);
let scriptNode = script.node;

if (!scriptNode) {
// Create script element and add it to document body
scriptNode = document.createElement('script');
scriptNode.src = src;
scriptNode.async = true;
scriptNode.setAttribute('data-status', 'loading');
document.body.appendChild(scriptNode);

// Store status in attribute on script
// This can be read by other instances of this hook
const setAttributeFromEvent = (event: Event) => {
const scriptStatus: UseScriptStatus =
event.type === 'load' ? 'ready' : 'error';

scriptNode?.setAttribute('data-status', scriptStatus);
};

scriptNode.addEventListener('load', setAttributeFromEvent);
scriptNode.addEventListener('error', setAttributeFromEvent);
} else {
// Grab existing script status from attribute and set to state.
setStatus(script.status ?? cachedScriptStatus ?? 'loading');
}

// Script event handler to update status in state
// Note: Even if the script already exists we still need to add
// event handlers to update the state for *this* hook instance.
const setStateFromEvent = (event: Event) => {
const newStatus = event.type === 'load' ? 'ready' : 'error';
setStatus(newStatus);
cachedScriptStatuses[src] = newStatus;
};

// Add event listeners
scriptNode.addEventListener('load', setStateFromEvent);
scriptNode.addEventListener('error', setStateFromEvent);

// Remove event listeners on cleanup
return () => {
if (scriptNode) {
scriptNode.removeEventListener('load', setStateFromEvent);
scriptNode.removeEventListener('error', setStateFromEvent);
}
if (scriptNode && options?.removeOnUnmount) {
scriptNode.remove();
}
};
}, [src, options?.shouldPreventLoad, options?.removeOnUnmount]);

return status;
}

export default useScript;
8 changes: 1 addition & 7 deletions react/src/index.css
Original file line number Diff line number Diff line change
@@ -27,10 +27,4 @@ body {

body {
background-color: var(--background);
}

@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
}