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:
Krzysztof Wolski 2023-07-10 11:04:21 +02:00 committed by GitHub
parent d4089ab519
commit be4e7d2922
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 256 additions and 13 deletions

View 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.

View file

@ -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>
);
};

View 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) || [];
}

View file

@ -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",
});
});
});

View 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,
};
};

View file

@ -1,6 +1,7 @@
import { createLogger } from "@saleor/apps-shared";
import { router } from "../trpc/trpc-server";
import { protectedWithConfigurationServices } from "../trpc/protected-client-procedure-with-services";
import { fetchAppPermissions } from "../../lib/fetch-app-permissions";
export const appConfigurationRouter = router({
featureFlags: protectedWithConfigurationServices.query(async ({ ctx }) => {
@ -9,4 +10,12 @@ export const appConfigurationRouter = router({
logger.debug("appConfigurationRouter.featureFlags called");
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;
}),
});

View file

@ -19,6 +19,8 @@ import { Select } from "@saleor/react-hook-form-macaw";
import { TextLink } from "@saleor/apps-ui";
import { messageEventTypesLabels } from "../../event-handlers/message-event-types";
import { Table } from "../../../components/table";
import { getEventFormStatus } from "../../../lib/get-event-form-status";
import { ManagePermissionsTextLink } from "../../../components/manage-permissions-text-link";
interface SendgridEventsSectionProps {
configuration: SendgridConfiguration;
@ -28,6 +30,7 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr
const { notifySuccess, notifyError } = useDashboardNotification();
const { data: featureFlags } = trpcClient.app.featureFlags.useQuery();
const { data: appPermissions } = trpcClient.app.appPermissions.useQuery();
// Sort events by displayed label
const eventsSorted = configuration.events.sort((a, b) =>
@ -98,8 +101,12 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr
</Table.Header>
<Table.Body>
{eventsSorted.map((event, index) => {
const isUnsupported =
!featureFlags?.giftCardSentEvent && event.eventType === "GIFT_CARD_SENT";
const { isDisabled, requiredSaleorVersion, missingPermission } =
getEventFormStatus({
appPermissions,
featureFlags: featureFlags,
eventType: event.eventType,
});
return (
<Table.Row key={event.eventType}>
@ -109,14 +116,22 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr
<input
type="checkbox"
{...register(`events.${index}.active`)}
disabled={isUnsupported}
disabled={isDisabled}
/>
</Tooltip.Trigger>
{isUnsupported && (
{requiredSaleorVersion ? (
<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.Content>
) : (
missingPermission && (
<Tooltip.Content side="left">
<ManagePermissionsTextLink missingPermission={missingPermission} />
<Tooltip.Arrow />
</Tooltip.Content>
)
)}
</Tooltip>
</Table.Cell>
@ -128,7 +143,7 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr
control={control}
name={`events.${index}.template`}
options={templateChoices}
disabled={isUnsupported}
disabled={isDisabled}
/>
</Table.Cell>
</Table.Row>

View file

@ -19,6 +19,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { trpcClient } from "../../trpc/trpc-client";
import { useForm } from "react-hook-form";
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 {
configuration: SmtpConfiguration;
@ -29,6 +31,7 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) =>
const router = useRouter();
const { data: featureFlags } = trpcClient.app.featureFlags.useQuery();
const { data: appPermissions } = trpcClient.app.appPermissions.useQuery();
// Sort events by displayed label
const eventsSorted = configuration.events.sort((a, b) =>
@ -87,8 +90,12 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) =>
</Table.Header>
<Table.Body>
{eventsSorted.map((event, index) => {
const isUnsupported =
!featureFlags?.giftCardSentEvent && event.eventType === "GIFT_CARD_SENT";
const { isDisabled, requiredSaleorVersion, missingPermission } =
getEventFormStatus({
appPermissions,
featureFlags: featureFlags,
eventType: event.eventType,
});
return (
<Table.Row key={event.eventType}>
@ -98,14 +105,22 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) =>
<input
type="checkbox"
{...register(`events.${index}.active`)}
disabled={isUnsupported}
disabled={isDisabled}
/>
</Tooltip.Trigger>
{isUnsupported && (
{requiredSaleorVersion ? (
<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.Content>
) : (
missingPermission && (
<Tooltip.Content side="left">
<ManagePermissionsTextLink missingPermission={missingPermission} />
<Tooltip.Arrow />
</Tooltip.Content>
)
)}
</Tooltip>
</Table.Cell>
@ -121,7 +136,7 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) =>
smtpUrls.eventConfiguration(configuration.id, event.eventType)
);
}}
disabled={isUnsupported}
disabled={isDisabled}
>
Edit template
</Button>

View file

@ -28,7 +28,7 @@ export default createManifestHandler({
homepageUrl: "https://github.com/saleor/apps",
id: "saleor.app.emails-and-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
*/