Skip to content

Commit b94b345

Browse files
committed
story, official logo
1 parent 0a0f1d9 commit b94b345

10 files changed

Lines changed: 204 additions & 65 deletions

src/components/Sidebar/menu/utils.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ describe("mapToExtensionsItems", () => {
4141
mountName: "NAVIGATION_CATALOG",
4242
targetName: "APP_PAGE",
4343
settings: {},
44+
isSaleorOfficial: false,
4445
};
4546

4647
const mockHeader: SidebarMenuItem = {
@@ -214,6 +215,7 @@ describe("getMenuItemExtension", () => {
214215
mountName: "NAVIGATION_CATALOG",
215216
settings: {},
216217
targetName: "POPUP",
218+
isSaleorOfficial: false,
217219
};
218220

219221
const mockExtension: Extension = {

src/extensions/getExtensionsItems.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const mockedExtension: ExtensionWithParams = {
5252
open: jest.fn(),
5353
targetName: "POPUP",
5454
settings: {},
55+
isSaleorOfficial: false,
5556
};
5657

5758
describe("getExtensionsItems", () => {

src/extensions/hooks/useExtensions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "@dashboard/extensions/domain/app-extension-manifest-available-mounts";
77
import { appExtensionManifestOptionsSchemaWithDefault } from "@dashboard/extensions/domain/app-extension-manifest-options";
88
import { AppExtensionManifestTarget } from "@dashboard/extensions/domain/app-extension-manifest-target";
9+
import { isSaleorOfficialAppUrl } from "@dashboard/extensions/isSaleorOfficialAppUrl";
910
import { isUrlAbsolute } from "@dashboard/extensions/isUrlAbsolute";
1011
import { newTabActions } from "@dashboard/extensions/new-tab-actions";
1112
import { type ExtensionListQuery, useExtensionListQuery } from "@dashboard/graphql";
@@ -35,6 +36,8 @@ const prepareExtensionsWithActions = ({
3536
*/
3637
const newTabMethod = settingsValidation.data?.newTabTarget?.method ?? "GET";
3738

39+
const resolvedUrl = isUrlAbsolute(url) ? url : `${appUrl ?? ""}${url}`;
40+
3841
return {
3942
id,
4043
app,
@@ -45,6 +48,7 @@ const prepareExtensionsWithActions = ({
4548
mountName: ALL_APP_EXTENSION_MOUNTS.parse(mountName),
4649
targetName: AppExtensionManifestTarget.parse(targetName),
4750
settings,
51+
isSaleorOfficial: isSaleorOfficialAppUrl(resolvedUrl),
4852
/**
4953
* Only available for NEW_TAB, POPUP, APP_PAGE
5054
* TODO: Change interface to *not* contain this method if type is WIDGET
Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,90 @@
1-
import { isExternalAppUrl } from "./isExternalAppUrl";
1+
import { isSaleorOfficialAppUrl } from "./isSaleorOfficialAppUrl";
22

33
const setCloudDomain = (value: string | undefined): void => {
44
window.__SALEOR_CONFIG__ = { ...window.__SALEOR_CONFIG__, SALEOR_CLOUD_APP_DOMAIN: value };
55
};
66

7-
describe("isExternalAppUrl", () => {
7+
describe("isSaleorOfficialAppUrl", () => {
88
const originalConfig = window.__SALEOR_CONFIG__;
99

1010
afterEach(() => {
1111
window.__SALEOR_CONFIG__ = originalConfig;
1212
});
1313

14-
it("returns true when cloud domain env var is not set", () => {
14+
it("returns false when cloud domain env var is not set", () => {
1515
// Arrange
1616
setCloudDomain(undefined);
1717

1818
// Act
19-
const result = isExternalAppUrl("https://anything.example.com/widget");
19+
const result = isSaleorOfficialAppUrl("https://app.saleor.app/widget");
2020

2121
// Assert
22-
expect(result).toBe(true);
22+
expect(result).toBe(false);
2323
});
2424

25-
it("returns true when cloud domain env var is empty string", () => {
25+
it("returns false when cloud domain env var is empty string", () => {
2626
// Arrange
2727
setCloudDomain("");
2828

2929
// Act
30-
const result = isExternalAppUrl("https://app.saleor.app/widget");
30+
const result = isSaleorOfficialAppUrl("https://app.saleor.app/widget");
3131

3232
// Assert
33-
expect(result).toBe(true);
33+
expect(result).toBe(false);
3434
});
3535

36-
it("returns false for URL hosted exactly on the cloud domain", () => {
36+
it("returns true for URL hosted exactly on the cloud domain", () => {
3737
// Arrange
3838
setCloudDomain("saleor.app");
3939

4040
// Act
41-
const result = isExternalAppUrl("https://saleor.app/widget");
41+
const result = isSaleorOfficialAppUrl("https://saleor.app/widget");
4242

4343
// Assert
44-
expect(result).toBe(false);
44+
expect(result).toBe(true);
4545
});
4646

47-
it("returns false for URL hosted on a subdomain of the cloud domain", () => {
47+
it("returns true for URL hosted on a subdomain of the cloud domain", () => {
4848
// Arrange
4949
setCloudDomain("saleor.app");
5050

5151
// Act
52-
const result = isExternalAppUrl("https://my-app.saleor.app/widget");
52+
const result = isSaleorOfficialAppUrl("https://my-app.saleor.app/widget");
5353

5454
// Assert
55-
expect(result).toBe(false);
55+
expect(result).toBe(true);
5656
});
5757

58-
it("returns true for URL hosted outside the cloud domain", () => {
58+
it("returns false for URL hosted outside the cloud domain", () => {
5959
// Arrange
6060
setCloudDomain("saleor.app");
6161

6262
// Act
63-
const result = isExternalAppUrl("https://example.com/widget");
63+
const result = isSaleorOfficialAppUrl("https://example.com/widget");
6464

6565
// Assert
66-
expect(result).toBe(true);
66+
expect(result).toBe(false);
6767
});
6868

6969
it("does not match a domain that merely ends with the cloud domain string", () => {
7070
// Arrange
7171
setCloudDomain("saleor.app");
7272

7373
// Act - "evilsaleor.app" must not be treated as a subdomain of "saleor.app"
74-
const result = isExternalAppUrl("https://evilsaleor.app/widget");
74+
const result = isSaleorOfficialAppUrl("https://evilsaleor.app/widget");
7575

7676
// Assert
77-
expect(result).toBe(true);
77+
expect(result).toBe(false);
7878
});
7979

80-
it("returns true when URL cannot be parsed", () => {
80+
it("returns false when URL cannot be parsed", () => {
8181
// Arrange
8282
setCloudDomain("saleor.app");
8383

8484
// Act
85-
const result = isExternalAppUrl("not-a-url");
85+
const result = isSaleorOfficialAppUrl("not-a-url");
8686

8787
// Assert
88-
expect(result).toBe(true);
88+
expect(result).toBe(false);
8989
});
9090
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { getSaleorCloudAppDomain } from "@dashboard/config";
2+
3+
/**
4+
* Returns true when the URL is hosted under the configured Saleor Cloud app
5+
* domain (exact host or subdomain). When the domain is not configured or the
6+
* URL cannot be parsed, the URL cannot be verified as official and the function
7+
* returns false.
8+
*/
9+
export const isSaleorOfficialAppUrl = (url: string): boolean => {
10+
const cloudDomain = getSaleorCloudAppDomain();
11+
12+
if (cloudDomain === null) {
13+
return false;
14+
}
15+
16+
try {
17+
const { hostname } = new URL(url);
18+
19+
return hostname === cloudDomain || hostname.endsWith(`.${cloudDomain}`);
20+
} catch {
21+
return false;
22+
}
23+
};

src/extensions/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ export interface Extension {
8383
open: () => void;
8484
targetName: AppExtensionManifestTarget;
8585
settings: RelayToFlat<NonNullable<ExtensionListQuery["appExtensions"]>>[0]["settings"];
86+
/**
87+
* True when the extension's resolved URL is hosted under the configured Saleor
88+
* Cloud app domain (SALEOR_CLOUD_APP_DOMAIN). Resolved at the time the
89+
* extension is mapped from the GraphQL response.
90+
*/
91+
isSaleorOfficial: boolean;
8692
}
8793

8894
export interface ExtensionWithParams extends Omit<Extension, "open"> {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { type Extension } from "@dashboard/extensions/types";
2+
import type { Meta, StoryObj } from "@storybook/react-vite";
3+
4+
import { HomeWidgetTabs } from "./HomeWidgetTabs";
5+
6+
const buildExtension = (overrides: Partial<Extension>): Extension => ({
7+
id: "ext-1",
8+
app: {
9+
__typename: "App",
10+
id: "app-1",
11+
appUrl: "https://my-app.saleor.app",
12+
name: "Saleor App",
13+
brand: null,
14+
},
15+
accessToken: "token",
16+
permissions: [],
17+
label: "Widget",
18+
mountName: "HOMEPAGE_WIDGETS",
19+
url: "https://my-app.saleor.app/widget",
20+
open: () => undefined,
21+
targetName: "WIDGET",
22+
settings: null,
23+
isSaleorOfficial: true,
24+
...overrides,
25+
});
26+
27+
const officialExtension = buildExtension({
28+
id: "official",
29+
label: "Sales Insights",
30+
app: {
31+
__typename: "App",
32+
id: "saleor-app",
33+
appUrl: "https://insights.saleor.app",
34+
name: "Saleor Insights",
35+
brand: null,
36+
},
37+
url: "https://insights.saleor.app/widget",
38+
isSaleorOfficial: true,
39+
});
40+
41+
const externalExtension = buildExtension({
42+
id: "external",
43+
label: "Third-party widget",
44+
app: {
45+
__typename: "App",
46+
id: "external-app",
47+
appUrl: "https://third-party.example.com",
48+
name: "Acme Analytics",
49+
brand: null,
50+
},
51+
url: "https://third-party.example.com/widget",
52+
isSaleorOfficial: false,
53+
});
54+
55+
const meta: Meta<typeof HomeWidgetTabs> = {
56+
title: "Home/HomeWidgetTabs",
57+
component: HomeWidgetTabs,
58+
args: {
59+
activeExtensionId: officialExtension.id,
60+
extensions: [officialExtension, externalExtension],
61+
},
62+
};
63+
64+
export default meta;
65+
66+
type Story = StoryObj<typeof HomeWidgetTabs>;
67+
68+
export const Default: Story = {};
69+
70+
export const OnlyOfficialApps: Story = {
71+
args: {
72+
extensions: [
73+
officialExtension,
74+
buildExtension({
75+
id: "official-2",
76+
label: "Order Tools",
77+
app: {
78+
__typename: "App",
79+
id: "saleor-orders",
80+
appUrl: "https://orders.saleor.app",
81+
name: "Saleor Orders",
82+
brand: null,
83+
},
84+
url: "https://orders.saleor.app/widget",
85+
isSaleorOfficial: true,
86+
}),
87+
],
88+
},
89+
};
90+
91+
export const OnlyExternalApps: Story = {
92+
args: {
93+
activeExtensionId: externalExtension.id,
94+
extensions: [
95+
externalExtension,
96+
buildExtension({
97+
id: "external-2",
98+
label: "Marketing Hub",
99+
app: {
100+
__typename: "App",
101+
id: "marketing-app",
102+
appUrl: "https://marketing.example.com",
103+
name: "Marketing Co.",
104+
brand: null,
105+
},
106+
url: "https://marketing.example.com/widget",
107+
isSaleorOfficial: false,
108+
}),
109+
],
110+
},
111+
};
112+
113+
export const SingleTab: Story = {
114+
args: {
115+
extensions: [officialExtension],
116+
},
117+
};

src/home/HomeWidgetTabs.tsx

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { Tab, TabContainer } from "@dashboard/components/Tab";
2-
import { isUrlAbsolute } from "@dashboard/extensions/isUrlAbsolute";
32
import { type Extension } from "@dashboard/extensions/types";
3+
import { SaleorLogo } from "@dashboard/extensions/views/InstallCustomExtension/components/InstallSectionData/InstallExtensionManifestData/SaleorLogo";
44
import useNavigator from "@dashboard/hooks/useNavigator";
55
import { Box, Text } from "@saleor/macaw-ui-next";
6-
import { Blocks } from "lucide-react";
76

8-
import { isExternalAppUrl } from "./isExternalAppUrl";
97
import { homeWidgetUrl } from "./urls";
108

119
const HomeTab = Tab<string>("home-widget-tab");
@@ -15,36 +13,40 @@ interface HomeWidgetTabsProps {
1513
activeExtensionId: string | null;
1614
}
1715

18-
const resolveExtensionUrl = (extension: Extension): string =>
19-
isUrlAbsolute(extension.url) ? extension.url : `${extension.app.appUrl}${extension.url}`;
20-
2116
export const HomeWidgetTabs = ({ extensions, activeExtensionId }: HomeWidgetTabsProps) => {
2217
const navigate = useNavigator();
2318

2419
return (
2520
<TabContainer>
26-
{extensions.map(extension => {
27-
const isExternal = isExternalAppUrl(resolveExtensionUrl(extension));
28-
29-
return (
30-
<HomeTab
31-
key={extension.id}
32-
isActive={extension.id === activeExtensionId}
33-
changeTab={() => navigate(homeWidgetUrl(extension.id))}
34-
testId={`home-widget-tab-${extension.id}`}
35-
>
36-
<Box display="inline-flex" alignItems="center" gap={2}>
37-
<Box display="inline-flex" flexDirection="column">
38-
<span>{extension.label}</span>
39-
<Text size={1} color="default2">
40-
{extension.app.name}
41-
</Text>
21+
{extensions.map(extension => (
22+
<HomeTab
23+
key={extension.id}
24+
isActive={extension.id === activeExtensionId}
25+
changeTab={() => navigate(homeWidgetUrl(extension.id))}
26+
testId={`home-widget-tab-${extension.id}`}
27+
>
28+
<Box display="inline-flex" alignItems="center" gap={2}>
29+
{extension.isSaleorOfficial && (
30+
<Box
31+
__width={20}
32+
__height={20}
33+
display="inline-flex"
34+
alignItems="center"
35+
justifyContent="center"
36+
data-test-id={`saleor-app-badge-${extension.id}`}
37+
>
38+
<SaleorLogo />
4239
</Box>
43-
{isExternal && <Blocks />}
40+
)}
41+
<Box display="inline-flex" flexDirection="column">
42+
<span>{extension.label}</span>
43+
<Text size={1} color="default2">
44+
{extension.app.name}
45+
</Text>
4446
</Box>
45-
</HomeTab>
46-
);
47-
})}
47+
</Box>
48+
</HomeTab>
49+
))}
4850
</TabContainer>
4951
);
5052
};

0 commit comments

Comments
 (0)