App permissions request flow (#3962)
* WIP - app permissions request flow * Add copy to permissions view * Add better ui * Handle scenario when app already has permissions * refactor urls * tests * Extract translations * fix strict ts * extract mesages * Fix linter * CR Fixes + handle case when user cant update app * Extract messages
This commit is contained in:
parent
ad01400ab1
commit
fa6e469792
23 changed files with 511 additions and 15 deletions
5
.changeset/seven-carrots-hope.md
Normal file
5
.changeset/seven-carrots-hope.md
Normal file
|
@ -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
|
|
@ -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"
|
||||
|
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={AppPaths.resolveRequestPermissionsPath(":id")}
|
||||
component={AppPermissionRequestView}
|
||||
/>
|
||||
<Route path={AppPaths.resolveAppPath(":id")} component={AppViewRoute} />
|
||||
</Switch>
|
||||
</>
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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", () => {
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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(","),
|
||||
})}`,
|
||||
),
|
||||
};
|
||||
|
|
|
@ -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) => <Text size="small" {...props} />;
|
||||
const SmallHeading = (props: TextProps) => (
|
||||
<SmallText as="h2" variant="heading" {...props} />
|
||||
);
|
||||
const WrapperBox = (props: BoxProps) => (
|
||||
<Box
|
||||
marginX="auto"
|
||||
marginY={12}
|
||||
borderColor="neutralHighlight"
|
||||
borderWidth={1}
|
||||
borderStyle="solid"
|
||||
padding={8}
|
||||
__maxWidth={"600px"}
|
||||
borderRadius={4}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
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 (
|
||||
<Box padding={12}>
|
||||
<SmallText as="h1" variant="hero" textAlign="center">
|
||||
{formatMessage(appPermissionsRequestViewMessages.headerAuthorize)}{" "}
|
||||
{data.app.name}
|
||||
</SmallText>
|
||||
<WrapperBox>
|
||||
<Box display="flex" gap={4}>
|
||||
{data.app.brand?.logo.default && (
|
||||
<Box as="img" __width={"50px"} src={data.app.brand.logo.default} />
|
||||
)}
|
||||
<Box>
|
||||
<Text>
|
||||
<Text variant="bodyStrong">{data.app.name}</Text>{" "}
|
||||
{formatMessage(appPermissionsRequestViewMessages.by)}{" "}
|
||||
{data.app.author}
|
||||
</Text>
|
||||
<Text as="p" color="textNeutralSubdued">
|
||||
{formatMessage(
|
||||
appPermissionsRequestViewMessages.requestsNewPermissions,
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box marginY={8} display="grid" gridTemplateColumns={2} gap={6}>
|
||||
<Box
|
||||
borderRightStyle="solid"
|
||||
borderWidth={1}
|
||||
borderColor="neutralHighlight"
|
||||
paddingRight={6}
|
||||
>
|
||||
<SmallHeading marginBottom={2}>
|
||||
{formatMessage(
|
||||
appPermissionsRequestViewMessages.currentPermissionsHeader,
|
||||
)}
|
||||
</SmallHeading>
|
||||
{(data.app.permissions ?? []).map(permission => (
|
||||
<Text as="p" key={permission.code}>
|
||||
{permission.name}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
<Box>
|
||||
<SmallHeading marginBottom={2}>
|
||||
{formatMessage(
|
||||
appPermissionsRequestViewMessages.requestedPermissionsHeader,
|
||||
)}
|
||||
</SmallHeading>
|
||||
{mapCodesToNames(requestedPermissions).map(permissionName => (
|
||||
<Text as="p" key={permissionName}>
|
||||
{permissionName}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
borderTopStyle="solid"
|
||||
paddingTop={8}
|
||||
borderWidth={1}
|
||||
borderColor="neutralHighlight"
|
||||
>
|
||||
<SmallHeading marginBottom={2}>
|
||||
{formatMessage(
|
||||
appPermissionsRequestViewMessages.approveScenarioHelperHeader,
|
||||
)}
|
||||
</SmallHeading>
|
||||
<SmallText>
|
||||
{formatMessage(
|
||||
appPermissionsRequestViewMessages.approveScenarioHelperBody,
|
||||
)}
|
||||
</SmallText>
|
||||
<SmallText as="p">
|
||||
<Link
|
||||
target="__blank"
|
||||
href="https://docs.saleor.io/docs/3.x/developer/permissions#app-permissions"
|
||||
>
|
||||
{formatMessage(
|
||||
appPermissionsRequestViewMessages.permissionsDocsLink,
|
||||
)}
|
||||
</Link>
|
||||
</SmallText>
|
||||
<SmallHeading marginBottom={2} marginTop={4}>
|
||||
{formatMessage(
|
||||
appPermissionsRequestViewMessages.denyScenarioHelperHeader,
|
||||
)}
|
||||
</SmallHeading>
|
||||
<SmallText>
|
||||
{formatMessage(
|
||||
appPermissionsRequestViewMessages.denyScenarioHelperBody,
|
||||
)}
|
||||
</SmallText>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="flex-end" gap={4} marginTop={12}>
|
||||
<Button disabled={loading} onClick={onDeny} variant="secondary">
|
||||
{formatMessage(appPermissionsRequestViewMessages.denyButton)}
|
||||
</Button>
|
||||
<Button disabled={loading} onClick={onApprove}>
|
||||
{formatMessage(appPermissionsRequestViewMessages.approveButton)}
|
||||
</Button>
|
||||
</Box>
|
||||
</WrapperBox>
|
||||
</Box>
|
||||
);
|
||||
};
|
1
src/apps/views/AppPermissionRequestView/index.ts
Normal file
1
src/apps/views/AppPermissionRequestView/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./AppPermissionRequestView";
|
54
src/apps/views/AppPermissionRequestView/messages.ts
Normal file
54
src/apps/views/AppPermissionRequestView/messages.ts
Normal file
|
@ -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',
|
||||
},
|
||||
});
|
|
@ -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");
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -26,6 +26,9 @@ export const AppView: React.FC<AppProps> = ({ 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<AppProps> = ({ id }) => {
|
|||
return <NotFoundPage onBack={() => 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;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export * from "./AppInstallView";
|
||||
export * from "./AppListView";
|
||||
export * from "./AppManageView";
|
||||
export * from "./AppPermissionRequestView";
|
||||
export * from "./AppView";
|
||||
|
|
Loading…
Reference in a new issue