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 { withSentryConfig } = require("@sentry/nextjs");
|
||||||
|
|
||||||
const isSentryPropertiesInEnvironment = Boolean(
|
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} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
|
transpilePackages: [
|
||||||
|
"@saleor/apps-shared",
|
||||||
|
"@saleor/apps-ui",
|
||||||
|
"@saleor/react-hook-form-macaw",
|
||||||
|
"@saleor/trpc",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const configWithSentry = withSentryConfig(
|
const configWithSentry = withSentryConfig(
|
||||||
|
@ -23,9 +28,7 @@ const configWithSentry = withSentryConfig(
|
||||||
tunnelRoute: "/monitoring",
|
tunnelRoute: "/monitoring",
|
||||||
hideSourceMaps: true,
|
hideSourceMaps: true,
|
||||||
disableLogger: true,
|
disableLogger: true,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
module.exports = isSentryPropertiesInEnvironment ? configWithSentry : nextConfig;
|
module.exports = isSentryPropertiesInEnvironment ? configWithSentry : nextConfig;
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,9 @@
|
||||||
"@hookform/resolvers": "^3.1.0",
|
"@hookform/resolvers": "^3.1.0",
|
||||||
"@saleor/app-sdk": "0.41.1",
|
"@saleor/app-sdk": "0.41.1",
|
||||||
"@saleor/apps-shared": "workspace:*",
|
"@saleor/apps-shared": "workspace:*",
|
||||||
|
"@saleor/apps-ui": "workspace:*",
|
||||||
"@saleor/macaw-ui": "0.8.0-pre.127",
|
"@saleor/macaw-ui": "0.8.0-pre.127",
|
||||||
|
"@saleor/trpc": "workspace:*",
|
||||||
"@sentry/nextjs": "7.67.0",
|
"@sentry/nextjs": "7.67.0",
|
||||||
"@tanstack/react-query": "4.29.19",
|
"@tanstack/react-query": "4.29.19",
|
||||||
"@trpc/client": "10.34.0",
|
"@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",
|
token: "TOKEN",
|
||||||
saleorApiUrl: "http://localhost:8000/graphql/",
|
saleorApiUrl: "http://localhost:8000/graphql/",
|
||||||
appId: "app",
|
appId: "app",
|
||||||
|
ssr: true,
|
||||||
|
baseUrl: "localhost:3000",
|
||||||
})
|
})
|
||||||
.upsertChannelOverride({
|
.upsertChannelOverride({
|
||||||
channelSlug: "test",
|
channelSlug: "test",
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Controller, useForm } from "react-hook-form";
|
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 { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
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 { 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";
|
import { AddressV2Schema, AddressV2Shape } from "../schema-v2/app-config-schema.v2";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -57,12 +58,29 @@ export const AddressForm = (props: Props & InnerFormProps) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
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) => {
|
onSubmit={handleSubmit((data, event) => {
|
||||||
return props.onSubmit(data);
|
return props.onSubmit(data);
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box display={"grid"} gap={3} marginBottom={9}>
|
<Box display={"grid"} gap={3}>
|
||||||
{fieldsBlock1.map((fieldName) => (
|
{fieldsBlock1.map((fieldName) => (
|
||||||
<Controller
|
<Controller
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
|
@ -128,21 +146,7 @@ export const AddressForm = (props: Props & InnerFormProps) => {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
<Box display={"grid"} justifyContent={"flex-end"} gap={1.5} gridAutoFlow={"column"}>
|
</Layout.AppSectionCard>
|
||||||
<Button
|
|
||||||
variant="tertiary"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
props.onCancel();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text color={"textNeutralSubdued"}>Cancel</Text>
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" variant="primary">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -164,9 +168,6 @@ export const ConnectedAddressForm = (props: Props) => {
|
||||||
|
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
|
||||||
const addressData =
|
|
||||||
channelOverrideConfigQuery.data && channelOverrideConfigQuery.data[props.channelSlug];
|
|
||||||
|
|
||||||
const submitHandler = useCallback(
|
const submitHandler = useCallback(
|
||||||
async (data: AddressV2Shape) => {
|
async (data: AddressV2Shape) => {
|
||||||
return upsertConfigMutation.mutate({
|
return upsertConfigMutation.mutate({
|
||||||
|
@ -174,7 +175,7 @@ export const ConnectedAddressForm = (props: Props) => {
|
||||||
channelSlug: props.channelSlug,
|
channelSlug: props.channelSlug,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[props.channelSlug, upsertConfigMutation]
|
[props.channelSlug, upsertConfigMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onCancelHandler = useCallback(() => {
|
const onCancelHandler = useCallback(() => {
|
||||||
|
@ -182,7 +183,7 @@ export const ConnectedAddressForm = (props: Props) => {
|
||||||
}, [push]);
|
}, [push]);
|
||||||
|
|
||||||
if (channelOverrideConfigQuery.isLoading) {
|
if (channelOverrideConfigQuery.isLoading) {
|
||||||
return <Text color={"textNeutralSubdued"}>Loading</Text>;
|
return <SkeletonLayout.Section />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,21 +1,13 @@
|
||||||
import { Box, Text } from "@saleor/macaw-ui/next";
|
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||||
import { DefaultShopAddress } from "../../shop-info/ui/default-shop-address";
|
import { DefaultShopAddress } from "../../shop-info/ui/default-shop-address";
|
||||||
import { AppSection } from "../../ui/AppSection";
|
|
||||||
import { PerChannelConfigList } from "../../channels/ui/per-channel-config-list";
|
import { PerChannelConfigList } from "../../channels/ui/per-channel-config-list";
|
||||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { Layout } from "@saleor/apps-ui";
|
||||||
|
|
||||||
export const AppConfigView = () => {
|
export const AppConfigView = () => {
|
||||||
const { appBridge } = useAppBridge();
|
const { appBridge } = useAppBridge();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
|
||||||
<Box
|
|
||||||
display={"grid"}
|
|
||||||
justifyContent={"space-between"}
|
|
||||||
__gridTemplateColumns={"400px 400px"}
|
|
||||||
gap={10}
|
|
||||||
__marginBottom={"200px"}
|
|
||||||
>
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text as={"h1"} variant={"hero"} marginBottom={5}>
|
<Text as={"h1"} variant={"hero"} marginBottom={5}>
|
||||||
Configuration
|
Configuration
|
||||||
|
@ -24,6 +16,10 @@ export const AppConfigView = () => {
|
||||||
The Invoices App will generate invoices for each order, for which{" "}
|
The Invoices App will generate invoices for each order, for which{" "}
|
||||||
<code>INVOICE_REQUESTED</code> event will be triggered
|
<code>INVOICE_REQUESTED</code> event will be triggered
|
||||||
</Text>
|
</Text>
|
||||||
|
<Layout.AppSection
|
||||||
|
marginTop={10}
|
||||||
|
heading={"Default address of the shop"}
|
||||||
|
sideContent={
|
||||||
<Text as={"p"} marginBottom={1.5}>
|
<Text as={"p"} marginBottom={1.5}>
|
||||||
By default it will use{" "}
|
By default it will use{" "}
|
||||||
<a
|
<a
|
||||||
|
@ -32,7 +28,7 @@ export const AppConfigView = () => {
|
||||||
appBridge?.dispatch(
|
appBridge?.dispatch(
|
||||||
actions.Redirect({
|
actions.Redirect({
|
||||||
to: "/site-settings",
|
to: "/site-settings",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -40,22 +36,25 @@ export const AppConfigView = () => {
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
address, but each channel can be configured separately
|
address, but each channel can be configured separately
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
}
|
||||||
<Box>
|
>
|
||||||
<DefaultShopAddress />
|
<DefaultShopAddress />
|
||||||
</Box>
|
</Layout.AppSection>
|
||||||
</Box>
|
|
||||||
<AppSection
|
<Layout.AppSection
|
||||||
includePadding={true}
|
marginTop={10}
|
||||||
heading={"Shop address per channel"}
|
heading={"Shop address per channel"}
|
||||||
mainContent={<PerChannelConfigList />}
|
|
||||||
sideContent={
|
sideContent={
|
||||||
<Text>
|
<Text>
|
||||||
Configure custom billing address for each channel. If not set, default shop address will
|
Configure custom billing address for each channel. If not set, default shop address will
|
||||||
be used
|
be used
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
<Layout.AppSectionCard>
|
||||||
|
<PerChannelConfigList />
|
||||||
|
</Layout.AppSectionCard>
|
||||||
|
</Layout.AppSection>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Box, ChevronRightIcon, Text, Button } from "@saleor/macaw-ui/next";
|
import { Box, ChevronRightIcon, Text, Button } from "@saleor/macaw-ui/next";
|
||||||
import { AppSection } from "../../ui/AppSection";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ConnectedAddressForm } from "../ui/address-form";
|
import { ConnectedAddressForm } from "../ui/address-form";
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
import { Layout } from "@saleor/apps-ui";
|
||||||
|
|
||||||
export const ChannelConfigView = () => {
|
export const ChannelConfigView = () => {
|
||||||
const {
|
const {
|
||||||
|
@ -15,7 +15,7 @@ export const ChannelConfigView = () => {
|
||||||
const { notifySuccess } = useDashboardNotification();
|
const { notifySuccess } = useDashboardNotification();
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
return null;
|
return null; // TODO: error
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -29,10 +29,9 @@ export const ChannelConfigView = () => {
|
||||||
<Text>{channel}</Text>
|
<Text>{channel}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<AppSection
|
<Layout.AppSection
|
||||||
includePadding={true}
|
includePadding={true}
|
||||||
heading={"Shop address per channel"}
|
heading={"Shop address per channel"}
|
||||||
mainContent={<ConnectedAddressForm channelSlug={channel as string} />}
|
|
||||||
sideContent={
|
sideContent={
|
||||||
<Box>
|
<Box>
|
||||||
<Text marginBottom={5} as={"p"}>
|
<Text marginBottom={5} as={"p"}>
|
||||||
|
@ -51,7 +50,9 @@ export const ChannelConfigView = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
<ConnectedAddressForm channelSlug={channel as string} />
|
||||||
|
</Layout.AppSection>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Box, Text, Chip, Button } from "@saleor/macaw-ui/next";
|
import { Box, Text, Chip, Button } from "@saleor/macaw-ui/next";
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { SkeletonLayout } from "@saleor/apps-ui";
|
||||||
|
|
||||||
const defaultAddressChip = (
|
const defaultAddressChip = (
|
||||||
<Chip __display={"inline-block"} size={"large"}>
|
<Chip __display={"inline-block"} size={"large"}>
|
||||||
|
@ -17,7 +18,7 @@ export const PerChannelConfigList = () => {
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
|
||||||
if (shopChannelsQuery.isLoading || channelsOverridesQuery.isLoading) {
|
if (shopChannelsQuery.isLoading || channelsOverridesQuery.isLoading) {
|
||||||
return <Text color={"textNeutralSubdued"}>Loading...</Text>;
|
return <SkeletonLayout.Section />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderChannelAddress = (slug: string) => {
|
const renderChannelAddress = (slug: string) => {
|
||||||
|
|
|
@ -2,30 +2,31 @@ import { Box, Text, Button } from "@saleor/macaw-ui/next";
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { ButtonsBox, Layout, SkeletonLayout } from "@saleor/apps-ui";
|
||||||
|
|
||||||
const Wrapper = ({ children }: PropsWithChildren<{}>) => {
|
const Wrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||||
const { appBridge } = useAppBridge();
|
const { appBridge } = useAppBridge();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Layout.AppSectionCard
|
||||||
<Box display={"flex"} justifyContent={"space-between"} marginBottom={5}>
|
footer={
|
||||||
<Text variant={"heading"}>Default address of the shop</Text>
|
<ButtonsBox>
|
||||||
<Button
|
<Button
|
||||||
size={"small"}
|
|
||||||
variant={"tertiary"}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
appBridge?.dispatch(
|
appBridge?.dispatch(
|
||||||
actions.Redirect({
|
actions.Redirect({
|
||||||
to: "/site-settings",
|
to: "/site-settings",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text color={"textNeutralSubdued"}>Edit</Text>
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</ButtonsBox>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Box>{children}</Box>
|
<Box>{children}</Box>
|
||||||
</Box>
|
</Layout.AppSectionCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ export const DefaultShopAddress = () => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Text color={"textNeutralSubdued"}>Loading...</Text>
|
<SkeletonLayout.Section />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -70,6 +71,9 @@ export const DefaultShopAddress = () => {
|
||||||
if (data && data.companyAddress) {
|
if (data && data.companyAddress) {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<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"}>
|
<Text size={"small"} as={"p"}>
|
||||||
{data.companyAddress.companyName}
|
{data.companyAddress.companyName}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -1,34 +1,13 @@
|
||||||
import { httpBatchLink } from "@trpc/client";
|
|
||||||
import { createTRPCNext } from "@trpc/next";
|
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 { appBridgeInstance } from "../../pages/_app";
|
||||||
import { AppRouter } from "./trpc-app-router";
|
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>({
|
export const trpcClient = createTRPCNext<AppRouter>({
|
||||||
config({ ctx }) {
|
config() {
|
||||||
return {
|
return {
|
||||||
links: [
|
links: [createHttpBatchLink(appBridgeInstance)],
|
||||||
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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
// queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
|
// 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 { initTRPC } from "@trpc/server";
|
||||||
import { TrpcContext } from "./trpc-context";
|
|
||||||
import { Permission } from "@saleor/app-sdk/types";
|
import { Permission } from "@saleor/app-sdk/types";
|
||||||
|
import { TrpcContext } from "@saleor/trpc";
|
||||||
|
|
||||||
interface Meta {
|
interface Meta {
|
||||||
requiredClientPermissions?: Permission[];
|
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 React, { ReactElement } from "react";
|
||||||
import { AppProps } from "next/app";
|
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 { trpcClient } from "../modules/trpc/trpc-client";
|
||||||
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
|
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
|
||||||
|
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure instance is a singleton.
|
* Ensure instance is a singleton.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as trpcNext from "@trpc/server/adapters/next";
|
import * as trpcNext from "@trpc/server/adapters/next";
|
||||||
import { createTrpcContext } from "../../../modules/trpc/trpc-context";
|
|
||||||
import { appRouter } from "../../../modules/trpc/trpc-app-router";
|
import { appRouter } from "../../../modules/trpc/trpc-app-router";
|
||||||
import { createLogger } from "@saleor/apps-shared";
|
import { createLogger } from "@saleor/apps-shared";
|
||||||
|
import { createTrpcContext } from "@saleor/trpc";
|
||||||
|
|
||||||
const logger = createLogger({ name: "tRPC error" });
|
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 {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
"@material-ui/lab": "4.0.0-alpha.61",
|
"@material-ui/lab": "4.0.0-alpha.61",
|
||||||
"@saleor/app-sdk": "0.41.1",
|
"@saleor/app-sdk": "0.41.1",
|
||||||
"@saleor/apps-shared": "workspace:*",
|
"@saleor/apps-shared": "workspace:*",
|
||||||
|
"@saleor/apps-ui": "workspace:*",
|
||||||
"@saleor/macaw-ui": "0.8.0-pre.127",
|
"@saleor/macaw-ui": "0.8.0-pre.127",
|
||||||
"@sentry/nextjs": "7.67.0",
|
"@sentry/nextjs": "7.67.0",
|
||||||
"@urql/exchange-auth": "^2.1.4",
|
"@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 { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
|
||||||
import React from "react";
|
import { NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared";
|
||||||
import { AppProps } from "next/app";
|
|
||||||
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
|
||||||
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
|
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
|
||||||
import { NoSSRWrapper } from "@saleor/apps-shared";
|
import "@saleor/macaw-ui/next/style";
|
||||||
import { ThemeSynchronizer } from "../hooks/theme-synchronizer";
|
import { AppProps } from "next/app";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure instance is a singleton.
|
* Ensure instance is a singleton.
|
||||||
|
@ -18,7 +15,9 @@ function SaleorApp({ Component, pageProps }: AppProps) {
|
||||||
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ThemeSynchronizer />
|
<ThemeSynchronizer />
|
||||||
|
<Box padding={10}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
</Box>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</AppBridgeProvider>
|
</AppBridgeProvider>
|
||||||
</NoSSRWrapper>
|
</NoSSRWrapper>
|
||||||
|
|
|
@ -3,13 +3,8 @@ import { Head, Html, Main, NextScript } from "next/document";
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
|
||||||
<Head />
|
<Head />
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600;700;800&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
|
|
|
@ -61,12 +61,20 @@ const handler: NextProtectedApiHandler = async (request, res, ctx) => {
|
||||||
data: await getAppSettings(settings),
|
data: await getAppSettings(settings),
|
||||||
});
|
});
|
||||||
case "POST": {
|
case "POST": {
|
||||||
await settings.set((request.body as PostRequestBody).data);
|
try {
|
||||||
|
await settings.set((JSON.parse(request.body) as PostRequestBody).data);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: await getAppSettings(settings),
|
data: await getAppSettings(settings),
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return res.status(405).end();
|
return res.status(405).end();
|
||||||
|
|
|
@ -8,17 +8,17 @@ import { orderCreatedWebhook } from "./webhooks/order-created";
|
||||||
import { orderFullyPaidWebhook } from "./webhooks/order-fully-paid";
|
import { orderFullyPaidWebhook } from "./webhooks/order-fully-paid";
|
||||||
|
|
||||||
const handler = createManifestHandler({
|
const handler = createManifestHandler({
|
||||||
async manifestFactory(context): Promise<AppManifest> {
|
async manifestFactory({ appBaseUrl }): Promise<AppManifest> {
|
||||||
const { appBaseUrl } = context;
|
const iframeBaseUrl = process.env.APP_IFRAME_BASE_URL ?? appBaseUrl;
|
||||||
|
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
about:
|
about: "Klaviyo integration allows sending Klaviyo notifications on Saleor events.",
|
||||||
"Klaviyo integration allows sending Klaviyo notifications on Saleor events.",
|
appUrl: iframeBaseUrl,
|
||||||
appUrl: appBaseUrl,
|
|
||||||
author: "Saleor Commerce",
|
author: "Saleor Commerce",
|
||||||
brand: {
|
brand: {
|
||||||
logo: {
|
logo: {
|
||||||
default: `${context.appBaseUrl}/logo.png`,
|
default: `${apiBaseURL}/logo.png`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dataPrivacyUrl: "https://saleor.io/legal/privacy/",
|
dataPrivacyUrl: "https://saleor.io/legal/privacy/",
|
||||||
|
@ -27,7 +27,7 @@ const handler = createManifestHandler({
|
||||||
name: "Klaviyo",
|
name: "Klaviyo",
|
||||||
permissions: ["MANAGE_USERS", "MANAGE_ORDERS"],
|
permissions: ["MANAGE_USERS", "MANAGE_ORDERS"],
|
||||||
supportUrl: "https://github.com/saleor/apps/discussions",
|
supportUrl: "https://github.com/saleor/apps/discussions",
|
||||||
tokenTargetUrl: `${appBaseUrl}/api/register`,
|
tokenTargetUrl: `${apiBaseURL}/api/register`,
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
webhooks: [
|
webhooks: [
|
||||||
customerCreatedWebhook.getWebhookManifest(appBaseUrl),
|
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 { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
|
||||||
|
|
||||||
import { ChangeEvent, SyntheticEvent, useEffect, useState } from "react";
|
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 { 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 {
|
interface ConfigurationField {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Section(props: BoxProps) {
|
|
||||||
return <Box padding={4} {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Instructions() {
|
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 (
|
return (
|
||||||
<Section>
|
<Box>
|
||||||
<Text as={"h3"} variant="heading">
|
<Text as={"h3"} variant="heading" marginY={4}>
|
||||||
How to set up
|
How to set up
|
||||||
</Text>
|
</Text>
|
||||||
<Text as="p">App will send events as Klaviyo metrics each time Saleor Event occurs.</Text>
|
<Text as="p" marginBottom={2}>
|
||||||
<Text as="p">
|
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.
|
When first metric is sent, it should be available in Klaviyo to build on top of.
|
||||||
</Text>
|
</Text>
|
||||||
<Text as="p">
|
<Text as="p" marginBottom={4}>
|
||||||
Metric name can be customized, PUBLIC_TOKEN must be provided to enable the app.
|
Metric name can be customized, PUBLIC_TOKEN must be provided to enable the app.
|
||||||
</Text>
|
</Text>
|
||||||
<Text as={"h3"} variant="heading">
|
<Text as={"h3"} variant="heading">
|
||||||
|
@ -49,16 +33,9 @@ function Instructions() {
|
||||||
</Text>
|
</Text>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<TextLink href="https://github.com/saleor/saleor-app-klaviyo" newTab>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
openExternalUrl("https://github.com/saleor/saleor-app-klaviyo");
|
|
||||||
}}
|
|
||||||
href="https://github.com/saleor/saleor-app-klaviyo"
|
|
||||||
>
|
|
||||||
Visit repository & readme
|
Visit repository & readme
|
||||||
</a>
|
</TextLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<Text as={"h3"} variant="heading">
|
<Text as={"h3"} variant="heading">
|
||||||
|
@ -66,47 +43,28 @@ function Instructions() {
|
||||||
</Text>
|
</Text>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<TextLink
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
openExternalUrl(
|
|
||||||
"https://help.klaviyo.com/hc/en-us/articles/115005062267-How-to-Manage-Your-Account-s-API-Keys"
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
href="https://help.klaviyo.com/hc/en-us/articles/115005062267-How-to-Manage-Your-Account-s-API-Keys"
|
href="https://help.klaviyo.com/hc/en-us/articles/115005062267-How-to-Manage-Your-Account-s-API-Keys"
|
||||||
|
newTab
|
||||||
>
|
>
|
||||||
Read about public tokens
|
Read about public tokens
|
||||||
</a>
|
</TextLink>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<TextLink href="https://www.klaviyo.com/account#api-keys-tab" newTab>
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
openExternalUrl("https://www.klaviyo.com/account#api-keys-tab");
|
|
||||||
}}
|
|
||||||
href="https://www.klaviyo.com/account#api-keys-tab"
|
|
||||||
>
|
|
||||||
Get public token here
|
Get public token here
|
||||||
</a>
|
</TextLink>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<TextLink
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
openExternalUrl(
|
|
||||||
"https://help.klaviyo.com/hc/en-us/articles/115005076787-Guide-to-Managing-Your-Metrics"
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
href="https://help.klaviyo.com/hc/en-us/articles/115005076787-Guide-to-Managing-Your-Metrics"
|
href="https://help.klaviyo.com/hc/en-us/articles/115005076787-Guide-to-Managing-Your-Metrics"
|
||||||
|
newTab
|
||||||
>
|
>
|
||||||
Read about metrics
|
Read about metrics
|
||||||
</a>
|
</TextLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Section>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +72,7 @@ function Configuration() {
|
||||||
const { appBridgeState } = useAppBridge();
|
const { appBridgeState } = useAppBridge();
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
const [configuration, setConfiguration] = useState<ConfigurationField[]>();
|
const [configuration, setConfiguration] = useState<ConfigurationField[]>();
|
||||||
|
const authenticatedFetch = useAuthenticatedFetch() as typeof window.fetch;
|
||||||
|
|
||||||
const { data: configurationData, error } = useAppApi({
|
const { data: configurationData, error } = useAppApi({
|
||||||
url: "/api/configuration",
|
url: "/api/configuration",
|
||||||
|
@ -131,13 +90,8 @@ function Configuration() {
|
||||||
const handleSubmit = (event: SyntheticEvent) => {
|
const handleSubmit = (event: SyntheticEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
fetch("/api/configuration", {
|
authenticatedFetch("/api/configuration", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: [
|
|
||||||
["content-type", "application/json"],
|
|
||||||
[SALEOR_API_URL_HEADER, appBridgeState?.saleorApiUrl!],
|
|
||||||
[SALEOR_AUTHORIZATION_BEARER_HEADER, appBridgeState?.token!],
|
|
||||||
],
|
|
||||||
body: JSON.stringify({ data: configuration }),
|
body: JSON.stringify({ data: configuration }),
|
||||||
})
|
})
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
|
@ -149,7 +103,7 @@ function Configuration() {
|
||||||
})
|
})
|
||||||
.catch(async () => {
|
.catch(async () => {
|
||||||
await notifyError(
|
await notifyError(
|
||||||
"Configuration update failed. Ensure fields are filled correctly and you have MANAGE_APPS permission"
|
"Configuration update failed. Ensure fields are filled correctly and you have MANAGE_APPS permission",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -158,7 +112,7 @@ function Configuration() {
|
||||||
const { name, value } = event.target as HTMLInputElement;
|
const { name, value } = event.target as HTMLInputElement;
|
||||||
|
|
||||||
setConfiguration((prev) =>
|
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) {
|
if (configuration === undefined) {
|
||||||
return <p>Loading...</p>;
|
return <SkeletonLayout.Section />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppColumnsLayout>
|
<Box>
|
||||||
<div />
|
<Breadcrumbs marginBottom={10}>
|
||||||
<Section>
|
<Breadcrumbs.Item>Configuration</Breadcrumbs.Item>
|
||||||
<Text variant={"heading"} marginBottom={4} as={"h2"}>
|
</Breadcrumbs>
|
||||||
Klaviyo configuration
|
<Layout.AppSection heading="Set up integration" sideContent={<Instructions />}>
|
||||||
</Text>
|
<Layout.AppSectionCard
|
||||||
<Box as={"form"} display={"grid"} gap={4} gridAutoFlow={"row"} onSubmit={handleSubmit}>
|
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 }) => (
|
{configuration!.map(({ key, value }) => (
|
||||||
<div key={key}>
|
<div key={key}>
|
||||||
<Input label={key} name={key} onChange={onChange} value={value} />
|
<Input label={key} name={key} onChange={onChange} value={value} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div>
|
|
||||||
<Button type="submit" variant="primary">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Section>
|
</Layout.AppSectionCard>
|
||||||
<Instructions />
|
</Layout.AppSection>
|
||||||
</AppColumnsLayout>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,9 @@
|
||||||
"SENTRY_PROJECT",
|
"SENTRY_PROJECT",
|
||||||
"SENTRY_AUTH_TOKEN",
|
"SENTRY_AUTH_TOKEN",
|
||||||
"NEXT_PUBLIC_VERCEL_ENV",
|
"NEXT_PUBLIC_VERCEL_ENV",
|
||||||
"SENTRY_ENVIRONMENT"
|
"SENTRY_ENVIRONMENT",
|
||||||
|
"APP_API_BASE_URL",
|
||||||
|
"APP_IFRAME_BASE_URL"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,8 @@
|
||||||
"vals",
|
"vals",
|
||||||
"urql",
|
"urql",
|
||||||
"Protos",
|
"Protos",
|
||||||
"pino"
|
"pino",
|
||||||
|
"IFRAME"
|
||||||
],
|
],
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.26.2",
|
"@changesets/cli": "^2.26.2",
|
||||||
|
"@sentry/cli": "2.20.6",
|
||||||
"@types/node": "18.15.3",
|
"@types/node": "18.15.3",
|
||||||
"cspell": "^7.2.0",
|
"cspell": "^7.2.0",
|
||||||
"eslint": "8.46.0",
|
"eslint": "8.46.0",
|
||||||
|
@ -27,8 +28,7 @@
|
||||||
"next": "13.4.8",
|
"next": "13.4.8",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.0.3",
|
||||||
"syncpack": "10.9.3",
|
"syncpack": "10.9.3",
|
||||||
"turbo": "1.10.12",
|
"turbo": "1.10.12"
|
||||||
"@sentry/cli": "2.20.6"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
|
|
@ -642,9 +642,15 @@ importers:
|
||||||
'@saleor/apps-shared':
|
'@saleor/apps-shared':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/shared
|
version: link:../../packages/shared
|
||||||
|
'@saleor/apps-ui':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/ui
|
||||||
'@saleor/macaw-ui':
|
'@saleor/macaw-ui':
|
||||||
specifier: 0.8.0-pre.127
|
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)
|
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':
|
'@sentry/nextjs':
|
||||||
specifier: 7.67.0
|
specifier: 7.67.0
|
||||||
version: 7.67.0(next@13.4.8)(react@18.2.0)
|
version: 7.67.0(next@13.4.8)(react@18.2.0)
|
||||||
|
@ -787,6 +793,9 @@ importers:
|
||||||
'@saleor/apps-shared':
|
'@saleor/apps-shared':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/shared
|
version: link:../../packages/shared
|
||||||
|
'@saleor/apps-ui':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/ui
|
||||||
'@saleor/macaw-ui':
|
'@saleor/macaw-ui':
|
||||||
specifier: 0.8.0-pre.127
|
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)
|
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