Add gift card permission to the manifest and block event (#733)
* Handle missing permissions and old Saleor Version * Throw an error when fetching app permissions fails
This commit is contained in:
parent
d4089ab519
commit
be4e7d2922
9 changed files with 256 additions and 13 deletions
7
.changeset/popular-clouds-play.md
Normal file
7
.changeset/popular-clouds-play.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
"saleor-app-emails-and-messages": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Manage gift card permission is now required to enable gift cards event.
|
||||||
|
|
||||||
|
Added message to interface for users with missing permission or Saleor version without the support for this event.
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { useAppBridge, actions } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { SaleorVersionCompatibilityValidator } from "@saleor/apps-shared";
|
||||||
|
import { PermissionEnum } from "../../generated/graphql";
|
||||||
|
import { Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { TextLink } from "@saleor/apps-ui";
|
||||||
|
|
||||||
|
interface ManagePermissionsTextLinkProps {
|
||||||
|
missingPermission: PermissionEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns TextLink component with link to manage permissions if used in compatible dashboard version.
|
||||||
|
* Otherwise returns text instructions to reinstall the app.
|
||||||
|
*/
|
||||||
|
export const ManagePermissionsTextLink = ({
|
||||||
|
missingPermission,
|
||||||
|
}: ManagePermissionsTextLinkProps) => {
|
||||||
|
const { appBridgeState, appBridge } = useAppBridge();
|
||||||
|
const dashboardVersion = appBridgeState?.dashboardVersion;
|
||||||
|
|
||||||
|
// Editing app permissions has been introduced in Saleor Dashboard 3.15
|
||||||
|
const isPermissionManagementAvailable = dashboardVersion
|
||||||
|
? new SaleorVersionCompatibilityValidator(">=3.15").isValid(dashboardVersion)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
const appId = appBridgeState?.id;
|
||||||
|
|
||||||
|
if (!isPermissionManagementAvailable || !appId) {
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
To use this feature, the {missingPermission} permission is required. Please reinstall the
|
||||||
|
app.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
To use this feature, the {missingPermission} permission is required. Please go to{" "}
|
||||||
|
{/* TODO: Update the shared package to handle dashboard links */}
|
||||||
|
<TextLink
|
||||||
|
onClick={(e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Redirect({
|
||||||
|
to: `/apps/${appId}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
href="#"
|
||||||
|
>
|
||||||
|
Manage App section
|
||||||
|
</TextLink>
|
||||||
|
, and grant the permission
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
34
apps/emails-and-messages/src/lib/fetch-app-permissions.ts
Normal file
34
apps/emails-and-messages/src/lib/fetch-app-permissions.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { Client, gql } from "urql";
|
||||||
|
import {
|
||||||
|
FetchAppPermissionsDocument,
|
||||||
|
FetchAppPermissionsQuery,
|
||||||
|
PermissionEnum,
|
||||||
|
} from "../../generated/graphql";
|
||||||
|
import { createLogger } from "@saleor/apps-shared";
|
||||||
|
|
||||||
|
gql`
|
||||||
|
query FetchAppPermissions {
|
||||||
|
app {
|
||||||
|
permissions {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
name: "fetchAppPermissions",
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function fetchAppPermissions(client: Client): Promise<PermissionEnum[]> {
|
||||||
|
const { error, data } = await client
|
||||||
|
.query<FetchAppPermissionsQuery>(FetchAppPermissionsDocument, {})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.error(error, "Error fetching app permissions");
|
||||||
|
throw new Error("Could not fetch the app permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data?.app?.permissions?.map((p) => p.code) || [];
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { vi, expect, describe, it } from "vitest";
|
||||||
|
import { getEventFormStatus } from "./get-event-form-status";
|
||||||
|
import { PermissionEnum } from "../../generated/graphql";
|
||||||
|
|
||||||
|
describe("getEventFormStatus", function () {
|
||||||
|
it("No message or disable flag, when event other than GIFT_CARD_SENT is passed", () => {
|
||||||
|
expect(
|
||||||
|
getEventFormStatus({
|
||||||
|
eventType: "ORDER_CREATED",
|
||||||
|
appPermissions: [PermissionEnum.ManageGiftCard],
|
||||||
|
featureFlags: {
|
||||||
|
giftCardSentEvent: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
tooltipMessage: undefined,
|
||||||
|
isDisabled: false,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
getEventFormStatus({
|
||||||
|
eventType: "ORDER_CREATED",
|
||||||
|
appPermissions: [],
|
||||||
|
featureFlags: {
|
||||||
|
giftCardSentEvent: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
isDisabled: false,
|
||||||
|
missingPermission: undefined,
|
||||||
|
requiredSaleorVersion: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Return disable flag and lack of the permission message, when GIFT_CARD_SENT is passed and app has no manage gift card permission", () => {
|
||||||
|
expect(
|
||||||
|
getEventFormStatus({
|
||||||
|
eventType: "GIFT_CARD_SENT",
|
||||||
|
appPermissions: [],
|
||||||
|
featureFlags: {
|
||||||
|
giftCardSentEvent: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
isDisabled: true,
|
||||||
|
missingPermission: PermissionEnum.ManageGiftCard,
|
||||||
|
requiredSaleorVersion: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Return disable flag and unsupported Saleor version message, when GIFT_CARD_SENT is passed with missing feature flag", () => {
|
||||||
|
expect(
|
||||||
|
getEventFormStatus({
|
||||||
|
eventType: "GIFT_CARD_SENT",
|
||||||
|
appPermissions: [PermissionEnum.ManageGiftCard],
|
||||||
|
featureFlags: {
|
||||||
|
giftCardSentEvent: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
isDisabled: true,
|
||||||
|
missingPermission: undefined,
|
||||||
|
requiredSaleorVersion: ">=3.13",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
40
apps/emails-and-messages/src/lib/get-event-form-status.ts
Normal file
40
apps/emails-and-messages/src/lib/get-event-form-status.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { PermissionEnum } from "../../generated/graphql";
|
||||||
|
import { MessageEventTypes } from "../modules/event-handlers/message-event-types";
|
||||||
|
import { FeatureFlagsState } from "../modules/feature-flag-service/get-feature-flags";
|
||||||
|
|
||||||
|
interface getEventFormStatusArgs {
|
||||||
|
eventType: MessageEventTypes;
|
||||||
|
featureFlags?: FeatureFlagsState;
|
||||||
|
appPermissions?: PermissionEnum[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEventFormStatus = ({
|
||||||
|
eventType,
|
||||||
|
featureFlags,
|
||||||
|
appPermissions,
|
||||||
|
}: getEventFormStatusArgs): {
|
||||||
|
missingPermission: PermissionEnum | undefined;
|
||||||
|
isDisabled: boolean;
|
||||||
|
requiredSaleorVersion: string | undefined;
|
||||||
|
} => {
|
||||||
|
// Since GIFT_CARD_SENT is the only event with such validation, we can exit early
|
||||||
|
if (eventType !== "GIFT_CARD_SENT") {
|
||||||
|
return {
|
||||||
|
isDisabled: false,
|
||||||
|
missingPermission: undefined,
|
||||||
|
requiredSaleorVersion: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUnsupported = !featureFlags?.giftCardSentEvent;
|
||||||
|
|
||||||
|
const hasGiftCardPermission = (appPermissions || []).includes(PermissionEnum.ManageGiftCard);
|
||||||
|
|
||||||
|
const isDisabled = isUnsupported || !hasGiftCardPermission;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDisabled,
|
||||||
|
missingPermission: hasGiftCardPermission ? undefined : PermissionEnum.ManageGiftCard,
|
||||||
|
requiredSaleorVersion: isUnsupported ? ">=3.13" : undefined,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,6 +1,7 @@
|
||||||
import { createLogger } from "@saleor/apps-shared";
|
import { createLogger } from "@saleor/apps-shared";
|
||||||
import { router } from "../trpc/trpc-server";
|
import { router } from "../trpc/trpc-server";
|
||||||
import { protectedWithConfigurationServices } from "../trpc/protected-client-procedure-with-services";
|
import { protectedWithConfigurationServices } from "../trpc/protected-client-procedure-with-services";
|
||||||
|
import { fetchAppPermissions } from "../../lib/fetch-app-permissions";
|
||||||
|
|
||||||
export const appConfigurationRouter = router({
|
export const appConfigurationRouter = router({
|
||||||
featureFlags: protectedWithConfigurationServices.query(async ({ ctx }) => {
|
featureFlags: protectedWithConfigurationServices.query(async ({ ctx }) => {
|
||||||
|
@ -9,4 +10,12 @@ export const appConfigurationRouter = router({
|
||||||
logger.debug("appConfigurationRouter.featureFlags called");
|
logger.debug("appConfigurationRouter.featureFlags called");
|
||||||
return await ctx.featureFlagService.getFeatureFlags();
|
return await ctx.featureFlagService.getFeatureFlags();
|
||||||
}),
|
}),
|
||||||
|
appPermissions: protectedWithConfigurationServices.query(async ({ ctx }) => {
|
||||||
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
||||||
|
logger.debug("appConfigurationRouter.permissions called");
|
||||||
|
const appPermissions = await fetchAppPermissions(ctx.apiClient);
|
||||||
|
|
||||||
|
return appPermissions;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,6 +19,8 @@ import { Select } from "@saleor/react-hook-form-macaw";
|
||||||
import { TextLink } from "@saleor/apps-ui";
|
import { TextLink } from "@saleor/apps-ui";
|
||||||
import { messageEventTypesLabels } from "../../event-handlers/message-event-types";
|
import { messageEventTypesLabels } from "../../event-handlers/message-event-types";
|
||||||
import { Table } from "../../../components/table";
|
import { Table } from "../../../components/table";
|
||||||
|
import { getEventFormStatus } from "../../../lib/get-event-form-status";
|
||||||
|
import { ManagePermissionsTextLink } from "../../../components/manage-permissions-text-link";
|
||||||
|
|
||||||
interface SendgridEventsSectionProps {
|
interface SendgridEventsSectionProps {
|
||||||
configuration: SendgridConfiguration;
|
configuration: SendgridConfiguration;
|
||||||
|
@ -28,6 +30,7 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
|
||||||
const { data: featureFlags } = trpcClient.app.featureFlags.useQuery();
|
const { data: featureFlags } = trpcClient.app.featureFlags.useQuery();
|
||||||
|
const { data: appPermissions } = trpcClient.app.appPermissions.useQuery();
|
||||||
|
|
||||||
// Sort events by displayed label
|
// Sort events by displayed label
|
||||||
const eventsSorted = configuration.events.sort((a, b) =>
|
const eventsSorted = configuration.events.sort((a, b) =>
|
||||||
|
@ -98,8 +101,12 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
{eventsSorted.map((event, index) => {
|
{eventsSorted.map((event, index) => {
|
||||||
const isUnsupported =
|
const { isDisabled, requiredSaleorVersion, missingPermission } =
|
||||||
!featureFlags?.giftCardSentEvent && event.eventType === "GIFT_CARD_SENT";
|
getEventFormStatus({
|
||||||
|
appPermissions,
|
||||||
|
featureFlags: featureFlags,
|
||||||
|
eventType: event.eventType,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Row key={event.eventType}>
|
<Table.Row key={event.eventType}>
|
||||||
|
@ -109,14 +116,22 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{...register(`events.${index}.active`)}
|
{...register(`events.${index}.active`)}
|
||||||
disabled={isUnsupported}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
{isUnsupported && (
|
{requiredSaleorVersion ? (
|
||||||
<Tooltip.Content side="left">
|
<Tooltip.Content side="left">
|
||||||
Event is available in Saleor version 3.13 and above only.
|
The feature requires Saleor version {requiredSaleorVersion}. Update
|
||||||
|
the instance to enable.
|
||||||
<Tooltip.Arrow />
|
<Tooltip.Arrow />
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
|
) : (
|
||||||
|
missingPermission && (
|
||||||
|
<Tooltip.Content side="left">
|
||||||
|
<ManagePermissionsTextLink missingPermission={missingPermission} />
|
||||||
|
<Tooltip.Arrow />
|
||||||
|
</Tooltip.Content>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
@ -128,7 +143,7 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr
|
||||||
control={control}
|
control={control}
|
||||||
name={`events.${index}.template`}
|
name={`events.${index}.template`}
|
||||||
options={templateChoices}
|
options={templateChoices}
|
||||||
disabled={isUnsupported}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
|
|
|
@ -19,6 +19,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { setBackendErrors } from "../../../lib/set-backend-errors";
|
import { setBackendErrors } from "../../../lib/set-backend-errors";
|
||||||
|
import { getEventFormStatus } from "../../../lib/get-event-form-status";
|
||||||
|
import { ManagePermissionsTextLink } from "../../../components/manage-permissions-text-link";
|
||||||
|
|
||||||
interface SmtpEventsSectionProps {
|
interface SmtpEventsSectionProps {
|
||||||
configuration: SmtpConfiguration;
|
configuration: SmtpConfiguration;
|
||||||
|
@ -29,6 +31,7 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) =>
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { data: featureFlags } = trpcClient.app.featureFlags.useQuery();
|
const { data: featureFlags } = trpcClient.app.featureFlags.useQuery();
|
||||||
|
const { data: appPermissions } = trpcClient.app.appPermissions.useQuery();
|
||||||
|
|
||||||
// Sort events by displayed label
|
// Sort events by displayed label
|
||||||
const eventsSorted = configuration.events.sort((a, b) =>
|
const eventsSorted = configuration.events.sort((a, b) =>
|
||||||
|
@ -87,8 +90,12 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) =>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
{eventsSorted.map((event, index) => {
|
{eventsSorted.map((event, index) => {
|
||||||
const isUnsupported =
|
const { isDisabled, requiredSaleorVersion, missingPermission } =
|
||||||
!featureFlags?.giftCardSentEvent && event.eventType === "GIFT_CARD_SENT";
|
getEventFormStatus({
|
||||||
|
appPermissions,
|
||||||
|
featureFlags: featureFlags,
|
||||||
|
eventType: event.eventType,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Row key={event.eventType}>
|
<Table.Row key={event.eventType}>
|
||||||
|
@ -98,14 +105,22 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) =>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{...register(`events.${index}.active`)}
|
{...register(`events.${index}.active`)}
|
||||||
disabled={isUnsupported}
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
{isUnsupported && (
|
{requiredSaleorVersion ? (
|
||||||
<Tooltip.Content side="left">
|
<Tooltip.Content side="left">
|
||||||
Event is available in Saleor version 3.13 and above only.
|
The feature requires Saleor version {requiredSaleorVersion}. Update
|
||||||
|
the instance to enable.
|
||||||
<Tooltip.Arrow />
|
<Tooltip.Arrow />
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
|
) : (
|
||||||
|
missingPermission && (
|
||||||
|
<Tooltip.Content side="left">
|
||||||
|
<ManagePermissionsTextLink missingPermission={missingPermission} />
|
||||||
|
<Tooltip.Arrow />
|
||||||
|
</Tooltip.Content>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
@ -121,7 +136,7 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) =>
|
||||||
smtpUrls.eventConfiguration(configuration.id, event.eventType)
|
smtpUrls.eventConfiguration(configuration.id, event.eventType)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
disabled={isUnsupported}
|
disabled={isDisabled}
|
||||||
>
|
>
|
||||||
Edit template
|
Edit template
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default createManifestHandler({
|
||||||
homepageUrl: "https://github.com/saleor/apps",
|
homepageUrl: "https://github.com/saleor/apps",
|
||||||
id: "saleor.app.emails-and-messages",
|
id: "saleor.app.emails-and-messages",
|
||||||
name: "Emails & Messages",
|
name: "Emails & Messages",
|
||||||
permissions: ["MANAGE_ORDERS", "MANAGE_USERS"],
|
permissions: ["MANAGE_ORDERS", "MANAGE_USERS", "MANAGE_GIFT_CARD"],
|
||||||
/**
|
/**
|
||||||
* Requires 3.10 due to invoices event payload - in previous versions, order reference was missing
|
* Requires 3.10 due to invoices event payload - in previous versions, order reference was missing
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue