diff --git a/.changeset/popular-clouds-play.md b/.changeset/popular-clouds-play.md new file mode 100644 index 0000000..c36ea74 --- /dev/null +++ b/.changeset/popular-clouds-play.md @@ -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. diff --git a/apps/emails-and-messages/src/components/manage-permissions-text-link.tsx b/apps/emails-and-messages/src/components/manage-permissions-text-link.tsx new file mode 100644 index 0000000..53dcefc --- /dev/null +++ b/apps/emails-and-messages/src/components/manage-permissions-text-link.tsx @@ -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 ( + + To use this feature, the {missingPermission} permission is required. Please reinstall the + app. + + ); + } + + return ( + + To use this feature, the {missingPermission} permission is required. Please go to{" "} + {/* TODO: Update the shared package to handle dashboard links */} + { + e.preventDefault(); + + appBridge?.dispatch( + actions.Redirect({ + to: `/apps/${appId}`, + }) + ); + }} + href="#" + > + Manage App section + + , and grant the permission + + ); +}; diff --git a/apps/emails-and-messages/src/lib/fetch-app-permissions.ts b/apps/emails-and-messages/src/lib/fetch-app-permissions.ts new file mode 100644 index 0000000..35d6c49 --- /dev/null +++ b/apps/emails-and-messages/src/lib/fetch-app-permissions.ts @@ -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 { + const { error, data } = await client + .query(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) || []; +} diff --git a/apps/emails-and-messages/src/lib/get-event-form-status.test.ts b/apps/emails-and-messages/src/lib/get-event-form-status.test.ts new file mode 100644 index 0000000..7b0c7f9 --- /dev/null +++ b/apps/emails-and-messages/src/lib/get-event-form-status.test.ts @@ -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", + }); + }); +}); diff --git a/apps/emails-and-messages/src/lib/get-event-form-status.ts b/apps/emails-and-messages/src/lib/get-event-form-status.ts new file mode 100644 index 0000000..693906f --- /dev/null +++ b/apps/emails-and-messages/src/lib/get-event-form-status.ts @@ -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, + }; +}; diff --git a/apps/emails-and-messages/src/modules/app-configuration/app-configuration.router.ts b/apps/emails-and-messages/src/modules/app-configuration/app-configuration.router.ts index d3b9eee..bacfa80 100644 --- a/apps/emails-and-messages/src/modules/app-configuration/app-configuration.router.ts +++ b/apps/emails-and-messages/src/modules/app-configuration/app-configuration.router.ts @@ -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; + }), }); diff --git a/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-events-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-events-section.tsx index 7d8a741..46f973f 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-events-section.tsx +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-events-section.tsx @@ -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 {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 ( @@ -109,14 +116,22 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr - {isUnsupported && ( + {requiredSaleorVersion ? ( - Event is available in Saleor version 3.13 and above only. + The feature requires Saleor version {requiredSaleorVersion}. Update + the instance to enable. + ) : ( + missingPermission && ( + + + + + ) )} @@ -128,7 +143,7 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr control={control} name={`events.${index}.template`} options={templateChoices} - disabled={isUnsupported} + disabled={isDisabled} /> diff --git a/apps/emails-and-messages/src/modules/smtp/ui/smtp-events-section.tsx b/apps/emails-and-messages/src/modules/smtp/ui/smtp-events-section.tsx index c755340..ca4bd95 100644 --- a/apps/emails-and-messages/src/modules/smtp/ui/smtp-events-section.tsx +++ b/apps/emails-and-messages/src/modules/smtp/ui/smtp-events-section.tsx @@ -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) => {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 ( @@ -98,14 +105,22 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) => - {isUnsupported && ( + {requiredSaleorVersion ? ( - Event is available in Saleor version 3.13 and above only. + The feature requires Saleor version {requiredSaleorVersion}. Update + the instance to enable. + ) : ( + missingPermission && ( + + + + + ) )} @@ -121,7 +136,7 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) => smtpUrls.eventConfiguration(configuration.id, event.eventType) ); }} - disabled={isUnsupported} + disabled={isDisabled} > Edit template diff --git a/apps/emails-and-messages/src/pages/api/manifest.ts b/apps/emails-and-messages/src/pages/api/manifest.ts index a758492..655fb1a 100644 --- a/apps/emails-and-messages/src/pages/api/manifest.ts +++ b/apps/emails-and-messages/src/pages/api/manifest.ts @@ -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 */