update macaw in klaviyo (#670)
* update macaw in klaviyo * cr fixes * removed e2e artifacts
This commit is contained in:
parent
3bd7e3f05d
commit
37e50db29c
12 changed files with 131 additions and 267 deletions
5
.changeset/proud-phones-change.md
Normal file
5
.changeset/proud-phones-change.md
Normal 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.
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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't be accessed outside of the Saleor Dashboard
|
||||
</Typography>
|
||||
<Typography variant="subtitle2" style={{ marginTop: "2rem" }}>
|
||||
❌ {warnings[cause]}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 };
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
32
apps/klaviyo/src/pages/index.tsx
Normal file
32
apps/klaviyo/src/pages/index.tsx
Normal 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;
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue