update macaw in klaviyo (#670)

* update macaw in klaviyo

* cr fixes

* removed e2e artifacts
This commit is contained in:
Lukasz Ostrowski 2023-06-28 09:45:35 +02:00 committed by GitHub
parent 3bd7e3f05d
commit 37e50db29c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 131 additions and 267 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-klaviyo": minor
---
Rewritten app to use @saleor/macaw-ui/next. App should work faster and be visually more aligned with rest of the Dashboard.

View file

@ -7,15 +7,6 @@ const isSentryPropertiesInEnvironment =
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"], transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
redirects() {
return [
{
source: "/",
destination: "/configuration",
permanent: false,
},
];
},
}; };
const configWithSentry = withSentryConfig( const configWithSentry = withSentryConfig(

View file

@ -16,7 +16,7 @@
"@material-ui/lab": "4.0.0-alpha.61", "@material-ui/lab": "4.0.0-alpha.61",
"@saleor/app-sdk": "0.40.1", "@saleor/app-sdk": "0.40.1",
"@saleor/apps-shared": "workspace:*", "@saleor/apps-shared": "workspace:*",
"@saleor/macaw-ui": "^0.7.2", "@saleor/macaw-ui": "0.8.0-pre.95",
"@sentry/nextjs": "7.55.2", "@sentry/nextjs": "7.55.2",
"@urql/exchange-auth": "^2.1.4", "@urql/exchange-auth": "^2.1.4",
"clsx": "^1.2.1", "clsx": "^1.2.1",
@ -30,6 +30,7 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"urql": "^4.0.4", "urql": "^4.0.4",
"usehooks-ts": "^2.9.1",
"vite": "4.3.9", "vite": "4.3.9",
"vitest": "0.31.3" "vitest": "0.31.3"
}, },

View file

@ -1,32 +0,0 @@
import { Typography } from "@material-ui/core";
import React from "react";
type WarningCause =
| "not_in_iframe"
| "missing_access_token"
| "invalid_access_token"
| "unknown_cause";
interface AccessWarningProps {
cause?: WarningCause;
}
const warnings: Record<WarningCause, string> = {
not_in_iframe: "The view can only be displayed in the iframe.",
missing_access_token: "App doesn't have an access token.",
invalid_access_token: "Access token is invalid.",
unknown_cause: "Something went wrong.",
};
export function AccessWarning({ cause = "unknown_cause" }: AccessWarningProps) {
return (
<div suppressHydrationWarning>
<Typography variant="subtitle1">
App can&apos;t be accessed outside of the Saleor Dashboard
</Typography>
<Typography variant="subtitle2" style={{ marginTop: "2rem" }}>
{warnings[cause]}
</Typography>
</div>
);
}

View file

@ -1,18 +0,0 @@
import { CircularProgress, Typography } from "@material-ui/core";
import React from "react";
import { useStyles } from "./styles";
export function LoadingPage() {
const classes = useStyles();
return (
<div className={classes.loaderContainer}>
<CircularProgress size={100} />
<Typography variant="subtitle1" className={classes.message}>
Attempting connection to Saleor Dashboard
</Typography>
</div>
);
}

View file

@ -1,14 +0,0 @@
import { makeStyles } from "@saleor/macaw-ui";
const useStyles = makeStyles((theme) => ({
loaderContainer: {
display: "flex",
flexDirection: "column",
alignItems: "center",
},
message: {
marginTop: theme.spacing(4),
},
}));
export { useStyles };

View file

@ -1,33 +1,25 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge"; import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useTheme } from "@saleor/macaw-ui"; import { useTheme } from "@saleor/macaw-ui/next";
import { memo, useEffect } from "react"; import { useEffect } from "react";
/** // todo move to shared
* Macaw-ui stores its theme mode in memory and local storage. To synchronize App with Dashboard, export function ThemeSynchronizer() {
* Macaw must be informed about this change from AppBridge.
*
* If you are not using Macaw, you can remove this.
*/
function _ThemeSynchronizer() {
const { appBridgeState } = useAppBridge(); const { appBridgeState } = useAppBridge();
const { setTheme, themeType } = useTheme(); const { setTheme } = useTheme();
useEffect(() => { useEffect(() => {
if (!setTheme || !appBridgeState?.theme) { if (!setTheme || !appBridgeState?.theme) {
return; return;
} }
if (themeType !== appBridgeState?.theme) { if (appBridgeState.theme === "light") {
setTheme(appBridgeState.theme); setTheme("defaultLight");
/**
* Hack to fix macaw, which is going into infinite loop on light mode (probably de-sync local storage with react state)
* TODO Fix me when Macaw 2.0 is shipped
*/
window.localStorage.setItem("macaw-ui-theme", appBridgeState.theme);
} }
}, [appBridgeState?.theme, setTheme, themeType]);
if (appBridgeState.theme === "dark") {
setTheme("defaultDark");
}
}, [appBridgeState?.theme, setTheme]);
return null; return null;
} }
export const ThemeSynchronizer = memo(_ThemeSynchronizer);

View file

@ -1,21 +1,17 @@
import { makeStyles } from "@saleor/macaw-ui"; import { Box } from "@saleor/macaw-ui/next";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
const useStyles = makeStyles({ export function AppColumnsLayout({ children }: PropsWithChildren<{}>) {
root: { return (
display: "grid", <Box
gridTemplateColumns: "280px auto 280px", display={"grid"}
alignItems: "start", __gridTemplateColumns={"280px auto 280px"}
gap: 32, gap={4}
maxWidth: 1180, __maxWidth={"1180px"}
margin: "0 auto", marginX={"auto"}
}, marginY={0}
}); >
{children}
type Props = PropsWithChildren<{}>; </Box>
);
export function AppColumnsLayout({ children }: Props) {
const styles = useStyles();
return <div className={styles.root}>{children}</div>;
} }

View file

@ -1,92 +1,24 @@
import "@saleor/apps-shared/src/globals.css"; import "@saleor/macaw-ui/next/style";
import { StylesProvider, Theme } from "@material-ui/core/styles";
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge"; import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
import { import React from "react";
dark, import { AppProps } from "next/app";
light, import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
SaleorThemeColors, import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
ThemeProvider as MacawUIThemeProvider, import { NoSSRWrapper } from "@saleor/apps-shared";
} from "@saleor/macaw-ui";
import React, { PropsWithChildren, useEffect } from "react";
import { ThemeSynchronizer } from "../hooks/theme-synchronizer"; import { ThemeSynchronizer } from "../hooks/theme-synchronizer";
import { AppLayoutProps } from "../../types";
import { createGenerateClassName } from "@material-ui/core";
type PalettesOverride = Record<"light" | "dark", SaleorThemeColors>;
/** /**
* Temporary override of colors, to match new dashboard palette. * Ensure instance is a singleton.
* Long term this will be replaced with Macaw UI 2.x with up to date design tokens
*/ */
const palettes: PalettesOverride = { export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
light: {
...light,
background: {
default: "#fff",
paper: "#fff",
},
},
dark: {
...dark,
background: {
default: "hsla(211, 42%, 14%, 1)",
paper: "hsla(211, 42%, 14%, 1)",
},
},
};
const themeOverrides: Partial<Theme> = {
overrides: {
MuiTableCell: {
body: {
paddingBottom: 8,
paddingTop: 8,
},
root: {
height: 56,
paddingBottom: 4,
paddingTop: 4,
},
},
},
};
const generateClassName = createGenerateClassName({
productionPrefix: "c",
disableGlobal: true,
});
/**
* Ensure instance is a singleton, so React 18 dev mode doesn't render it twice
*/
const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
// That's a hack required by Macaw-UI incompatibility with React@18
const ThemeProvider = MacawUIThemeProvider as React.FC<
PropsWithChildren<{ overrides?: Partial<Theme>; ssr: boolean; palettes: PalettesOverride }>
>;
function SaleorApp({ Component, pageProps }: AppLayoutProps) {
const getLayout = Component.getLayout ?? ((page) => page);
useEffect(() => {
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles) {
jssStyles?.parentElement?.removeChild(jssStyles);
}
}, []);
function SaleorApp({ Component, pageProps }: AppProps) {
return ( return (
<AppBridgeProvider appBridgeInstance={appBridgeInstance}> <AppBridgeProvider appBridgeInstance={appBridgeInstance}>
<StylesProvider generateClassName={generateClassName}> <ThemeProvider>
<ThemeProvider palettes={palettes} overrides={themeOverrides} ssr>
<ThemeSynchronizer /> <ThemeSynchronizer />
{getLayout(<Component {...pageProps} />)} <Component {...pageProps} />
</ThemeProvider> </ThemeProvider>
</StylesProvider>
</AppBridgeProvider> </AppBridgeProvider>
); );
} }

View file

@ -1,32 +1,20 @@
import { Link, List, ListItem, Paper, PaperProps, TextField, Typography } from "@material-ui/core";
import Skeleton from "@material-ui/lab/Skeleton";
import { useAppBridge, withAuthorization } from "@saleor/app-sdk/app-bridge"; import { useAppBridge, withAuthorization } from "@saleor/app-sdk/app-bridge";
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const"; import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
import { ConfirmButton, ConfirmButtonTransitionState, makeStyles } from "@saleor/macaw-ui";
import { ChangeEvent, SyntheticEvent, useEffect, useState } from "react"; import { ChangeEvent, SyntheticEvent, useEffect, useState } from "react";
import { AccessWarning } from "../components/AccessWarning/AccessWarning";
import { useAppApi } from "../hooks/useAppApi"; import { useAppApi } from "../hooks/useAppApi";
import { AppColumnsLayout } from "../lib/ui/app-columns-layout"; import { AppColumnsLayout } from "../lib/ui/app-columns-layout";
import { useDashboardNotification } from "@saleor/apps-shared"; import { useDashboardNotification } from "@saleor/apps-shared";
import { Box, BoxProps, Text, Input, Button } from "@saleor/macaw-ui/next";
interface ConfigurationField { interface ConfigurationField {
key: string; key: string;
value: string; value: string;
} }
const useStyles = makeStyles((theme) => ({ function Section(props: BoxProps) {
confirmButton: { return <Box padding={4} {...props} />;
marginLeft: "auto",
},
fieldContainer: {
marginBottom: theme.spacing(2),
},
}));
function Section(props: PaperProps) {
return <Paper style={{ padding: 24 }} elevation={0} {...props} />;
} }
function Instructions() { function Instructions() {
@ -46,22 +34,22 @@ function Instructions() {
return ( return (
<Section> <Section>
<Typography paragraph variant="h3"> <Text as={"h3"} variant="heading">
How to set up How to set up
</Typography> </Text>
<Typography paragraph> <Text as="p">App will send events as Klaviyo metrics each time Saleor Event occurs.</Text>
App will send events as Klaviyo metrics each time Saleor Event occurs. <Text as="p">
</Typography>
<Typography paragraph>
When first metric is sent, it should be available in Klaviyo to build on top of. When first metric is sent, it should be available in Klaviyo to build on top of.
</Typography> </Text>
<Typography paragraph> <Text as="p">
Metric name can be customized, PUBLIC_TOKEN must be provided to enable the app. Metric name can be customized, PUBLIC_TOKEN must be provided to enable the app.
</Typography> </Text>
<Typography variant="h3">Useful links</Typography> <Text as={"h3"} variant="heading">
<List> Useful links
<ListItem> </Text>
<Link <ul>
<li>
<a
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@ -70,13 +58,15 @@ function Instructions() {
href="https://github.com/saleor/saleor-app-klaviyo" href="https://github.com/saleor/saleor-app-klaviyo"
> >
Visit repository & readme Visit repository & readme
</Link> </a>
</ListItem> </li>
</List> </ul>
<Typography variant="h3">How to configure</Typography> <Text as={"h3"} variant="heading">
<List> How to configure
<ListItem> </Text>
<Link <ul>
<li>
<a
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@ -87,10 +77,10 @@ function Instructions() {
href="https://help.klaviyo.com/hc/en-us/articles/115005062267-How-to-Manage-Your-Account-s-API-Keys" href="https://help.klaviyo.com/hc/en-us/articles/115005062267-How-to-Manage-Your-Account-s-API-Keys"
> >
Read about public tokens Read about public tokens
</Link> </a>
</ListItem> </li>
<ListItem> <li>
<Link <a
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@ -99,10 +89,10 @@ function Instructions() {
href="https://www.klaviyo.com/account#api-keys-tab" href="https://www.klaviyo.com/account#api-keys-tab"
> >
Get public token here Get public token here
</Link> </a>
</ListItem> </li>
<ListItem> <li>
<Link <a
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
@ -113,19 +103,17 @@ function Instructions() {
href="https://help.klaviyo.com/hc/en-us/articles/115005076787-Guide-to-Managing-Your-Metrics" href="https://help.klaviyo.com/hc/en-us/articles/115005076787-Guide-to-Managing-Your-Metrics"
> >
Read about metrics Read about metrics
</Link> </a>
</ListItem> </li>
</List> </ul>
</Section> </Section>
); );
} }
function Configuration() { function Configuration() {
const { appBridgeState } = useAppBridge(); const { appBridgeState } = useAppBridge();
const classes = useStyles();
const { notifySuccess, notifyError } = useDashboardNotification(); const { notifySuccess, notifyError } = useDashboardNotification();
const [configuration, setConfiguration] = useState<ConfigurationField[]>(); const [configuration, setConfiguration] = useState<ConfigurationField[]>();
const [transitionState, setTransitionState] = useState<ConfirmButtonTransitionState>("default");
const { data: configurationData, error } = useAppApi({ const { data: configurationData, error } = useAppApi({
url: "/api/configuration", url: "/api/configuration",
@ -142,7 +130,6 @@ function Configuration() {
*/ */
const handleSubmit = (event: SyntheticEvent) => { const handleSubmit = (event: SyntheticEvent) => {
event.preventDefault(); event.preventDefault();
setTransitionState("loading");
fetch("/api/configuration", { fetch("/api/configuration", {
method: "POST", method: "POST",
@ -157,12 +144,10 @@ function Configuration() {
if (response.status !== 200) { if (response.status !== 200) {
throw new Error("Error saving configuration data"); throw new Error("Error saving configuration data");
} }
setTransitionState("success");
notifySuccess("Success", "Configuration updated successfully"); notifySuccess("Success", "Configuration updated successfully");
}) })
.catch(async () => { .catch(async () => {
setTransitionState("error");
await notifyError( await notifyError(
"Configuration update failed. Ensure fields are filled correctly and you have MANAGE_APPS permission" "Configuration update failed. Ensure fields are filled correctly and you have MANAGE_APPS permission"
); );
@ -207,41 +192,32 @@ function Configuration() {
} }
if (configuration === undefined) { if (configuration === undefined) {
return <Skeleton />; return <p>Loading...</p>;
} }
return ( return (
<AppColumnsLayout> <AppColumnsLayout>
<div /> <div />
<Section> <Section>
<form onSubmit={handleSubmit}> <Text variant={"heading"} marginBottom={4} as={"h2"}>
Klaviyo configuration
</Text>
<Box as={"form"} display={"grid"} gap={4} gridAutoFlow={"row"} onSubmit={handleSubmit}>
{configuration!.map(({ key, value }) => ( {configuration!.map(({ key, value }) => (
<div key={key} className={classes.fieldContainer}> <div key={key}>
<TextField label={key} name={key} fullWidth onChange={onChange} value={value} /> <Input label={key} name={key} onChange={onChange} value={value} />
</div> </div>
))} ))}
<div> <div>
<ConfirmButton <Button type="submit" variant="primary">
type="submit" Save
variant="primary" </Button>
transitionState={transitionState}
labels={{
confirm: "Save",
error: "Error",
}}
className={classes.confirmButton}
/>
</div> </div>
</form> </Box>
</Section> </Section>
<Instructions /> <Instructions />
</AppColumnsLayout> </AppColumnsLayout>
); );
} }
export default withAuthorization({ export default Configuration;
notIframe: <AccessWarning cause="not_in_iframe" />,
unmounted: null,
noDashboardToken: <AccessWarning cause="missing_access_token" />,
dashboardTokenInvalid: <AccessWarning cause="invalid_access_token" />,
})(Configuration);

View file

@ -0,0 +1,32 @@
import { NextPage } from "next";
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useEffect } from "react";
import { useIsMounted } from "usehooks-ts";
import { useRouter } from "next/router";
import { isInIframe } from "@saleor/apps-shared";
const IndexPage: NextPage = () => {
const { appBridgeState } = useAppBridge();
const isMounted = useIsMounted();
const { replace } = useRouter();
useEffect(() => {
if (isMounted() && appBridgeState?.ready) {
replace("/configuration");
}
}, [isMounted, appBridgeState?.ready, replace]);
if (isInIframe()) {
return <span>Loading...</span>;
}
return (
<div>
<h1>Saleor Klaviyo</h1>
<p>This is Saleor App that allows to use external service to handle taxes.</p>
<p>Install the app in your Saleor instance and open it in Dashboard.</p>
</div>
);
};
export default IndexPage;

View file

@ -769,8 +769,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../packages/shared version: link:../../packages/shared
'@saleor/macaw-ui': '@saleor/macaw-ui':
specifier: ^0.7.2 specifier: 0.8.0-pre.95
version: 0.7.3(@material-ui/core@4.12.4)(@material-ui/icons@4.11.3)(@material-ui/lab@4.0.0-alpha.61)(@types/react@18.2.5)(react-dom@18.2.0)(react-helmet@6.1.0)(react@18.2.0) version: 0.8.0-pre.95(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
'@sentry/nextjs': '@sentry/nextjs':
specifier: 7.55.2 specifier: 7.55.2
version: 7.55.2(next@13.3.0)(react@18.2.0) version: 7.55.2(next@13.3.0)(react@18.2.0)
@ -810,6 +810,9 @@ importers:
urql: urql:
specifier: ^4.0.4 specifier: ^4.0.4
version: 4.0.4(graphql@16.6.0)(react@18.2.0) version: 4.0.4(graphql@16.6.0)(react@18.2.0)
usehooks-ts:
specifier: ^2.9.1
version: 2.9.1(react-dom@18.2.0)(react@18.2.0)
vite: vite:
specifier: 4.3.9 specifier: 4.3.9
version: 4.3.9(@types/node@18.15.3) version: 4.3.9(@types/node@18.15.3)