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 = {
reactStrictMode: true,
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
redirects() {
return [
{
source: "/",
destination: "/configuration",
permanent: false,
},
];
},
};
const configWithSentry = withSentryConfig(

View file

@ -16,7 +16,7 @@
"@material-ui/lab": "4.0.0-alpha.61",
"@saleor/app-sdk": "0.40.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/macaw-ui": "^0.7.2",
"@saleor/macaw-ui": "0.8.0-pre.95",
"@sentry/nextjs": "7.55.2",
"@urql/exchange-auth": "^2.1.4",
"clsx": "^1.2.1",
@ -30,6 +30,7 @@
"react-dom": "18.2.0",
"react-helmet": "^6.1.0",
"urql": "^4.0.4",
"usehooks-ts": "^2.9.1",
"vite": "4.3.9",
"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 { useTheme } from "@saleor/macaw-ui";
import { memo, useEffect } from "react";
import { useTheme } from "@saleor/macaw-ui/next";
import { useEffect } from "react";
/**
* Macaw-ui stores its theme mode in memory and local storage. To synchronize App with Dashboard,
* Macaw must be informed about this change from AppBridge.
*
* If you are not using Macaw, you can remove this.
*/
function _ThemeSynchronizer() {
// todo move to shared
export function ThemeSynchronizer() {
const { appBridgeState } = useAppBridge();
const { setTheme, themeType } = useTheme();
const { setTheme } = useTheme();
useEffect(() => {
if (!setTheme || !appBridgeState?.theme) {
return;
}
if (themeType !== appBridgeState?.theme) {
setTheme(appBridgeState.theme);
/**
* 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);
if (appBridgeState.theme === "light") {
setTheme("defaultLight");
}
}, [appBridgeState?.theme, setTheme, themeType]);
if (appBridgeState.theme === "dark") {
setTheme("defaultDark");
}
}, [appBridgeState?.theme, setTheme]);
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";
const useStyles = makeStyles({
root: {
display: "grid",
gridTemplateColumns: "280px auto 280px",
alignItems: "start",
gap: 32,
maxWidth: 1180,
margin: "0 auto",
},
});
type Props = PropsWithChildren<{}>;
export function AppColumnsLayout({ children }: Props) {
const styles = useStyles();
return <div className={styles.root}>{children}</div>;
export function AppColumnsLayout({ children }: PropsWithChildren<{}>) {
return (
<Box
display={"grid"}
__gridTemplateColumns={"280px auto 280px"}
gap={4}
__maxWidth={"1180px"}
marginX={"auto"}
marginY={0}
>
{children}
</Box>
);
}

View file

@ -1,92 +1,24 @@
import "@saleor/apps-shared/src/globals.css";
import { StylesProvider, Theme } from "@material-ui/core/styles";
import "@saleor/macaw-ui/next/style";
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
import {
dark,
light,
SaleorThemeColors,
ThemeProvider as MacawUIThemeProvider,
} from "@saleor/macaw-ui";
import React, { PropsWithChildren, useEffect } from "react";
import React from "react";
import { AppProps } from "next/app";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
import { NoSSRWrapper } from "@saleor/apps-shared";
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.
* Long term this will be replaced with Macaw UI 2.x with up to date design tokens
* Ensure instance is a singleton.
*/
const palettes: PalettesOverride = {
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);
}
}, []);
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
function SaleorApp({ Component, pageProps }: AppProps) {
return (
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
<StylesProvider generateClassName={generateClassName}>
<ThemeProvider palettes={palettes} overrides={themeOverrides} ssr>
<ThemeProvider>
<ThemeSynchronizer />
{getLayout(<Component {...pageProps} />)}
<Component {...pageProps} />
</ThemeProvider>
</StylesProvider>
</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 { 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 { AccessWarning } from "../components/AccessWarning/AccessWarning";
import { useAppApi } from "../hooks/useAppApi";
import { AppColumnsLayout } from "../lib/ui/app-columns-layout";
import { useDashboardNotification } from "@saleor/apps-shared";
import { Box, BoxProps, Text, Input, Button } from "@saleor/macaw-ui/next";
interface ConfigurationField {
key: string;
value: string;
}
const useStyles = makeStyles((theme) => ({
confirmButton: {
marginLeft: "auto",
},
fieldContainer: {
marginBottom: theme.spacing(2),
},
}));
function Section(props: PaperProps) {
return <Paper style={{ padding: 24 }} elevation={0} {...props} />;
function Section(props: BoxProps) {
return <Box padding={4} {...props} />;
}
function Instructions() {
@ -46,22 +34,22 @@ function Instructions() {
return (
<Section>
<Typography paragraph variant="h3">
<Text as={"h3"} variant="heading">
How to set up
</Typography>
<Typography paragraph>
App will send events as Klaviyo metrics each time Saleor Event occurs.
</Typography>
<Typography paragraph>
</Text>
<Text as="p">App will send events as Klaviyo metrics each time Saleor Event occurs.</Text>
<Text as="p">
When first metric is sent, it should be available in Klaviyo to build on top of.
</Typography>
<Typography paragraph>
</Text>
<Text as="p">
Metric name can be customized, PUBLIC_TOKEN must be provided to enable the app.
</Typography>
<Typography variant="h3">Useful links</Typography>
<List>
<ListItem>
<Link
</Text>
<Text as={"h3"} variant="heading">
Useful links
</Text>
<ul>
<li>
<a
onClick={(e) => {
e.preventDefault();
@ -70,13 +58,15 @@ function Instructions() {
href="https://github.com/saleor/saleor-app-klaviyo"
>
Visit repository & readme
</Link>
</ListItem>
</List>
<Typography variant="h3">How to configure</Typography>
<List>
<ListItem>
<Link
</a>
</li>
</ul>
<Text as={"h3"} variant="heading">
How to configure
</Text>
<ul>
<li>
<a
onClick={(e) => {
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"
>
Read about public tokens
</Link>
</ListItem>
<ListItem>
<Link
</a>
</li>
<li>
<a
onClick={(e) => {
e.preventDefault();
@ -99,10 +89,10 @@ function Instructions() {
href="https://www.klaviyo.com/account#api-keys-tab"
>
Get public token here
</Link>
</ListItem>
<ListItem>
<Link
</a>
</li>
<li>
<a
onClick={(e) => {
e.preventDefault();
@ -113,19 +103,17 @@ function Instructions() {
href="https://help.klaviyo.com/hc/en-us/articles/115005076787-Guide-to-Managing-Your-Metrics"
>
Read about metrics
</Link>
</ListItem>
</List>
</a>
</li>
</ul>
</Section>
);
}
function Configuration() {
const { appBridgeState } = useAppBridge();
const classes = useStyles();
const { notifySuccess, notifyError } = useDashboardNotification();
const [configuration, setConfiguration] = useState<ConfigurationField[]>();
const [transitionState, setTransitionState] = useState<ConfirmButtonTransitionState>("default");
const { data: configurationData, error } = useAppApi({
url: "/api/configuration",
@ -142,7 +130,6 @@ function Configuration() {
*/
const handleSubmit = (event: SyntheticEvent) => {
event.preventDefault();
setTransitionState("loading");
fetch("/api/configuration", {
method: "POST",
@ -157,12 +144,10 @@ function Configuration() {
if (response.status !== 200) {
throw new Error("Error saving configuration data");
}
setTransitionState("success");
notifySuccess("Success", "Configuration updated successfully");
})
.catch(async () => {
setTransitionState("error");
await notifyError(
"Configuration update failed. Ensure fields are filled correctly and you have MANAGE_APPS permission"
);
@ -207,41 +192,32 @@ function Configuration() {
}
if (configuration === undefined) {
return <Skeleton />;
return <p>Loading...</p>;
}
return (
<AppColumnsLayout>
<div />
<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 }) => (
<div key={key} className={classes.fieldContainer}>
<TextField label={key} name={key} fullWidth onChange={onChange} value={value} />
<div key={key}>
<Input label={key} name={key} onChange={onChange} value={value} />
</div>
))}
<div>
<ConfirmButton
type="submit"
variant="primary"
transitionState={transitionState}
labels={{
confirm: "Save",
error: "Error",
}}
className={classes.confirmButton}
/>
<Button type="submit" variant="primary">
Save
</Button>
</div>
</form>
</Box>
</Section>
<Instructions />
</AppColumnsLayout>
);
}
export default withAuthorization({
notIframe: <AccessWarning cause="not_in_iframe" />,
unmounted: null,
noDashboardToken: <AccessWarning cause="missing_access_token" />,
dashboardTokenInvalid: <AccessWarning cause="invalid_access_token" />,
})(Configuration);
export default 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:*
version: link:../../packages/shared
'@saleor/macaw-ui':
specifier: ^0.7.2
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)
specifier: 0.8.0-pre.95
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':
specifier: 7.55.2
version: 7.55.2(next@13.3.0)(react@18.2.0)
@ -810,6 +810,9 @@ importers:
urql:
specifier: ^4.0.4
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:
specifier: 4.3.9
version: 4.3.9(@types/node@18.15.3)