Invoices, Klaviyo: Refactor to shared components (#989)
This commit is contained in:
parent
86bc946b3e
commit
4aee4e11f8
34 changed files with 220 additions and 359 deletions
5
.changeset/nine-rivers-flow.md
Normal file
5
.changeset/nine-rivers-flow.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-klaviyo": patch
|
||||
---
|
||||
|
||||
Fixed error where config couldn't be saved
|
5
.changeset/real-pigs-promise.md
Normal file
5
.changeset/real-pigs-promise.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-invoices": minor
|
||||
---
|
||||
|
||||
Replace text "loading" messages with skeletons
|
5
.changeset/twelve-pianos-relate.md
Normal file
5
.changeset/twelve-pianos-relate.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-invoices": minor
|
||||
---
|
||||
|
||||
Redesigned app layout. Now app uses shared sections as other apps.
|
5
.changeset/wicked-llamas-talk.md
Normal file
5
.changeset/wicked-llamas-talk.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-klaviyo": minor
|
||||
---
|
||||
|
||||
Improved app layout to match modern style.
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -53,6 +53,8 @@ describe("appConfigurationRouter", function () {
|
|||
token: "TOKEN",
|
||||
saleorApiUrl: "http://localhost:8000/graphql/",
|
||||
appId: "app",
|
||||
ssr: true,
|
||||
baseUrl: "localhost:3000",
|
||||
})
|
||||
.upsertChannelOverride({
|
||||
channelSlug: "test",
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -1,29 +1,25 @@
|
|||
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
|
||||
</Text>
|
||||
<Text as={"p"} marginBottom={1.5}>
|
||||
The Invoices App will generate invoices for each order, for which{" "}
|
||||
<code>INVOICE_REQUESTED</code> event will be triggered
|
||||
</Text>
|
||||
<Text as={"h1"} variant={"hero"} marginBottom={5}>
|
||||
Configuration
|
||||
</Text>
|
||||
<Text as={"p"} marginBottom={1.5}>
|
||||
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}
|
||||
}
|
||||
>
|
||||
<DefaultShopAddress />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
<Button
|
||||
size={"small"}
|
||||
variant={"tertiary"}
|
||||
onClick={() => {
|
||||
appBridge?.dispatch(
|
||||
actions.Redirect({
|
||||
to: "/site-settings",
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Text color={"textNeutralSubdued"}>Edit</Text>
|
||||
</Button>
|
||||
</Box>
|
||||
<Layout.AppSectionCard
|
||||
footer={
|
||||
<ButtonsBox>
|
||||
<Button
|
||||
onClick={() => {
|
||||
appBridge?.dispatch(
|
||||
actions.Redirect({
|
||||
to: "/site-settings",
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</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>
|
||||
|
|
|
@ -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 } } },
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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>;
|
|
@ -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[];
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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.
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 />
|
||||
<Component {...pageProps} />
|
||||
<Box padding={10}>
|
||||
<Component {...pageProps} />
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
</AppBridgeProvider>
|
||||
</NoSSRWrapper>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
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();
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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}>
|
||||
{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>
|
||||
<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>
|
||||
))}
|
||||
</Box>
|
||||
</Layout.AppSectionCard>
|
||||
</Layout.AppSection>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,8 @@
|
|||
"vals",
|
||||
"urql",
|
||||
"Protos",
|
||||
"pino"
|
||||
"pino",
|
||||
"IFRAME"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"node_modules",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue