diff --git a/.changeset/seven-carrots-hope.md b/.changeset/seven-carrots-hope.md new file mode 100644 index 000000000..3f6465f5e --- /dev/null +++ b/.changeset/seven-carrots-hope.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Now App can use app-sdk (with 0.43.0 version) to request new permissions from the dashboard user diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 6869284cd..b0df04f9e 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1750,6 +1750,9 @@ "context": "notification", "string": "Published pages" }, + "B+Ba0R": { + "string": "Requested Permissions" + }, "B/y6LC": { "context": "section header", "string": "Unfulfilled Products" @@ -4152,6 +4155,9 @@ "context": "total price", "string": "Total" }, + "S1p0Ja": { + "string": "The app will have access to new permissions. From now on it will be able to use them to perform operations these permissions allow. You should ensure you trust the app before you approve." + }, "S22jIs": { "context": "button", "string": "Set new password" @@ -4187,6 +4193,9 @@ "SHm7ee": { "string": "Search by product name, attribute, product type etc..." }, + "SI3/nl": { + "string": "Nothing will change in terms of permissions. The Dashboard will redirect to the app and inform it that you denied the request." + }, "SKFr04": { "string": "Attribute not found." }, @@ -4695,6 +4704,9 @@ "context": "weight units type", "string": "Weight" }, + "VkYZQ8": { + "string": "Current Permissions" + }, "VmMDLN": { "context": "permission list item description", "string": "This group is last source of that permission" @@ -4746,6 +4758,9 @@ "context": "product field", "string": "Name" }, + "WCaf5C": { + "string": "Approve" + }, "WCg2GZ": { "context": "change warehouse dialog search placeholder", "string": "Search warehouses" @@ -5161,6 +5176,9 @@ "Yo2kC+": { "string": "Couldn't process image" }, + "Yo4h/D": { + "string": "Learn more about permissions" + }, "YpLVVc": { "context": "action", "string": "Exclude postal codes" @@ -5677,6 +5695,9 @@ "context": "button", "string": "Unassign and save" }, + "cOki0G": { + "string": "What happens if I deny?" + }, "cPAc45": { "context": "column picker search no results message", "string": "No results found" @@ -6318,6 +6339,9 @@ "context": "table head", "string": "Collection Name" }, + "htvX+Z": { + "string": "Deny" + }, "hw9Fah": { "context": "button", "string": "Send invite" @@ -6517,6 +6541,10 @@ "context": "description", "string": "No members found" }, + "jVjsVq": { + "context": "App by Author", + "string": "by" + }, "jWna9Q": { "string": "Content Type Name" }, @@ -6524,6 +6552,9 @@ "context": "product availability", "string": "Hide in product listings" }, + "ja+tNj": { + "string": "requests access to new permissions." + }, "jd/LWa": { "string": "Voucher applies to all countries" }, @@ -8024,6 +8055,10 @@ "context": "number of postal code ranges", "string": "{number} postal code ranges" }, + "udJUSa": { + "context": "Authorize {app name}", + "string": "Authorize" + }, "ufD5Jr": { "string": "Content type" }, @@ -8271,6 +8306,9 @@ "context": "section header", "string": "Plugin Information and Status" }, + "w6kcxY": { + "string": "What happens if I approve?" + }, "w9xgN9": { "context": "see error log label in notification", "string": "See error log" diff --git a/package-lock.json b/package-lock.json index 555ea2586..0b9a31bdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,7 +116,7 @@ "@graphql-codegen/typescript-react-apollo": "^3.2.5", "@percy/cli": "^1.21.0", "@percy/cypress": "^3.1.2", - "@saleor/app-sdk": "0.41.0", + "@saleor/app-sdk": "0.43.0", "@swc/jest": "^0.2.26", "@types/apollo-upload-client": "^17.0.2", "@types/color-convert": "^2.0.0", @@ -8758,9 +8758,9 @@ } }, "node_modules/@saleor/app-sdk": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@saleor/app-sdk/-/app-sdk-0.41.0.tgz", - "integrity": "sha512-9Yc73N8QvqfHlhsPzuWqwlbl+o/gq/VJ+HB2OUro68zTF+rapEssb79OUGLp2JmnTlM4xovCiPwPlDsW8nVSqw==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@saleor/app-sdk/-/app-sdk-0.43.0.tgz", + "integrity": "sha512-hDZ/VNgz6vxeA1h4GtKYs2la8j8E2OocKWDK6HZYbsqDg83K+9LzFOIOHf7U/ZEmBgXR0Ezd0vuhmJBPKLC4jA==", "dev": true, "dependencies": { "@changesets/cli": "^2.26.0", @@ -41238,9 +41238,9 @@ } }, "@saleor/app-sdk": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@saleor/app-sdk/-/app-sdk-0.41.0.tgz", - "integrity": "sha512-9Yc73N8QvqfHlhsPzuWqwlbl+o/gq/VJ+HB2OUro68zTF+rapEssb79OUGLp2JmnTlM4xovCiPwPlDsW8nVSqw==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@saleor/app-sdk/-/app-sdk-0.43.0.tgz", + "integrity": "sha512-hDZ/VNgz6vxeA1h4GtKYs2la8j8E2OocKWDK6HZYbsqDg83K+9LzFOIOHf7U/ZEmBgXR0Ezd0vuhmJBPKLC4jA==", "dev": true, "requires": { "@changesets/cli": "^2.26.0", diff --git a/package.json b/package.json index e48810132..e60c63ff0 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@graphql-codegen/typescript-react-apollo": "^3.2.5", "@percy/cli": "^1.21.0", "@percy/cypress": "^3.1.2", - "@saleor/app-sdk": "0.41.0", + "@saleor/app-sdk": "0.43.0", "@swc/jest": "^0.2.26", "@types/apollo-upload-client": "^17.0.2", "@types/color-convert": "^2.0.0", diff --git a/src/apps/apps-routing.tsx b/src/apps/apps-routing.tsx index 6ca16fce7..b4424e70d 100644 --- a/src/apps/apps-routing.tsx +++ b/src/apps/apps-routing.tsx @@ -11,6 +11,7 @@ import { AppInstallView, AppListView, AppManageView, + AppPermissionRequestView, AppView, } from "src/apps/views"; @@ -64,6 +65,11 @@ export const AppsSectionRoot = () => { path={AppPaths.resolveAppDetailsPath(":id")} component={AppManageRoute} /> + diff --git a/src/apps/components/AppFrame/appActionsHandler.test.ts b/src/apps/components/AppFrame/appActionsHandler.test.ts index 84d39810a..82392979f 100644 --- a/src/apps/components/AppFrame/appActionsHandler.test.ts +++ b/src/apps/components/AppFrame/appActionsHandler.test.ts @@ -269,4 +269,26 @@ describe("AppActionsHandler", function () { }); }); }); + + describe("useHandlePermissionRequest", () => { + it("Redirects to a dedicated page with params from action", () => { + const hookRenderResult = renderHook(() => + AppActionsHandler.useHandlePermissionRequest("XYZ"), + ); + + hookRenderResult.result.current.handle({ + type: "requestPermissions", + payload: { + actionId: "123", + permissions: ["MANAGE_ORDERS", "MANAGE_CHANNELS"], + redirectPath: "/permissions-result", + }, + }); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith( + "/apps/XYZ/permissions?redirectPath=%2Fpermissions-result&requestedPermissions=MANAGE_ORDERS%2CMANAGE_CHANNELS" + ); + }); + }); }); diff --git a/src/apps/components/AppFrame/appActionsHandler.ts b/src/apps/components/AppFrame/appActionsHandler.ts index 5e90b5bb7..8c454b380 100644 --- a/src/apps/components/AppFrame/appActionsHandler.ts +++ b/src/apps/components/AppFrame/appActionsHandler.ts @@ -11,6 +11,7 @@ import { NotificationAction, NotifyReady, RedirectAction, + RequestPermissions, UpdateRouting, } from "@saleor/app-sdk/app-bridge"; import { useIntl } from "react-intl"; @@ -208,10 +209,44 @@ const useNotifyReadyAction = ( }; }; +const useHandlePermissionRequest = (appId: string) => { + const navigate = useNavigator(); + + return { + handle: (action: RequestPermissions) => { + const { actionId, permissions, redirectPath } = action.payload; + + debug("Received RequestPermissions action"); + + if (permissions.length === 0) { + debug("Empty permissions array, skipping"); + + return createResponseStatus(actionId, false); + } + + if (!redirectPath || redirectPath.length === 0) { + debug("Invalid path, skipping"); + + return createResponseStatus(actionId, false); + } + + navigate( + AppUrls.resolveRequestPermissionsUrl(appId, { + redirectPath, + requestedPermissions: permissions, + }), + ); + + return createResponseStatus(actionId, true); + }, + }; +}; + export const AppActionsHandler = { useHandleNotificationAction, useHandleUpdateRoutingAction, useHandleRedirectAction, useNotifyReadyAction, createResponseStatus, + useHandlePermissionRequest, }; diff --git a/src/apps/components/AppFrame/useAppActions.ts b/src/apps/components/AppFrame/useAppActions.ts index 9d3c31d69..9ed10ebd8 100644 --- a/src/apps/components/AppFrame/useAppActions.ts +++ b/src/apps/components/AppFrame/useAppActions.ts @@ -30,6 +30,8 @@ export const useAppActions = ( appToken, versions, ); + const { handle: handlePermissionRequest } = + AppActionsHandler.useHandlePermissionRequest(appId); /** * Store if app has performed a handshake with Dashboard, to avoid sending events before that @@ -57,6 +59,9 @@ export const useAppActions = ( return response; } + case "requestPermissions": { + return handlePermissionRequest(action) + } default: { throw new Error("Unknown action type"); } diff --git a/src/apps/components/AppPermissionsDialog/AppPermissionsDialog.tsx b/src/apps/components/AppPermissionsDialog/AppPermissionsDialog.tsx index bc7d5b072..6bc082a27 100644 --- a/src/apps/components/AppPermissionsDialog/AppPermissionsDialog.tsx +++ b/src/apps/components/AppPermissionsDialog/AppPermissionsDialog.tsx @@ -2,7 +2,7 @@ import { AppPermissionsDialogConfirmation } from "@dashboard/apps/components/App import { AppPermissionsDialogPermissionPicker } from "@dashboard/apps/components/AppPermissionsDialog/AppPermissionsDialogPermissionPicker"; import { useAppPermissionsDialogState } from "@dashboard/apps/components/AppPermissionsDialog/AppPermissionsDialogState"; import { AppPermissionsDialogMessages } from "@dashboard/apps/components/AppPermissionsDialog/messages"; -import { useGetAvailableAppPermissions } from "@dashboard/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions"; +import { useGetAvailableAppPermissions } from "@dashboard/apps/hooks/useGetAvailableAppPermissions"; import { PermissionEnum, useAppQuery, diff --git a/src/apps/components/AppPermissionsDialog/AppPermissionsDialogConfirmation.tsx b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogConfirmation.tsx index 5d0c3ce49..34e9f0f56 100644 --- a/src/apps/components/AppPermissionsDialog/AppPermissionsDialogConfirmation.tsx +++ b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogConfirmation.tsx @@ -1,5 +1,5 @@ import { AppPermissionsDialogMessages } from "@dashboard/apps/components/AppPermissionsDialog/messages"; -import { useGetAvailableAppPermissions } from "@dashboard/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions"; +import { useGetAvailableAppPermissions } from "@dashboard/apps/hooks/useGetAvailableAppPermissions"; import { PermissionEnum } from "@dashboard/graphql"; import { Box, Button, Text } from "@saleor/macaw-ui/next"; import React from "react"; diff --git a/src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.ts b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.ts index dcb8d643e..b6b8d1409 100644 --- a/src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.ts +++ b/src/apps/components/AppPermissionsDialog/AppPermissionsDialogState.ts @@ -1,4 +1,4 @@ -import { getPermissionsDiff } from "@dashboard/apps/components/AppPermissionsDialog/getPermissionsDiff"; +import { getPermissionsDiff } from "@dashboard/apps/getPermissionsDiff"; import { PermissionEnum } from "@dashboard/graphql"; import { useState } from "react"; diff --git a/src/apps/components/AppPermissionsDialog/getPermissionsDiff.test.ts b/src/apps/getPermissionsDiff.test.ts similarity index 95% rename from src/apps/components/AppPermissionsDialog/getPermissionsDiff.test.ts rename to src/apps/getPermissionsDiff.test.ts index f15c5425e..55fa5761f 100644 --- a/src/apps/components/AppPermissionsDialog/getPermissionsDiff.test.ts +++ b/src/apps/getPermissionsDiff.test.ts @@ -1,6 +1,7 @@ -import { getPermissionsDiff } from "@dashboard/apps/components/AppPermissionsDialog/getPermissionsDiff"; import { PermissionEnum } from "@dashboard/graphql"; +import { getPermissionsDiff } from "./getPermissionsDiff"; + describe("getPermissionsDiff", () => { describe("Correctly resolves added permissions", () => { test("From empty to one new", () => { diff --git a/src/apps/components/AppPermissionsDialog/getPermissionsDiff.ts b/src/apps/getPermissionsDiff.ts similarity index 100% rename from src/apps/components/AppPermissionsDialog/getPermissionsDiff.ts rename to src/apps/getPermissionsDiff.ts diff --git a/src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.test.ts b/src/apps/hooks/useGetAvailableAppPermissions.test.ts similarity index 95% rename from src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.test.ts rename to src/apps/hooks/useGetAvailableAppPermissions.test.ts index 8b5bc044a..c6f748595 100644 --- a/src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.test.ts +++ b/src/apps/hooks/useGetAvailableAppPermissions.test.ts @@ -1,8 +1,9 @@ -import { useGetAvailableAppPermissions } from "@dashboard/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions"; import { PermissionEnum } from "@dashboard/graphql"; import useShop from "@dashboard/hooks/useShop"; import { renderHook } from "@testing-library/react-hooks"; +import { useGetAvailableAppPermissions } from "./useGetAvailableAppPermissions"; + type PermissionsFromApi = Array<{ __typename: "Permission"; code: PermissionEnum; diff --git a/src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.ts b/src/apps/hooks/useGetAvailableAppPermissions.ts similarity index 93% rename from src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.ts rename to src/apps/hooks/useGetAvailableAppPermissions.ts index 1dc3080d4..719af4465 100644 --- a/src/apps/components/AppPermissionsDialog/useGetAvailableAppPermissions.ts +++ b/src/apps/hooks/useGetAvailableAppPermissions.ts @@ -7,7 +7,7 @@ export const useGetAvailableAppPermissions = () => { /** * App can't have MANAGE_APPS so filter it out */ - const availablePermissions = shopData.permissions + const availablePermissions = shopData?.permissions .filter(perm => perm.code !== "MANAGE_APPS") .map(p => ({ code: p.code, @@ -41,5 +41,6 @@ export const useGetAvailableAppPermissions = () => { return { availablePermissions, mapCodesToNames, + isReady: !!shopData, }; }; diff --git a/src/apps/urls.ts b/src/apps/urls.ts index 2add6761c..f49d4df80 100644 --- a/src/apps/urls.ts +++ b/src/apps/urls.ts @@ -47,6 +47,8 @@ export const AppPaths = { resolveAppDeepPath: (id: string, subPath: string) => urlJoin(AppPaths.resolveAppPath(id), subPath), appInstallPath: urlJoin(AppSections.appsSection, "install"), + resolveRequestPermissionsPath: (id: string) => + urlJoin(AppSections.appsSection, id, "permissions"), }; export const AppUrls = { @@ -140,4 +142,20 @@ export const AppUrls = { return urlJoin(appUrl, window.location.search, iframeContextQueryString); }, + resolveRequestPermissionsUrl: ( + id: string, + params: { + requestedPermissions: string[]; + redirectPath: string; + }, + ) => + urlJoin( + AppSections.appsSection, + id, + "permissions", + `?${stringifyQs({ + redirectPath: params.redirectPath, + requestedPermissions: params.requestedPermissions.join(","), + })}`, + ), }; diff --git a/src/apps/views/AppPermissionRequestView/AppPermissionRequestView.tsx b/src/apps/views/AppPermissionRequestView/AppPermissionRequestView.tsx new file mode 100644 index 000000000..4a1f66cc1 --- /dev/null +++ b/src/apps/views/AppPermissionRequestView/AppPermissionRequestView.tsx @@ -0,0 +1,230 @@ +import { createAppsDebug } from "@dashboard/apps/apps-debug"; +import { getPermissionsDiff } from "@dashboard/apps/getPermissionsDiff"; +import { useGetAvailableAppPermissions } from "@dashboard/apps/hooks/useGetAvailableAppPermissions"; +import Link from "@dashboard/components/Link"; +import { + PermissionEnum, + useAppQuery, + useAppUpdatePermissionsMutation, +} from "@dashboard/graphql"; +import { Box, BoxProps, Button, Text, TextProps } from "@saleor/macaw-ui/next"; +import React, { useEffect } from "react"; +import { useIntl } from "react-intl"; +import { useLocation, useParams } from "react-router"; + +import { appPermissionsRequestViewMessages } from "./messages"; +import { usePermissionsRequestRedirects } from "./usePermissionsRequestRedirects"; + +const SmallText = (props: TextProps) => ; +const SmallHeading = (props: TextProps) => ( + +); +const WrapperBox = (props: BoxProps) => ( + +); + +function usePageQuery() { + const { search } = useLocation(); + + return React.useMemo(() => { + const params = new URLSearchParams(search); + const permissionsParams = params.get("requestedPermissions"); + + const requestedPermissions = permissionsParams + ? (permissionsParams.split(",") as PermissionEnum[]) + : []; + const redirectPath = params.get("redirectPath"); + + if (!redirectPath) throw new Error("Redirect path is required"); + + return { + requestedPermissions, + redirectPath, + }; + }, [search]); +} + +const debug = createAppsDebug("AppPermissionRequestView"); + +export const AppPermissionRequestView = () => { + const params = useParams<{ id: string }>(); + const { redirectPath, requestedPermissions } = usePageQuery(); + const { formatMessage } = useIntl(); + + const appId = params.id; + + const { data } = useAppQuery({ + variables: { + id: appId, + }, + }); + const [updatePermissions, { loading }] = useAppUpdatePermissionsMutation(); + + const { navigateToAppApproved, navigateToAppDenied } = + usePermissionsRequestRedirects({ + appId, + redirectPath, + }); + + const { mapCodesToNames, isReady } = useGetAvailableAppPermissions(); + + useEffect(() => { + if (!data || !data.app || !isReady) return; + + const diff = getPermissionsDiff( + (data.app.permissions ?? []).map(p => p.code), + requestedPermissions, + ); + + /** + * If app requests permissions that are already granted, redirect to app with success status + */ + if (diff.added.length === 0) navigateToAppApproved(); + }, [data, requestedPermissions]); + + if (!data || !isReady || !data.app) return null; + + const onApprove = () => { + updatePermissions({ + variables: { + id: appId, + permissions: [ + ...(data.app?.permissions ?? []).map(p => p.code), + ...requestedPermissions, + ], + }, + }) + .then(resp => { + const hasError = resp.data?.appUpdate?.errors?.length; + + if (hasError) { + debug("Failed to update the app permissions"); + + return navigateToAppDenied("UPDATE_PERMISSIONS_FAILED"); + } + + return navigateToAppApproved(); + }) + .catch(err => { + debug("updatePermissions failed", err); + + return navigateToAppDenied("UPDATE_PERMISSIONS_FAILED"); + }); + }; + + const onDeny = () => navigateToAppDenied("USER_DENIED_PERMISSIONS"); + + return ( + + + {formatMessage(appPermissionsRequestViewMessages.headerAuthorize)}{" "} + {data.app.name} + + + + {data.app.brand?.logo.default && ( + + )} + + + {data.app.name}{" "} + {formatMessage(appPermissionsRequestViewMessages.by)}{" "} + {data.app.author} + + + {formatMessage( + appPermissionsRequestViewMessages.requestsNewPermissions, + )} + + + + + + + {formatMessage( + appPermissionsRequestViewMessages.currentPermissionsHeader, + )} + + {(data.app.permissions ?? []).map(permission => ( + + {permission.name} + + ))} + + + + {formatMessage( + appPermissionsRequestViewMessages.requestedPermissionsHeader, + )} + + {mapCodesToNames(requestedPermissions).map(permissionName => ( + + {permissionName} + + ))} + + + + + {formatMessage( + appPermissionsRequestViewMessages.approveScenarioHelperHeader, + )} + + + {formatMessage( + appPermissionsRequestViewMessages.approveScenarioHelperBody, + )} + + + + {formatMessage( + appPermissionsRequestViewMessages.permissionsDocsLink, + )} + + + + {formatMessage( + appPermissionsRequestViewMessages.denyScenarioHelperHeader, + )} + + + {formatMessage( + appPermissionsRequestViewMessages.denyScenarioHelperBody, + )} + + + + + + + + + ); +}; diff --git a/src/apps/views/AppPermissionRequestView/index.ts b/src/apps/views/AppPermissionRequestView/index.ts new file mode 100644 index 000000000..067acbdb3 --- /dev/null +++ b/src/apps/views/AppPermissionRequestView/index.ts @@ -0,0 +1 @@ +export * from "./AppPermissionRequestView"; diff --git a/src/apps/views/AppPermissionRequestView/messages.ts b/src/apps/views/AppPermissionRequestView/messages.ts new file mode 100644 index 000000000..2fdd74162 --- /dev/null +++ b/src/apps/views/AppPermissionRequestView/messages.ts @@ -0,0 +1,54 @@ +import { defineMessages } from "react-intl"; + +export const appPermissionsRequestViewMessages = defineMessages({ + headerAuthorize: { + defaultMessage: "Authorize", + description: "Authorize {app name}", + id: "udJUSa", + }, + by: { + defaultMessage: "by", + description: "App by Author", + id: "jVjsVq", + }, + requestsNewPermissions: { + defaultMessage: "requests access to new permissions.", + id: "ja+tNj", + }, + currentPermissionsHeader: { + defaultMessage: "Current Permissions", + id: "VkYZQ8", + }, + requestedPermissionsHeader: { + defaultMessage: "Requested Permissions", + id: 'B+Ba0R', + }, + approveScenarioHelperHeader: { + defaultMessage: "What happens if I approve?", + id: "w6kcxY", + }, + approveScenarioHelperBody: { + defaultMessage: + "The app will have access to new permissions. From now on it will be able to use them to perform operations these permissions allow. You should ensure you trust the app before you approve.", + id: 'S1p0Ja', + }, + permissionsDocsLink: { + defaultMessage: "Learn more about permissions", + id: 'Yo4h/D' + }, + denyScenarioHelperHeader: { + defaultMessage: "What happens if I deny?", id: 'cOki0G', + }, + denyScenarioHelperBody: { + defaultMessage: + "Nothing will change in terms of permissions. The Dashboard will redirect to the app and inform it that you denied the request.", id: 'SI3/nl', + }, + denyButton: { + defaultMessage: "Deny", + id: 'htvX+Z', + }, + approveButton: { + defaultMessage: "Approve", + id: 'WCaf5C', + }, +}); diff --git a/src/apps/views/AppPermissionRequestView/usePermissionsRequestRedirects.test.ts b/src/apps/views/AppPermissionRequestView/usePermissionsRequestRedirects.test.ts new file mode 100644 index 000000000..2f9729938 --- /dev/null +++ b/src/apps/views/AppPermissionRequestView/usePermissionsRequestRedirects.test.ts @@ -0,0 +1,38 @@ +import { renderHook } from "@testing-library/react-hooks"; + +import { usePermissionsRequestRedirects } from "./usePermissionsRequestRedirects"; + +const mockNavigate = jest.fn(); +jest.mock("@dashboard/hooks/useNavigator", () => () => mockNavigate); + +describe("usePermissionsRequestRedirects", () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it("Navigates to redirect url provided by app - if approved", () => { + const { result } = renderHook(() => + usePermissionsRequestRedirects({ + appId: "XYZ", + redirectPath: "/permissions-request-result", + }), + ); + + result.current.navigateToAppApproved(); + + expect(mockNavigate).toHaveBeenCalledWith("/apps/XYZ/app?appPath=/permissions-request-result"); + }); + + it("Navigates to redirect url provided by app and appends ?error - provided", () => { + const { result } = renderHook(() => + usePermissionsRequestRedirects({ + appId: "XYZ", + redirectPath: "/permissions-request-result", + }), + ); + + result.current.navigateToAppDenied('USER_DENIED_PERMISSIONS'); + + expect(mockNavigate).toHaveBeenCalledWith("/apps/XYZ/app?appPath=/permissions-request-result&error=USER_DENIED_PERMISSIONS"); + }); +}); diff --git a/src/apps/views/AppPermissionRequestView/usePermissionsRequestRedirects.ts b/src/apps/views/AppPermissionRequestView/usePermissionsRequestRedirects.ts new file mode 100644 index 000000000..44efcddbd --- /dev/null +++ b/src/apps/views/AppPermissionRequestView/usePermissionsRequestRedirects.ts @@ -0,0 +1,33 @@ +import { AppPaths } from "@dashboard/apps/urls"; +import useNavigator from "@dashboard/hooks/useNavigator"; + +type Errors = "USER_DENIED_PERMISSIONS" | "UPDATE_PERMISSIONS_FAILED"; + +export const usePermissionsRequestRedirects = ({ + appId, + redirectPath, +}: { + appId: string; + redirectPath: string; +}) => { + const navigate = useNavigator(); + + const navigateToAppApproved = () => { + navigate( + AppPaths.resolveAppPath(encodeURIComponent(appId)) + + `?appPath=${redirectPath}`, + ); + }; + + const navigateToAppDenied = (error: Errors) => { + navigate( + AppPaths.resolveAppPath(encodeURIComponent(appId)) + + `?appPath=${redirectPath}&error=${error}`, + ); + }; + + return { + navigateToAppApproved, + navigateToAppDenied, + }; +}; diff --git a/src/apps/views/AppView/AppView.tsx b/src/apps/views/AppView/AppView.tsx index acc8b35bc..b6f2c3e10 100644 --- a/src/apps/views/AppView/AppView.tsx +++ b/src/apps/views/AppView/AppView.tsx @@ -26,6 +26,9 @@ export const AppView: React.FC = ({ id }) => { const notify = useNotifier(); const intl = useIntl(); + const queryParams = new URLSearchParams(location.search); + const appPath = queryParams.get("appPath"); + const handleError = useCallback( () => notify({ @@ -39,12 +42,16 @@ export const AppView: React.FC = ({ id }) => { return navigate(AppPaths.appListPath)} />; } - const appCompleteUrl = AppUrls.resolveAppCompleteUrlFromDashboardUrl( + let appCompleteUrl = AppUrls.resolveAppCompleteUrlFromDashboardUrl( location.pathname, data?.app?.appUrl || "", id, ); + if(appPath) { + appCompleteUrl = `${appCompleteUrl}/${appPath}`; + } + if (!data || !appCompleteUrl) { return null; } diff --git a/src/apps/views/index.ts b/src/apps/views/index.ts index 245b93fca..da6ce2510 100644 --- a/src/apps/views/index.ts +++ b/src/apps/views/index.ts @@ -1,4 +1,5 @@ export * from "./AppInstallView"; export * from "./AppListView"; export * from "./AppManageView"; +export * from "./AppPermissionRequestView"; export * from "./AppView";