Invoices, Klaviyo: Refactor to shared components (#989)

This commit is contained in:
Lukasz Ostrowski 2023-09-06 14:22:54 +02:00 committed by GitHub
parent 86bc946b3e
commit 4aee4e11f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 220 additions and 359 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-klaviyo": patch
---
Fixed error where config couldn't be saved

View file

@ -0,0 +1,5 @@
---
"saleor-app-invoices": minor
---
Replace text "loading" messages with skeletons

View file

@ -0,0 +1,5 @@
---
"saleor-app-invoices": minor
---
Redesigned app layout. Now app uses shared sections as other apps.

View file

@ -0,0 +1,5 @@
---
"saleor-app-klaviyo": minor
---
Improved app layout to match modern style.

View file

@ -1,13 +1,18 @@
const { withSentryConfig } = require("@sentry/nextjs");
const isSentryPropertiesInEnvironment = Boolean(
process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_PROJECT && process.env.SENTRY_ORG
process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_PROJECT && process.env.SENTRY_ORG,
);
/** @type {import('next').NextConfig} */
const nextConfig = {
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",
"@saleor/trpc",
],
};
const configWithSentry = withSentryConfig(
@ -23,9 +28,7 @@ const configWithSentry = withSentryConfig(
tunnelRoute: "/monitoring",
hideSourceMaps: true,
disableLogger: true,
}
},
);
module.exports = isSentryPropertiesInEnvironment ? configWithSentry : nextConfig;

View file

@ -15,7 +15,9 @@
"@hookform/resolvers": "^3.1.0",
"@saleor/app-sdk": "0.41.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127",
"@saleor/trpc": "workspace:*",
"@sentry/nextjs": "7.67.0",
"@tanstack/react-query": "4.29.19",
"@trpc/client": "10.34.0",

View file

@ -1,25 +0,0 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useTheme } from "@saleor/macaw-ui/next";
import { memo, useEffect } from "react";
// todo move to shared
export function ThemeSynchronizer() {
const { appBridgeState } = useAppBridge();
const { setTheme } = useTheme();
useEffect(() => {
if (!setTheme || !appBridgeState?.theme) {
return;
}
if (appBridgeState.theme === "light") {
setTheme("defaultLight");
}
if (appBridgeState.theme === "dark") {
setTheme("defaultDark");
}
}, [appBridgeState?.theme, setTheme]);
return null;
}

View file

@ -53,6 +53,8 @@ describe("appConfigurationRouter", function () {
token: "TOKEN",
saleorApiUrl: "http://localhost:8000/graphql/",
appId: "app",
ssr: true,
baseUrl: "localhost:3000",
})
.upsertChannelOverride({
channelSlug: "test",

View file

@ -1,13 +1,14 @@
import { Controller, useForm } from "react-hook-form";
import React, { useCallback, useEffect } from "react";
import { Box, Button, Input, Text } from "@saleor/macaw-ui/next";
import { SellerAddress } from "../address";
import { trpcClient } from "../../trpc/trpc-client";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useDashboardNotification } from "@saleor/apps-shared";
import { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
import { Box, Button, Input, Text } from "@saleor/macaw-ui/next";
import { useRouter } from "next/router";
import { useCallback } from "react";
import { z } from "zod";
import { trpcClient } from "../../trpc/trpc-client";
import { SellerAddress } from "../address";
import { AddressV2Schema, AddressV2Shape } from "../schema-v2/app-config-schema.v2";
type Props = {
@ -57,12 +58,29 @@ export const AddressForm = (props: Props & InnerFormProps) => {
});
return (
<form
<Layout.AppSectionCard
as="form"
footer={
<ButtonsBox>
<Button
variant="tertiary"
onClick={(e) => {
e.stopPropagation();
props.onCancel();
}}
>
<Text color={"textNeutralSubdued"}>Cancel</Text>
</Button>
<Button type="submit" variant="primary">
Save
</Button>
</ButtonsBox>
}
onSubmit={handleSubmit((data, event) => {
return props.onSubmit(data);
})}
>
<Box display={"grid"} gap={3} marginBottom={9}>
<Box display={"grid"} gap={3}>
{fieldsBlock1.map((fieldName) => (
<Controller
key={fieldName}
@ -128,21 +146,7 @@ export const AddressForm = (props: Props & InnerFormProps) => {
/>
))}
</Box>
<Box display={"grid"} justifyContent={"flex-end"} gap={1.5} gridAutoFlow={"column"}>
<Button
variant="tertiary"
onClick={(e) => {
e.stopPropagation();
props.onCancel();
}}
>
<Text color={"textNeutralSubdued"}>Cancel</Text>
</Button>
<Button type="submit" variant="primary">
Save
</Button>
</Box>
</form>
</Layout.AppSectionCard>
);
};
@ -164,9 +168,6 @@ export const ConnectedAddressForm = (props: Props) => {
const { push } = useRouter();
const addressData =
channelOverrideConfigQuery.data && channelOverrideConfigQuery.data[props.channelSlug];
const submitHandler = useCallback(
async (data: AddressV2Shape) => {
return upsertConfigMutation.mutate({
@ -174,7 +175,7 @@ export const ConnectedAddressForm = (props: Props) => {
channelSlug: props.channelSlug,
});
},
[props.channelSlug, upsertConfigMutation]
[props.channelSlug, upsertConfigMutation],
);
const onCancelHandler = useCallback(() => {
@ -182,7 +183,7 @@ export const ConnectedAddressForm = (props: Props) => {
}, [push]);
if (channelOverrideConfigQuery.isLoading) {
return <Text color={"textNeutralSubdued"}>Loading</Text>;
return <SkeletonLayout.Section />;
}
return (

View file

@ -1,21 +1,13 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import { DefaultShopAddress } from "../../shop-info/ui/default-shop-address";
import { AppSection } from "../../ui/AppSection";
import { PerChannelConfigList } from "../../channels/ui/per-channel-config-list";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { Layout } from "@saleor/apps-ui";
export const AppConfigView = () => {
const { appBridge } = useAppBridge();
return (
<Box>
<Box
display={"grid"}
justifyContent={"space-between"}
__gridTemplateColumns={"400px 400px"}
gap={10}
__marginBottom={"200px"}
>
<Box>
<Text as={"h1"} variant={"hero"} marginBottom={5}>
Configuration
@ -24,6 +16,10 @@ export const AppConfigView = () => {
The Invoices App will generate invoices for each order, for which{" "}
<code>INVOICE_REQUESTED</code> event will be triggered
</Text>
<Layout.AppSection
marginTop={10}
heading={"Default address of the shop"}
sideContent={
<Text as={"p"} marginBottom={1.5}>
By default it will use{" "}
<a
@ -32,7 +28,7 @@ export const AppConfigView = () => {
appBridge?.dispatch(
actions.Redirect({
to: "/site-settings",
})
}),
);
}}
>
@ -40,22 +36,25 @@ export const AppConfigView = () => {
</a>{" "}
address, but each channel can be configured separately
</Text>
</Box>
<Box>
}
>
<DefaultShopAddress />
</Box>
</Box>
<AppSection
includePadding={true}
</Layout.AppSection>
<Layout.AppSection
marginTop={10}
heading={"Shop address per channel"}
mainContent={<PerChannelConfigList />}
sideContent={
<Text>
Configure custom billing address for each channel. If not set, default shop address will
be used
</Text>
}
/>
>
<Layout.AppSectionCard>
<PerChannelConfigList />
</Layout.AppSectionCard>
</Layout.AppSection>
</Box>
);
};

View file

@ -1,9 +1,9 @@
import { Box, ChevronRightIcon, Text, Button } from "@saleor/macaw-ui/next";
import { AppSection } from "../../ui/AppSection";
import { useRouter } from "next/router";
import { ConnectedAddressForm } from "../ui/address-form";
import { trpcClient } from "../../trpc/trpc-client";
import { useDashboardNotification } from "@saleor/apps-shared";
import { Layout } from "@saleor/apps-ui";
export const ChannelConfigView = () => {
const {
@ -15,7 +15,7 @@ export const ChannelConfigView = () => {
const { notifySuccess } = useDashboardNotification();
if (!channel) {
return null;
return null; // TODO: error
}
return (
@ -29,10 +29,9 @@ export const ChannelConfigView = () => {
<Text>{channel}</Text>
</Box>
</Box>
<AppSection
<Layout.AppSection
includePadding={true}
heading={"Shop address per channel"}
mainContent={<ConnectedAddressForm channelSlug={channel as string} />}
sideContent={
<Box>
<Text marginBottom={5} as={"p"}>
@ -51,7 +50,9 @@ export const ChannelConfigView = () => {
</Button>
</Box>
}
/>
>
<ConnectedAddressForm channelSlug={channel as string} />
</Layout.AppSection>
</Box>
);
};

View file

@ -1,6 +1,7 @@
import { Box, Text, Chip, Button } from "@saleor/macaw-ui/next";
import { trpcClient } from "../../trpc/trpc-client";
import { useRouter } from "next/router";
import { SkeletonLayout } from "@saleor/apps-ui";
const defaultAddressChip = (
<Chip __display={"inline-block"} size={"large"}>
@ -17,7 +18,7 @@ export const PerChannelConfigList = () => {
const { push } = useRouter();
if (shopChannelsQuery.isLoading || channelsOverridesQuery.isLoading) {
return <Text color={"textNeutralSubdued"}>Loading...</Text>;
return <SkeletonLayout.Section />;
}
const renderChannelAddress = (slug: string) => {

View file

@ -2,30 +2,31 @@ import { Box, Text, Button } from "@saleor/macaw-ui/next";
import { trpcClient } from "../../trpc/trpc-client";
import { PropsWithChildren } from "react";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
const Wrapper = ({ children }: PropsWithChildren<{}>) => {
const { appBridge } = useAppBridge();
return (
<Box>
<Box display={"flex"} justifyContent={"space-between"} marginBottom={5}>
<Text variant={"heading"}>Default address of the shop</Text>
<Layout.AppSectionCard
footer={
<ButtonsBox>
<Button
size={"small"}
variant={"tertiary"}
onClick={() => {
appBridge?.dispatch(
actions.Redirect({
to: "/site-settings",
})
}),
);
}}
>
<Text color={"textNeutralSubdued"}>Edit</Text>
Edit
</Button>
</Box>
</ButtonsBox>
}
>
<Box>{children}</Box>
</Box>
</Layout.AppSectionCard>
);
};
@ -46,7 +47,7 @@ export const DefaultShopAddress = () => {
if (isLoading) {
return (
<Wrapper>
<Text color={"textNeutralSubdued"}>Loading...</Text>
<SkeletonLayout.Section />
</Wrapper>
);
}
@ -70,6 +71,9 @@ export const DefaultShopAddress = () => {
if (data && data.companyAddress) {
return (
<Wrapper>
<Text as="p" marginBottom={4} variant="caption">
This address will be used if custom address is not set for channel
</Text>
<Text size={"small"} as={"p"}>
{data.companyAddress.companyName}
</Text>

View file

@ -1,34 +1,13 @@
import { httpBatchLink } from "@trpc/client";
import { createTRPCNext } from "@trpc/next";
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
import { createHttpBatchLink } from "@saleor/trpc";
import { appBridgeInstance } from "../../pages/_app";
import { AppRouter } from "./trpc-app-router";
function getBaseUrl() {
if (typeof window !== "undefined") return "";
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
return `http://localhost:${process.env.PORT ?? 3000}`;
}
export const trpcClient = createTRPCNext<AppRouter>({
config({ ctx }) {
config() {
return {
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
headers() {
return {
/**
* Attach headers from app to client requests, so tRPC can add them to context
*/
[SALEOR_AUTHORIZATION_BEARER_HEADER]: appBridgeInstance?.getState().token,
[SALEOR_API_URL_HEADER]: appBridgeInstance?.getState().saleorApiUrl,
};
},
}),
],
links: [createHttpBatchLink(appBridgeInstance)],
// queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
};
},

View file

@ -1,13 +0,0 @@
import * as trpcNext from "@trpc/server/adapters/next";
import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
import { inferAsyncReturnType } from "@trpc/server";
export const createTrpcContext = async ({ res, req }: trpcNext.CreateNextContextOptions) => {
return {
token: req.headers[SALEOR_AUTHORIZATION_BEARER_HEADER] as string | undefined,
saleorApiUrl: req.headers[SALEOR_API_URL_HEADER] as string | undefined,
appId: undefined as undefined | string,
};
};
export type TrpcContext = inferAsyncReturnType<typeof createTrpcContext>;

View file

@ -1,6 +1,7 @@
import { initTRPC } from "@trpc/server";
import { TrpcContext } from "./trpc-context";
import { Permission } from "@saleor/app-sdk/types";
import { TrpcContext } from "@saleor/trpc";
interface Meta {
requiredClientPermissions?: Permission[];

View file

@ -1,36 +0,0 @@
import { Box, PropsWithBox, Text } from "@saleor/macaw-ui/next";
import { ReactNode } from "react";
// todo move to shared
export const AppSection = ({
heading,
sideContent,
mainContent,
includePadding = false,
...props
}: PropsWithBox<{
heading: string;
sideContent?: ReactNode;
mainContent: ReactNode;
includePadding?: boolean;
}>) => {
return (
<Box as="section" __gridTemplateColumns={"400px auto"} display={"grid"} gap={10} {...props}>
<Box>
<Text as="h2" variant={"heading"} size={"large"} marginBottom={1.5}>
{heading}
</Text>
{sideContent}
</Box>
<Box
borderStyle={"solid"}
borderColor={"neutralPlain"}
borderWidth={1}
padding={includePadding ? 5 : 0}
borderRadius={4}
>
{mainContent}
</Box>
</Box>
);
};

View file

@ -6,12 +6,11 @@ import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import React, { ReactElement } from "react";
import { AppProps } from "next/app";
import { NoSSRWrapper } from "@saleor/apps-shared";
import { NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared";
import { trpcClient } from "../modules/trpc/trpc-client";
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
import { NextPage } from "next";
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
/**
* Ensure instance is a singleton.

View file

@ -1,7 +1,7 @@
import * as trpcNext from "@trpc/server/adapters/next";
import { createTrpcContext } from "../../../modules/trpc/trpc-context";
import { appRouter } from "../../../modules/trpc/trpc-app-router";
import { createLogger } from "@saleor/apps-shared";
import { createTrpcContext } from "@saleor/trpc";
const logger = createLogger({ name: "tRPC error" });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,4 +0,0 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,8 +1,3 @@
body {
font-family: Inter, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
a {
cursor: pointer;
text-decoration: none;

View file

@ -16,6 +16,7 @@
"@material-ui/lab": "4.0.0-alpha.61",
"@saleor/app-sdk": "0.41.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.127",
"@sentry/nextjs": "7.67.0",
"@urql/exchange-auth": "^2.1.4",

View file

@ -1,25 +0,0 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useTheme } from "@saleor/macaw-ui/next";
import { useEffect } from "react";
// todo move to shared
export function ThemeSynchronizer() {
const { appBridgeState } = useAppBridge();
const { setTheme } = useTheme();
useEffect(() => {
if (!setTheme || !appBridgeState?.theme) {
return;
}
if (appBridgeState.theme === "light") {
setTheme("defaultLight");
}
if (appBridgeState.theme === "dark") {
setTheme("defaultDark");
}
}, [appBridgeState?.theme, setTheme]);
return null;
}

View file

@ -1,17 +0,0 @@
import { Box } from "@saleor/macaw-ui/next";
import { PropsWithChildren } from "react";
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,11 +1,8 @@
import "@saleor/macaw-ui/next/style";
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
import React from "react";
import { AppProps } from "next/app";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import { NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared";
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
import { NoSSRWrapper } from "@saleor/apps-shared";
import { ThemeSynchronizer } from "../hooks/theme-synchronizer";
import "@saleor/macaw-ui/next/style";
import { AppProps } from "next/app";
/**
* Ensure instance is a singleton.
@ -18,7 +15,9 @@ function SaleorApp({ Component, pageProps }: AppProps) {
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
<ThemeProvider>
<ThemeSynchronizer />
<Box padding={10}>
<Component {...pageProps} />
</Box>
</ThemeProvider>
</AppBridgeProvider>
</NoSSRWrapper>

View file

@ -3,13 +3,8 @@ import { Head, Html, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<Head />
<link
href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<body>
<Main />
<NextScript />

View file

@ -61,12 +61,20 @@ const handler: NextProtectedApiHandler = async (request, res, ctx) => {
data: await getAppSettings(settings),
});
case "POST": {
await settings.set((request.body as PostRequestBody).data);
try {
await settings.set((JSON.parse(request.body) as PostRequestBody).data);
return res.json({
success: true,
data: await getAppSettings(settings),
});
} catch (e) {
console.error(e);
return res.json({
success: false,
});
}
}
default:
return res.status(405).end();

View file

@ -8,17 +8,17 @@ import { orderCreatedWebhook } from "./webhooks/order-created";
import { orderFullyPaidWebhook } from "./webhooks/order-fully-paid";
const handler = createManifestHandler({
async manifestFactory(context): Promise<AppManifest> {
const { appBaseUrl } = context;
async manifestFactory({ appBaseUrl }): Promise<AppManifest> {
const iframeBaseUrl = process.env.APP_IFRAME_BASE_URL ?? appBaseUrl;
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
return {
about:
"Klaviyo integration allows sending Klaviyo notifications on Saleor events.",
appUrl: appBaseUrl,
about: "Klaviyo integration allows sending Klaviyo notifications on Saleor events.",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {
logo: {
default: `${context.appBaseUrl}/logo.png`,
default: `${apiBaseURL}/logo.png`,
},
},
dataPrivacyUrl: "https://saleor.io/legal/privacy/",
@ -27,7 +27,7 @@ const handler = createManifestHandler({
name: "Klaviyo",
permissions: ["MANAGE_USERS", "MANAGE_ORDERS"],
supportUrl: "https://github.com/saleor/apps/discussions",
tokenTargetUrl: `${appBaseUrl}/api/register`,
tokenTargetUrl: `${apiBaseURL}/api/register`,
version: pkg.version,
webhooks: [
customerCreatedWebhook.getWebhookManifest(appBaseUrl),

View file

@ -1,47 +1,31 @@
import { useAppBridge, withAuthorization } from "@saleor/app-sdk/app-bridge";
import { useAppBridge, useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge";
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
import { ChangeEvent, SyntheticEvent, useEffect, useState } from "react";
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";
import { Breadcrumbs, ButtonsBox, Layout, SkeletonLayout, TextLink } from "@saleor/apps-ui";
import { Box, Button, Input, Text } from "@saleor/macaw-ui/next";
import { useAppApi } from "../hooks/useAppApi";
interface ConfigurationField {
key: string;
value: string;
}
function Section(props: BoxProps) {
return <Box padding={4} {...props} />;
}
function Instructions() {
const { appBridge } = useAppBridge();
const openExternalUrl = (url: string) => {
// eslint-disable-next-line
appBridge?.dispatch({
type: "redirect",
payload: {
newContext: true,
actionId: "redirect_from_klaviyo_app",
to: url,
},
});
};
return (
<Section>
<Text as={"h3"} variant="heading">
<Box>
<Text as={"h3"} variant="heading" marginY={4}>
How to set up
</Text>
<Text as="p">App will send events as Klaviyo metrics each time Saleor Event occurs.</Text>
<Text as="p">
<Text as="p" marginBottom={2}>
App will send events as Klaviyo metrics each time Saleor Event occurs.
</Text>
<Text as="p" marginBottom={2}>
When first metric is sent, it should be available in Klaviyo to build on top of.
</Text>
<Text as="p">
<Text as="p" marginBottom={4}>
Metric name can be customized, PUBLIC_TOKEN must be provided to enable the app.
</Text>
<Text as={"h3"} variant="heading">
@ -49,16 +33,9 @@ function Instructions() {
</Text>
<ul>
<li>
<a
onClick={(e) => {
e.preventDefault();
openExternalUrl("https://github.com/saleor/saleor-app-klaviyo");
}}
href="https://github.com/saleor/saleor-app-klaviyo"
>
<TextLink href="https://github.com/saleor/saleor-app-klaviyo" newTab>
Visit repository & readme
</a>
</TextLink>
</li>
</ul>
<Text as={"h3"} variant="heading">
@ -66,47 +43,28 @@ function Instructions() {
</Text>
<ul>
<li>
<a
onClick={(e) => {
e.preventDefault();
openExternalUrl(
"https://help.klaviyo.com/hc/en-us/articles/115005062267-How-to-Manage-Your-Account-s-API-Keys"
);
}}
<TextLink
href="https://help.klaviyo.com/hc/en-us/articles/115005062267-How-to-Manage-Your-Account-s-API-Keys"
newTab
>
Read about public tokens
</a>
</TextLink>
</li>
<li>
<a
onClick={(e) => {
e.preventDefault();
openExternalUrl("https://www.klaviyo.com/account#api-keys-tab");
}}
href="https://www.klaviyo.com/account#api-keys-tab"
>
<TextLink href="https://www.klaviyo.com/account#api-keys-tab" newTab>
Get public token here
</a>
</TextLink>
</li>
<li>
<a
onClick={(e) => {
e.preventDefault();
openExternalUrl(
"https://help.klaviyo.com/hc/en-us/articles/115005076787-Guide-to-Managing-Your-Metrics"
);
}}
<TextLink
href="https://help.klaviyo.com/hc/en-us/articles/115005076787-Guide-to-Managing-Your-Metrics"
newTab
>
Read about metrics
</a>
</TextLink>
</li>
</ul>
</Section>
</Box>
);
}
@ -114,6 +72,7 @@ function Configuration() {
const { appBridgeState } = useAppBridge();
const { notifySuccess, notifyError } = useDashboardNotification();
const [configuration, setConfiguration] = useState<ConfigurationField[]>();
const authenticatedFetch = useAuthenticatedFetch() as typeof window.fetch;
const { data: configurationData, error } = useAppApi({
url: "/api/configuration",
@ -131,13 +90,8 @@ function Configuration() {
const handleSubmit = (event: SyntheticEvent) => {
event.preventDefault();
fetch("/api/configuration", {
authenticatedFetch("/api/configuration", {
method: "POST",
headers: [
["content-type", "application/json"],
[SALEOR_API_URL_HEADER, appBridgeState?.saleorApiUrl!],
[SALEOR_AUTHORIZATION_BEARER_HEADER, appBridgeState?.token!],
],
body: JSON.stringify({ data: configuration }),
})
.then(async (response) => {
@ -149,7 +103,7 @@ function Configuration() {
})
.catch(async () => {
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",
);
});
};
@ -158,7 +112,7 @@ function Configuration() {
const { name, value } = event.target as HTMLInputElement;
setConfiguration((prev) =>
prev!.map((prevField) => (prevField.key === name ? { ...prevField, value } : prevField))
prev!.map((prevField) => (prevField.key === name ? { ...prevField, value } : prevField)),
);
};
@ -192,31 +146,36 @@ function Configuration() {
}
if (configuration === undefined) {
return <p>Loading...</p>;
return <SkeletonLayout.Section />;
}
return (
<AppColumnsLayout>
<div />
<Section>
<Text variant={"heading"} marginBottom={4} as={"h2"}>
Klaviyo configuration
</Text>
<Box as={"form"} display={"grid"} gap={4} gridAutoFlow={"row"} onSubmit={handleSubmit}>
<Box>
<Breadcrumbs marginBottom={10}>
<Breadcrumbs.Item>Configuration</Breadcrumbs.Item>
</Breadcrumbs>
<Layout.AppSection heading="Set up integration" sideContent={<Instructions />}>
<Layout.AppSectionCard
as={"form"}
onSubmit={handleSubmit}
footer={
<ButtonsBox>
<Button type="submit" variant="primary">
Save
</Button>
</ButtonsBox>
}
>
<Box display={"grid"} gap={4} gridAutoFlow={"row"}>
{configuration!.map(({ key, value }) => (
<div key={key}>
<Input label={key} name={key} onChange={onChange} value={value} />
</div>
))}
<div>
<Button type="submit" variant="primary">
Save
</Button>
</div>
</Box>
</Section>
<Instructions />
</AppColumnsLayout>
</Layout.AppSectionCard>
</Layout.AppSection>
</Box>
);
}

View file

@ -18,7 +18,9 @@
"SENTRY_PROJECT",
"SENTRY_AUTH_TOKEN",
"NEXT_PUBLIC_VERCEL_ENV",
"SENTRY_ENVIRONMENT"
"SENTRY_ENVIRONMENT",
"APP_API_BASE_URL",
"APP_IFRAME_BASE_URL"
]
}
}

View file

@ -67,7 +67,8 @@
"vals",
"urql",
"Protos",
"pino"
"pino",
"IFRAME"
],
"ignorePaths": [
"node_modules",

View file

@ -18,6 +18,7 @@
},
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@sentry/cli": "2.20.6",
"@types/node": "18.15.3",
"cspell": "^7.2.0",
"eslint": "8.46.0",
@ -27,8 +28,7 @@
"next": "13.4.8",
"prettier": "3.0.3",
"syncpack": "10.9.3",
"turbo": "1.10.12",
"@sentry/cli": "2.20.6"
"turbo": "1.10.12"
},
"engines": {
"node": ">=18.0.0"

View file

@ -642,9 +642,15 @@ importers:
'@saleor/apps-shared':
specifier: workspace:*
version: link:../../packages/shared
'@saleor/apps-ui':
specifier: workspace:*
version: link:../../packages/ui
'@saleor/macaw-ui':
specifier: 0.8.0-pre.127
version: 0.8.0-pre.127(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
'@saleor/trpc':
specifier: workspace:*
version: link:../../packages/trpc
'@sentry/nextjs':
specifier: 7.67.0
version: 7.67.0(next@13.4.8)(react@18.2.0)
@ -787,6 +793,9 @@ importers:
'@saleor/apps-shared':
specifier: workspace:*
version: link:../../packages/shared
'@saleor/apps-ui':
specifier: workspace:*
version: link:../../packages/ui
'@saleor/macaw-ui':
specifier: 0.8.0-pre.127
version: 0.8.0-pre.127(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)