(null);
-
- const { mutate, error: saveError } = trpcClient.appConfiguration.setAndReplace.useMutation({
- onSuccess() {
- refetchConfig();
- notifySuccess("Success", "Saved app configuration");
- },
- });
-
- useEffect(() => {
- if (channels.isSuccess) {
- setActiveChannelSlug(channels.data![0]?.slug ?? null);
- }
- }, [channels.isSuccess, channels.data]);
-
- const activeChannel = useMemo(() => {
- try {
- return channels.data!.find((c) => c.slug === activeChannelSlug)!;
- } catch (e) {
- return null;
- }
- }, [channels.data, activeChannelSlug]);
-
- if (channels.isLoading || !channels.data) {
- return null;
- }
-
- if (!activeChannel) {
- return Error. No channel available
;
- }
-
- return (
-
- {
- setActiveChannelSlug(slug);
- window.scrollTo({ top: 0, behavior: "smooth" });
- }}
- />
-
- {activeChannel ? (
-
- {
- const newConfig = AppConfigContainer.setChannelAddress(configurationData)(
- activeChannel.slug
- )(data);
-
- mutate(newConfig);
- }}
- initialData={AppConfigContainer.getChannelAddress(configurationData)(
- activeChannel.slug
- )}
- channelName={activeChannel?.name ?? activeChannelSlug}
- />
- {saveError && {saveError.message}}
-
- ) : null}
-
-
- Generate invoices for orders in your shop
-
-
- Shop data on the invoice an be configured per channel. If not set it will use shop data
- from{" "}
- {
- appBridge?.dispatch(
- actions.Redirect({
- to: "/site-settings",
- })
- );
- }}
- >
- the configuration
-
-
-
- Go to{" "}
- {
- appBridge?.dispatch(
- actions.Redirect({
- to: "/orders",
- })
- );
- }}
- >
- Orders
- {" "}
- and open any Order. Then click Invoices -{">"} Generate. Invoice will be
- added to the order page
-
-
-
- );
-};
diff --git a/apps/invoices/src/modules/app-configuration/ui/channels-list.tsx b/apps/invoices/src/modules/app-configuration/ui/channels-list.tsx
deleted file mode 100644
index 35a8e3e..0000000
--- a/apps/invoices/src/modules/app-configuration/ui/channels-list.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import {
- makeStyles,
- OffsettedList,
- OffsettedListBody,
- OffsettedListHeader,
- OffsettedListItem,
- OffsettedListItemCell,
-} from "@saleor/macaw-ui";
-import clsx from "clsx";
-import { Typography } from "@material-ui/core";
-import React from "react";
-import { ChannelFragment } from "../../../../generated/graphql";
-
-const useStyles = makeStyles((theme) => {
- return {
- listItem: {
- cursor: "pointer",
- height: "auto !important",
- },
- listItemActive: {
- background: "#f4f4f4",
- borderRadius: 4,
- overflow: "hidden",
- },
- channelSlug: {
- fontFamily: "monospace",
- opacity: 0.8,
- },
- };
-});
-
-type Props = {
- channels: ChannelFragment[];
- activeChannelSlug: string;
- onChannelClick(channelSlug: string): void;
-};
-
-export const ChannelsList = ({ channels, activeChannelSlug, onChannelClick }: Props) => {
- const styles = useStyles();
-
- return (
-
-
-
- Available channels
-
-
-
- {channels.map((c) => {
- return (
- {
- onChannelClick(c.slug);
- }}
- >
-
- {c.name}
-
- {c.slug}
-
-
-
- );
- })}
-
-
- );
-};
diff --git a/apps/invoices/src/modules/app-configuration/views/app-config.view.tsx b/apps/invoices/src/modules/app-configuration/views/app-config.view.tsx
new file mode 100644
index 0000000..aa54eb2
--- /dev/null
+++ b/apps/invoices/src/modules/app-configuration/views/app-config.view.tsx
@@ -0,0 +1,61 @@
+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";
+
+export const AppConfigView = () => {
+ const { appBridge } = useAppBridge();
+
+ return (
+
+
+
+
+ Configuration
+
+
+ The Invoices App will generate invoices for each order, for which{" "}
+ INVOICE_REQUESTED
event will be triggered
+
+
+ By default it will use{" "}
+ {
+ appBridge?.dispatch(
+ actions.Redirect({
+ to: "/site-settings",
+ })
+ );
+ }}
+ >
+ site settings
+ {" "}
+ address, but each channel can be configured separately
+
+
+
+
+
+
+ }
+ sideContent={
+
+ Configure custom billing address for each channel. If not set, default shop address will
+ be used
+
+ }
+ />
+
+ );
+};
diff --git a/apps/invoices/src/modules/app-configuration/views/channel-config.view.tsx b/apps/invoices/src/modules/app-configuration/views/channel-config.view.tsx
new file mode 100644
index 0000000..549ba6d
--- /dev/null
+++ b/apps/invoices/src/modules/app-configuration/views/channel-config.view.tsx
@@ -0,0 +1,57 @@
+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";
+
+export const ChannelConfigView = () => {
+ const {
+ push,
+ query: { channel },
+ } = useRouter();
+
+ const { mutateAsync } = trpcClient.appConfiguration.removeChannelOverride.useMutation();
+ const { notifySuccess } = useDashboardNotification();
+
+ if (!channel) {
+ return null;
+ }
+
+ return (
+
+
+
+ Configuration
+
+ Edit channel
+
+ {channel}
+
+
+ }
+ sideContent={
+
+
+ Set custom billing address for {channel} channel.
+
+
+
+ }
+ />
+
+ );
+};
diff --git a/apps/invoices/src/modules/channels/ui/per-channel-config-list.tsx b/apps/invoices/src/modules/channels/ui/per-channel-config-list.tsx
new file mode 100644
index 0000000..dd8ca97
--- /dev/null
+++ b/apps/invoices/src/modules/channels/ui/per-channel-config-list.tsx
@@ -0,0 +1,108 @@
+import { Box, Text, Chip, Button } from "@saleor/macaw-ui/next";
+import { trpcClient } from "../../trpc/trpc-client";
+import { useRouter } from "next/router";
+
+const defaultAddressChip = (
+
+
+ Default
+
+
+);
+
+export const PerChannelConfigList = () => {
+ const shopChannelsQuery = trpcClient.channels.fetch.useQuery();
+ const channelsOverridesQuery = trpcClient.appConfiguration.fetchChannelsOverrides.useQuery();
+
+ const { push } = useRouter();
+
+ if (shopChannelsQuery.isLoading || channelsOverridesQuery.isLoading) {
+ return Loading...;
+ }
+
+ const renderChannelAddress = (slug: string) => {
+ const overridesDataRecord = channelsOverridesQuery.data;
+
+ if (!overridesDataRecord) {
+ return null; // todo should throw
+ }
+
+ if (overridesDataRecord[slug]) {
+ const address = overridesDataRecord[slug];
+
+ /**
+ * TODO extract address rendering
+ */
+ return (
+
+
+ {address.companyName}
+
+
+ {address.streetAddress1}
+
+
+ {address.streetAddress2}
+
+
+ {address.postalCode} {address.city}
+
+
+ {address.country}
+
+
+ );
+ } else {
+ return defaultAddressChip;
+ }
+ };
+
+ const renderActionButtonAddress = (slug: string) => {
+ const overridesDataRecord = channelsOverridesQuery.data;
+
+ if (!overridesDataRecord) {
+ return null; // todo should throw
+ }
+
+ return (
+
+ );
+ };
+
+ return (
+
+
+
+ Channel
+
+
+ Address
+
+
+ {shopChannelsQuery.data?.map((channel) => (
+
+ {channel.name}
+ {renderChannelAddress(channel.slug)}
+ {renderActionButtonAddress(channel.slug)}
+
+ ))}
+
+ );
+};
diff --git a/apps/invoices/src/modules/invoice-create-notifier/invoice-create-notifier.ts b/apps/invoices/src/modules/invoices/invoice-create-notifier/invoice-create-notifier.ts
similarity index 93%
rename from apps/invoices/src/modules/invoice-create-notifier/invoice-create-notifier.ts
rename to apps/invoices/src/modules/invoices/invoice-create-notifier/invoice-create-notifier.ts
index 7c2ca69..13223ac 100644
--- a/apps/invoices/src/modules/invoice-create-notifier/invoice-create-notifier.ts
+++ b/apps/invoices/src/modules/invoices/invoice-create-notifier/invoice-create-notifier.ts
@@ -1,5 +1,5 @@
import { Client, gql } from "urql";
-import { InvoiceCreateDocument } from "../../../generated/graphql";
+import { InvoiceCreateDocument } from "../../../../generated/graphql";
import { logger } from "@saleor/apps-shared";
gql`
diff --git a/apps/invoices/src/modules/invoice-file-name/hash-invoice-filename.test.ts b/apps/invoices/src/modules/invoices/invoice-file-name/hash-invoice-filename.test.ts
similarity index 100%
rename from apps/invoices/src/modules/invoice-file-name/hash-invoice-filename.test.ts
rename to apps/invoices/src/modules/invoices/invoice-file-name/hash-invoice-filename.test.ts
diff --git a/apps/invoices/src/modules/invoice-file-name/hash-invoice-filename.ts b/apps/invoices/src/modules/invoices/invoice-file-name/hash-invoice-filename.ts
similarity index 100%
rename from apps/invoices/src/modules/invoice-file-name/hash-invoice-filename.ts
rename to apps/invoices/src/modules/invoices/invoice-file-name/hash-invoice-filename.ts
diff --git a/apps/invoices/src/modules/invoice-file-name/resolve-temp-pdf-file-location.test.ts b/apps/invoices/src/modules/invoices/invoice-file-name/resolve-temp-pdf-file-location.test.ts
similarity index 100%
rename from apps/invoices/src/modules/invoice-file-name/resolve-temp-pdf-file-location.test.ts
rename to apps/invoices/src/modules/invoices/invoice-file-name/resolve-temp-pdf-file-location.test.ts
diff --git a/apps/invoices/src/modules/invoice-file-name/resolve-temp-pdf-file-location.ts b/apps/invoices/src/modules/invoices/invoice-file-name/resolve-temp-pdf-file-location.ts
similarity index 100%
rename from apps/invoices/src/modules/invoice-file-name/resolve-temp-pdf-file-location.ts
rename to apps/invoices/src/modules/invoices/invoice-file-name/resolve-temp-pdf-file-location.ts
diff --git a/apps/invoices/src/modules/invoice-generator/invoice-generator.ts b/apps/invoices/src/modules/invoices/invoice-generator/invoice-generator.ts
similarity index 58%
rename from apps/invoices/src/modules/invoice-generator/invoice-generator.ts
rename to apps/invoices/src/modules/invoices/invoice-generator/invoice-generator.ts
index 8c587c0..377b01d 100644
--- a/apps/invoices/src/modules/invoice-generator/invoice-generator.ts
+++ b/apps/invoices/src/modules/invoices/invoice-generator/invoice-generator.ts
@@ -1,5 +1,5 @@
-import { OrderPayloadFragment } from "../../../generated/graphql";
-import { SellerShopConfig } from "../app-configuration/app-config";
+import { OrderPayloadFragment } from "../../../../generated/graphql";
+import { SellerShopConfig } from "../../app-configuration/schema-v1/app-config-v1";
export interface InvoiceGenerator {
generate(input: {
diff --git a/apps/invoices/src/modules/invoice-generator/microinvoice/microinvoice-invoice-generator.test.ts b/apps/invoices/src/modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator.test.ts
similarity index 89%
rename from apps/invoices/src/modules/invoice-generator/microinvoice/microinvoice-invoice-generator.test.ts
rename to apps/invoices/src/modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator.test.ts
index 424ac21..a70d25d 100644
--- a/apps/invoices/src/modules/invoice-generator/microinvoice/microinvoice-invoice-generator.test.ts
+++ b/apps/invoices/src/modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator.test.ts
@@ -3,8 +3,8 @@ import { MicroinvoiceInvoiceGenerator } from "./microinvoice-invoice-generator";
import { readFile } from "fs/promises";
import { join } from "path";
import rimraf from "rimraf";
-import { mockOrder } from "../../../fixtures/mock-order";
-import { getMockAddress } from "../../../fixtures/mock-address";
+import { mockOrder } from "../../../../fixtures/mock-order";
+import { getMockAddress } from "../../../../fixtures/mock-address";
const dirToSet = process.env.TEMP_PDF_STORAGE_DIR as string;
const filePath = join(dirToSet, "test-invoice.pdf");
diff --git a/apps/invoices/src/modules/invoice-generator/microinvoice/microinvoice-invoice-generator.ts b/apps/invoices/src/modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator.ts
similarity index 96%
rename from apps/invoices/src/modules/invoice-generator/microinvoice/microinvoice-invoice-generator.ts
rename to apps/invoices/src/modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator.ts
index c57fd4f..fe6ff5a 100644
--- a/apps/invoices/src/modules/invoice-generator/microinvoice/microinvoice-invoice-generator.ts
+++ b/apps/invoices/src/modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator.ts
@@ -1,6 +1,6 @@
import { InvoiceGenerator } from "../invoice-generator";
-import { Order, OrderPayloadFragment } from "../../../../generated/graphql";
-import { SellerShopConfig } from "../../app-configuration/app-config";
+import { Order, OrderPayloadFragment } from "../../../../../generated/graphql";
+import { SellerShopConfig } from "../../../app-configuration/schema-v1/app-config-v1";
const Microinvoice = require("microinvoice");
export class MicroinvoiceInvoiceGenerator implements InvoiceGenerator {
diff --git a/apps/invoices/src/modules/invoice-number-generator/invoice-number-generator.test.ts b/apps/invoices/src/modules/invoices/invoice-number-generator/invoice-number-generator.test.ts
similarity index 100%
rename from apps/invoices/src/modules/invoice-number-generator/invoice-number-generator.test.ts
rename to apps/invoices/src/modules/invoices/invoice-number-generator/invoice-number-generator.test.ts
diff --git a/apps/invoices/src/modules/invoice-number-generator/invoice-number-generator.ts b/apps/invoices/src/modules/invoices/invoice-number-generator/invoice-number-generator.ts
similarity index 89%
rename from apps/invoices/src/modules/invoice-number-generator/invoice-number-generator.ts
rename to apps/invoices/src/modules/invoices/invoice-number-generator/invoice-number-generator.ts
index 25de828..043e41d 100644
--- a/apps/invoices/src/modules/invoice-number-generator/invoice-number-generator.ts
+++ b/apps/invoices/src/modules/invoices/invoice-number-generator/invoice-number-generator.ts
@@ -1,4 +1,4 @@
-import { OrderPayloadFragment } from "../../../generated/graphql";
+import { OrderPayloadFragment } from "../../../../generated/graphql";
interface IInvoiceNumberGenerationStrategy {
(order: OrderPayloadFragment): string;
diff --git a/apps/invoices/src/modules/invoice-uploader/invoice-uploader.ts b/apps/invoices/src/modules/invoices/invoice-uploader/invoice-uploader.ts
similarity index 100%
rename from apps/invoices/src/modules/invoice-uploader/invoice-uploader.ts
rename to apps/invoices/src/modules/invoices/invoice-uploader/invoice-uploader.ts
diff --git a/apps/invoices/src/modules/invoice-uploader/saleor-invoice-uploader.ts b/apps/invoices/src/modules/invoices/invoice-uploader/saleor-invoice-uploader.ts
similarity index 95%
rename from apps/invoices/src/modules/invoice-uploader/saleor-invoice-uploader.ts
rename to apps/invoices/src/modules/invoices/invoice-uploader/saleor-invoice-uploader.ts
index 44ed3d4..4454721 100644
--- a/apps/invoices/src/modules/invoice-uploader/saleor-invoice-uploader.ts
+++ b/apps/invoices/src/modules/invoices/invoice-uploader/saleor-invoice-uploader.ts
@@ -1,7 +1,7 @@
import { InvoiceUploader } from "./invoice-uploader";
import { Client, gql } from "urql";
import { readFile } from "fs/promises";
-import { FileUploadMutation } from "../../../generated/graphql";
+import { FileUploadMutation } from "../../../../generated/graphql";
/**
* Polyfill file because Node doesn't have it yet
* https://github.com/nodejs/node/commit/916af4ef2d63fe936a369bcf87ee4f69ec7c67ce
diff --git a/apps/invoices/src/modules/shop-info/shop-info-query-to-address-shape.ts b/apps/invoices/src/modules/shop-info/shop-info-query-to-address-shape.ts
new file mode 100644
index 0000000..ca37457
--- /dev/null
+++ b/apps/invoices/src/modules/shop-info/shop-info-query-to-address-shape.ts
@@ -0,0 +1,32 @@
+import { ShopInfoFragment, ShopInfoQuery } from "../../../generated/graphql";
+import { AddressV2Shape } from "../app-configuration/schema-v2/app-config-schema.v2";
+
+export const shopInfoQueryToAddressShape = (
+ shopFragment: ShopInfoFragment | null
+): AddressV2Shape | null => {
+ if (!shopFragment?.companyAddress) {
+ return null;
+ }
+
+ const {
+ streetAddress2,
+ streetAddress1,
+ country,
+ countryArea,
+ postalCode,
+ cityArea,
+ companyName,
+ city,
+ } = shopFragment.companyAddress;
+
+ return {
+ city,
+ cityArea,
+ companyName,
+ country: country.country,
+ countryArea,
+ postalCode,
+ streetAddress1,
+ streetAddress2,
+ };
+};
diff --git a/apps/invoices/src/modules/shop-info/shop-info.router.ts b/apps/invoices/src/modules/shop-info/shop-info.router.ts
new file mode 100644
index 0000000..a930644
--- /dev/null
+++ b/apps/invoices/src/modules/shop-info/shop-info.router.ts
@@ -0,0 +1,14 @@
+import { router } from "../trpc/trpc-server";
+import { protectedClientProcedure } from "../trpc/protected-client-procedure";
+import { createLogger } from "@saleor/apps-shared";
+import { ShopInfoFetcher } from "./shop-info-fetcher";
+
+export const shopInfoRouter = router({
+ fetchShopAddress: protectedClientProcedure.query(async ({ ctx, input }) => {
+ const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
+
+ logger.debug("shopInfoRouter.fetchShopAddress called");
+
+ return new ShopInfoFetcher(ctx.apiClient).fetchShopInfo();
+ }),
+});
diff --git a/apps/invoices/src/modules/shop-info/ui/default-shop-address.tsx b/apps/invoices/src/modules/shop-info/ui/default-shop-address.tsx
new file mode 100644
index 0000000..3d69117
--- /dev/null
+++ b/apps/invoices/src/modules/shop-info/ui/default-shop-address.tsx
@@ -0,0 +1,93 @@
+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";
+
+const Wrapper = ({ children }: PropsWithChildren<{}>) => {
+ const { appBridge } = useAppBridge();
+
+ return (
+
+
+ Default address of the shop
+
+
+ {children}
+
+ );
+};
+
+export const DefaultShopAddress = () => {
+ const { data, isLoading, error, refetch } = trpcClient.shopInfo.fetchShopAddress.useQuery();
+
+ if (error) {
+ return (
+
+
+ Error while fetching shop address
+
+
+
+ );
+ }
+
+ if (isLoading) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ if (data && data.companyAddress === null) {
+ return (
+
+
+ No default address set
+
+
+ Set default shop address or channel overrides
+
+
+ If no address is set, invoices will not be generated
+
+
+ );
+ }
+
+ if (data && data.companyAddress) {
+ return (
+
+
+ {data.companyAddress.companyName}
+
+
+ {data.companyAddress.streetAddress1}
+
+
+ {data.companyAddress.streetAddress2}
+
+
+ {data.companyAddress.postalCode} {data.companyAddress.city}
+
+
+ {data.companyAddress.country.country}
+
+
+ );
+ }
+
+ return null;
+};
diff --git a/apps/invoices/src/modules/trpc/trpc-app-router.ts b/apps/invoices/src/modules/trpc/trpc-app-router.ts
index 354df74..cdd706d 100644
--- a/apps/invoices/src/modules/trpc/trpc-app-router.ts
+++ b/apps/invoices/src/modules/trpc/trpc-app-router.ts
@@ -1,10 +1,12 @@
import { channelsRouter } from "../channels/channels.router";
import { router } from "./trpc-server";
-import { appConfigurationRouter } from "../app-configuration/app-configuration.router";
+import { shopInfoRouter } from "../shop-info/shop-info.router";
+import { appConfigurationRouter } from "../app-configuration/app-configuration-router";
export const appRouter = router({
channels: channelsRouter,
appConfiguration: appConfigurationRouter,
+ shopInfo: shopInfoRouter,
});
export type AppRouter = typeof appRouter;
diff --git a/apps/invoices/src/modules/ui/AppSection.tsx b/apps/invoices/src/modules/ui/AppSection.tsx
new file mode 100644
index 0000000..79ce08b
--- /dev/null
+++ b/apps/invoices/src/modules/ui/AppSection.tsx
@@ -0,0 +1,36 @@
+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 (
+
+
+
+ {heading}
+
+ {sideContent}
+
+
+ {mainContent}
+
+
+ );
+};
diff --git a/apps/invoices/src/modules/ui/app-columns-layout.tsx b/apps/invoices/src/modules/ui/app-columns-layout.tsx
deleted file mode 100644
index 94ee503..0000000
--- a/apps/invoices/src/modules/ui/app-columns-layout.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { makeStyles } from "@saleor/macaw-ui";
-import { PropsWithChildren } from "react";
-
-const useStyles = makeStyles({
- root: {
- display: "grid",
- gridTemplateColumns: "280px auto 280px",
- alignItems: "start",
- gap: 32,
- maxWidth: 1180,
- margin: "0 auto",
- },
-});
-
-type Props = PropsWithChildren<{}>;
-
-export const AppColumnsLayout = ({ children }: Props) => {
- const styles = useStyles();
-
- return {children}
;
-};
diff --git a/apps/invoices/src/pages/_app.tsx b/apps/invoices/src/pages/_app.tsx
index dfb65fa..a272d00 100644
--- a/apps/invoices/src/pages/_app.tsx
+++ b/apps/invoices/src/pages/_app.tsx
@@ -1,41 +1,58 @@
-import "@saleor/apps-shared/src/globals.css";
+import "@saleor/macaw-ui/next/style";
import "../styles/globals.css";
+
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
-import React, { useEffect } from "react";
+import React, { ReactElement } from "react";
import { AppProps } from "next/app";
-import { ThemeSynchronizer } from "../lib/theme-synchronizer";
-import { NoSSRWrapper } from "../lib/no-ssr-wrapper";
+
+import { NoSSRWrapper } from "@saleor/apps-shared";
import { trpcClient } from "../modules/trpc/trpc-client";
-import { MacawThemeProvider } from "@saleor/apps-shared";
+import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
+
+import { NextPage } from "next";
+import { ThemeSynchronizer } from "../lib/theme-synchronizer";
/**
* Ensure instance is a singleton.
* TODO: This is React 18 issue, consider hiding this workaround inside app-sdk
*/
-export const appBridgeInstance =
- typeof window !== "undefined" ? new AppBridge({ autoNotifyReady: false }) : undefined;
+export let appBridgeInstance: AppBridge | undefined;
-function NextApp({ Component, pageProps }: AppProps) {
- /**
- * Configure JSS (used by MacawUI) for SSR. If Macaw is not used, can be removed.
- */
- useEffect(() => {
- const jssStyles = document.querySelector("#jss-server-side");
+if (typeof window !== "undefined" && !appBridgeInstance) {
+ appBridgeInstance = new AppBridge();
+}
- if (jssStyles) {
- jssStyles?.parentElement?.removeChild(jssStyles);
- }
- }, []);
+/**
+ * Implementation of layout pattern
+ * https://nextjs.org/docs/basic-features/layouts#per-page-layouts
+ *
+ * In this app, there are pages inside the iframe, which will not use AppBridge etc, so they need
+ * to provider custom tree of wrappers
+ */
+export type NextPageWithLayoutOverwrite = NextPage
& {
+ overwriteLayout?: (page: ReactElement) => ReactElement;
+};
+
+type AppPropsWithLayout = AppProps & {
+ Component: NextPageWithLayoutOverwrite;
+};
+
+function NextApp({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLayout) {
+ if (Component.overwriteLayout) {
+ return Component.overwriteLayout();
+ }
return (
-
+
-
-
+
+
+
+
);
diff --git a/apps/invoices/src/pages/api/webhooks/invoice-requested.ts b/apps/invoices/src/pages/api/webhooks/invoice-requested.ts
index 752f391..5a06102 100644
--- a/apps/invoices/src/pages/api/webhooks/invoice-requested.ts
+++ b/apps/invoices/src/pages/api/webhooks/invoice-requested.ts
@@ -6,18 +6,26 @@ import {
OrderPayloadFragment,
} from "../../../../generated/graphql";
import { createClient } from "../../../lib/graphql";
-import { SaleorInvoiceUploader } from "../../../modules/invoice-uploader/saleor-invoice-uploader";
-import { InvoiceCreateNotifier } from "../../../modules/invoice-create-notifier/invoice-create-notifier";
+import { SaleorInvoiceUploader } from "../../../modules/invoices/invoice-uploader/saleor-invoice-uploader";
+import { InvoiceCreateNotifier } from "../../../modules/invoices/invoice-create-notifier/invoice-create-notifier";
import {
InvoiceNumberGenerationStrategy,
InvoiceNumberGenerator,
-} from "../../../modules/invoice-number-generator/invoice-number-generator";
-import { MicroinvoiceInvoiceGenerator } from "../../../modules/invoice-generator/microinvoice/microinvoice-invoice-generator";
-import { hashInvoiceFilename } from "../../../modules/invoice-file-name/hash-invoice-filename";
-import { resolveTempPdfFileLocation } from "../../../modules/invoice-file-name/resolve-temp-pdf-file-location";
+} from "../../../modules/invoices/invoice-number-generator/invoice-number-generator";
+import { MicroinvoiceInvoiceGenerator } from "../../../modules/invoices/invoice-generator/microinvoice/microinvoice-invoice-generator";
+import { hashInvoiceFilename } from "../../../modules/invoices/invoice-file-name/hash-invoice-filename";
+import { resolveTempPdfFileLocation } from "../../../modules/invoices/invoice-file-name/resolve-temp-pdf-file-location";
import { createLogger } from "@saleor/apps-shared";
-import { GetAppConfigurationService } from "../../../modules/app-configuration/get-app-configuration.service";
import { SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
+import { GetAppConfigurationV2Service } from "../../../modules/app-configuration/schema-v2/get-app-configuration.v2.service";
+import { ShopInfoFetcher } from "../../../modules/shop-info/shop-info-fetcher";
+import { z } from "zod";
+import {
+ AddressV2Schema,
+ AddressV2Shape,
+} from "../../../modules/app-configuration/schema-v2/app-config-schema.v2";
+import { ConfigV1ToV2MigrationService } from "../../../modules/app-configuration/schema-v2/config-v1-to-v2-migration.service";
+import { shopInfoQueryToAddressShape } from "../../../modules/shop-info/shop-info-query-to-address-shape";
const OrderPayload = gql`
fragment Address on Address {
@@ -136,6 +144,13 @@ export const invoiceRequestedWebhook = new SaleorAsyncWebhook = async (
req,
res,
@@ -160,14 +175,6 @@ export const handler: NextWebhookApiHandler = a
logger.debug({ invoiceName }, "Generated invoice name");
- if (!authData) {
- logger.error("Auth data not found");
-
- return res.status(403).json({
- error: `Could not find auth data. Check if app is installed.`,
- });
- }
-
try {
const client = createClient(authData.saleorApiUrl, async () =>
Promise.resolve({ token: authData.token })
@@ -182,17 +189,39 @@ export const handler: NextWebhookApiHandler = a
logger.debug({ tempPdfLocation }, "Resolved PDF location for temporary files");
- const appConfig = await new GetAppConfigurationService({
+ let appConfigV2 = await new GetAppConfigurationV2Service({
saleorApiUrl: authData.saleorApiUrl,
apiClient: client,
}).getConfiguration();
+ /**
+ * MIGRATION CODE START - remove when metadata migrated
+ */
+ if (!appConfigV2) {
+ const migrationService = new ConfigV1ToV2MigrationService(client, authData.saleorApiUrl);
+
+ appConfigV2 = await migrationService.migrate();
+ }
+ /**
+ * MIGRATION CODE END
+ */
+
+ const address: AddressV2Shape | null =
+ appConfigV2.getChannelsOverrides()[order.channel.slug] ??
+ (await new ShopInfoFetcher(client).fetchShopInfo().then(shopInfoQueryToAddressShape));
+
+ if (!address) {
+ // todo disable webhook
+
+ return res.status(200).end("App not configured");
+ }
+
await new MicroinvoiceInvoiceGenerator()
.generate({
order,
invoiceNumber: invoiceName,
filename: tempPdfLocation,
- companyAddressData: appConfig.shopConfigPerChannel[order.channel.slug]?.address,
+ companyAddressData: address,
})
.catch((err) => {
logger.error(err, "Error generating invoice");
diff --git a/apps/invoices/src/pages/configuration.tsx b/apps/invoices/src/pages/configuration.tsx
index 0937fa5..27db92d 100644
--- a/apps/invoices/src/pages/configuration.tsx
+++ b/apps/invoices/src/pages/configuration.tsx
@@ -1,31 +1,9 @@
import { NextPage } from "next";
-import React, { useEffect } from "react";
-import { ChannelsConfiguration } from "../modules/app-configuration/ui/channels-configuration";
-import { trpcClient } from "../modules/trpc/trpc-client";
-import { useRouter } from "next/router";
-import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
+import React from "react";
+import { AppConfigView } from "../modules/app-configuration/views/app-config.view";
const ConfigurationPage: NextPage = () => {
- const channels = trpcClient.channels.fetch.useQuery();
- const router = useRouter();
-
- const { appBridge, appBridgeState } = useAppBridge();
-
- useEffect(() => {
- if (channels.isFetched && appBridge && !appBridgeState?.ready) {
- if (appBridge && channels.isFetched) {
- appBridge.dispatch(actions.NotifyReady());
- }
- }
- }, [channels.isFetched, appBridge, appBridgeState?.ready]);
-
- useEffect(() => {
- if (channels.isSuccess && channels.data.length === 0) {
- router.push("/not-ready");
- }
- }, [channels.data, channels.isSuccess]);
-
- return ;
+ return ;
};
export default ConfigurationPage;
diff --git a/apps/invoices/src/pages/configuration/[channel].tsx b/apps/invoices/src/pages/configuration/[channel].tsx
new file mode 100644
index 0000000..d011565
--- /dev/null
+++ b/apps/invoices/src/pages/configuration/[channel].tsx
@@ -0,0 +1,8 @@
+import { NextPage } from "next";
+import { ChannelConfigView } from "../../modules/app-configuration/views/channel-config.view";
+
+const ChannelConfigPage: NextPage = () => {
+ return ;
+};
+
+export default ChannelConfigPage;
diff --git a/apps/invoices/src/pages/index.tsx b/apps/invoices/src/pages/index.tsx
index 536b7b3..2e5c110 100644
--- a/apps/invoices/src/pages/index.tsx
+++ b/apps/invoices/src/pages/index.tsx
@@ -4,6 +4,7 @@ import { useEffect } from "react";
import { useIsMounted } from "usehooks-ts";
import { useRouter } from "next/router";
import { isInIframe } from "@saleor/apps-shared";
+import { Box, Text } from "@saleor/macaw-ui/next";
const IndexPage: NextPage = () => {
const { appBridgeState } = useAppBridge();
@@ -21,11 +22,16 @@ const IndexPage: NextPage = () => {
}
return (
-
-
Saleor Invoices
-
This is Saleor App that allows invoices generation
-
Install app in your Saleor instance and open in with Dashboard
-
+
+
+ Saleor Invoices
+
+ This is Saleor App that allows invoices generation
+
+ Install app in your Saleor instance and open in with Dashboard{" "}
+ or check it on Github
+
+
);
};
diff --git a/apps/invoices/src/pages/not-ready.tsx b/apps/invoices/src/pages/not-ready.tsx
deleted file mode 100644
index e9c73bc..0000000
--- a/apps/invoices/src/pages/not-ready.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { AlertBase, Button } from "@saleor/macaw-ui";
-import React from "react";
-import { Typography } from "@material-ui/core";
-import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
-
-const NotReadyPage = () => {
- const { appBridge } = useAppBridge();
-
- return (
-
-
Saleor Invoices App
-
-
- App can not be used
-
-
- To configure Invoices App you need to create at least 1 channel
-
-
-
-
- );
-};
-
-export default NotReadyPage;
diff --git a/apps/invoices/src/styles/globals.css b/apps/invoices/src/styles/globals.css
index 220e555..2366a15 100644
--- a/apps/invoices/src/styles/globals.css
+++ b/apps/invoices/src/styles/globals.css
@@ -1,10 +1,9 @@
body {
font-family: Inter, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
- color: #111;
- padding-top: 32px;
}
a {
cursor: pointer;
+ text-decoration: none;
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f820209..ea685da 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -626,15 +626,9 @@ importers:
apps/invoices:
dependencies:
- '@material-ui/core':
- specifier: ^4.12.4
- version: 4.12.4(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
- '@material-ui/icons':
- specifier: ^4.11.3
- version: 4.11.3(@material-ui/core@4.12.4)(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
- '@material-ui/lab':
- specifier: 4.0.0-alpha.61
- version: 4.0.0-alpha.61(@material-ui/core@4.12.4)(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
+ '@hookform/resolvers':
+ specifier: ^3.1.0
+ version: 3.1.0(react-hook-form@7.43.1)
'@saleor/app-sdk':
specifier: 0.37.3
version: 0.37.3(next@13.3.0)(react-dom@18.2.0)(react@18.2.0)
@@ -642,8 +636,8 @@ importers:
specifier: workspace:*
version: link:../../packages/shared
'@saleor/macaw-ui':
- specifier: ^0.7.2
- version: 0.7.2(@material-ui/core@4.12.4)(@material-ui/icons@4.11.3)(@material-ui/lab@4.0.0-alpha.61)(@types/react@18.0.27)(react-dom@18.2.0)(react-helmet@6.1.0)(react@18.2.0)
+ specifier: ^0.8.0-pre.80
+ version: 0.8.0-pre.80(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
'@sentry/nextjs':
specifier: ^7.36.0
version: 7.36.0(next@13.3.0)(react@18.2.0)
@@ -4091,6 +4085,14 @@ packages:
react-hook-form: 7.43.1(react@18.2.0)
dev: false
+ /@hookform/resolvers@3.1.0(react-hook-form@7.43.1):
+ resolution: {integrity: sha512-z0A8K+Nxq+f83Whm/ajlwE6VtQlp/yPHZnXw7XWVPIGm1Vx0QV8KThU3BpbBRfAZ7/dYqCKKBNnQh85BkmBKkA==}
+ peerDependencies:
+ react-hook-form: ^7.0.0
+ dependencies:
+ react-hook-form: 7.43.1(react@18.2.0)
+ dev: false
+
/@hookform/resolvers@3.1.0(react-hook-form@7.43.9):
resolution: {integrity: sha512-z0A8K+Nxq+f83Whm/ajlwE6VtQlp/yPHZnXw7XWVPIGm1Vx0QV8KThU3BpbBRfAZ7/dYqCKKBNnQh85BkmBKkA==}
peerDependencies:
@@ -5004,6 +5006,34 @@ packages:
- '@types/react'
dev: false
+ /@radix-ui/react-popover@1.0.5(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-GRHZ8yD12MrN2NLobHPE8Rb5uHTxd9x372DE9PPNnBjpczAQHcZ5ne0KXG4xpf+RDdXSzdLv9ym6mYJCDTaUZg==}
+ peerDependencies:
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ dependencies:
+ '@babel/runtime': 7.20.13
+ '@radix-ui/primitive': 1.0.0
+ '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
+ '@radix-ui/react-context': 1.0.0(react@18.2.0)
+ '@radix-ui/react-dismissable-layer': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-focus-guards': 1.0.0(react@18.2.0)
+ '@radix-ui/react-focus-scope': 1.0.2(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-id': 1.0.0(react@18.2.0)
+ '@radix-ui/react-popper': 1.1.1(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-portal': 1.0.2(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-presence': 1.0.0(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-slot': 1.0.1(react@18.2.0)
+ '@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
+ aria-hidden: 1.2.2(@types/react@18.0.27)(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ react-remove-scroll: 2.5.5(@types/react@18.0.27)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ dev: false
+
/@radix-ui/react-popover@1.0.5(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GRHZ8yD12MrN2NLobHPE8Rb5uHTxd9x372DE9PPNnBjpczAQHcZ5ne0KXG4xpf+RDdXSzdLv9ym6mYJCDTaUZg==}
peerDependencies:
@@ -5248,6 +5278,31 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
+ /@radix-ui/react-tooltip@1.0.5(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w==}
+ peerDependencies:
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ dependencies:
+ '@babel/runtime': 7.20.13
+ '@radix-ui/primitive': 1.0.0
+ '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
+ '@radix-ui/react-context': 1.0.0(react@18.2.0)
+ '@radix-ui/react-dismissable-layer': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-id': 1.0.0(react@18.2.0)
+ '@radix-ui/react-popper': 1.1.1(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-portal': 1.0.2(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-presence': 1.0.0(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-slot': 1.0.1(react@18.2.0)
+ '@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
+ '@radix-ui/react-visually-hidden': 1.0.2(react-dom@18.2.0)(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ dev: false
+
/@radix-ui/react-tooltip@1.0.5(@types/react@18.0.38)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w==}
peerDependencies:
@@ -5537,6 +5592,38 @@ packages:
- '@types/react'
dev: false
+ /@saleor/macaw-ui@0.8.0-pre.80(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-ecP65upR3P8NtE/ZaznWP4Lk/BVTiHr33K/g0YH3535hdfevaE8WibWeQ7wS6g41pia1OfDYGiW0rTxXgmsS4g==}
+ engines: {node: '>=16 <19', pnpm: '>=8'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ '@dessert-box/react': 0.4.0(react@18.2.0)
+ '@floating-ui/react-dom-interactions': 0.5.0(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-accordion': 1.1.1(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-checkbox': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-dialog': 1.0.3(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-dropdown-menu': 2.0.4(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-popover': 1.0.5(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-portal': 1.0.2(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-radio-group': 1.1.2(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-select': 1.2.1(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-toggle': 1.0.2(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-tooltip': 1.0.5(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0)
+ '@vanilla-extract/css-utils': 0.1.3
+ clsx: 1.2.1
+ downshift: 6.1.12(react@18.2.0)
+ downshift7: /downshift@7.6.0(react@18.2.0)
+ lodash: 4.17.21
+ lodash-es: 4.17.21
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ react-inlinesvg: 3.0.1(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ dev: false
+
/@selderee/plugin-htmlparser2@0.10.0:
resolution: {integrity: sha512-gW69MEamZ4wk1OsOq1nG1jcyhXIQcnrsX5JwixVw/9xaiav8TCyjESAruu1Rz9yyInhgBXxkNwMeygKnN2uxNA==}
dependencies: