Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Service worker implementation #5168

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-snails-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

Now you can see when the new version was released or you lost connection.
12 changes: 12 additions & 0 deletions locale/defaultMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -6230,6 +6230,10 @@
"cLcy6F": {
"string": "Number of products"
},
"cLnkZn": {
"context": "You are currently offline. Some features may not be available.",
"string": "You are currently offline. Some features may not be available."
},
"cMFlOp": {
"context": "input label",
"string": "New Password"
Expand Down Expand Up @@ -8568,6 +8572,10 @@
"context": "header",
"string": "Unnamed Webhook Details"
},
"sngdVO": {
"context": "New dashboard version available.",
"string": "New dashboard version available."
},
"stjHjY": {
"context": "error message",
"string": "Promo code already exists"
Expand All @@ -8587,6 +8595,10 @@
"context": "ShippingZoneSettingsCard title",
"string": "Settings"
},
"t1Qs2z": {
"context": "Click here to reload.",
"string": "Click here to reload."
},
"t1UYU6": {
"context": "install app privacy",
"string": "Uninstalling the app will remove all your customer’s personal data stored by {name}."
Expand Down
4,240 changes: 3,668 additions & 572 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@
"typescript": "^5.0.4",
"typescript-strict-plugin": "^2.1.0",
"vite": "^3.2.7",
"vite-plugin-html": "^3.2.0"
"vite-plugin-html": "^3.2.0",
"vite-plugin-pwa": "^0.20.5"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.3.40",
Expand Down Expand Up @@ -248,6 +249,7 @@
"node"
],
"moduleNameMapper": {
"virtual:pwa-register/react": "identity-obj-proxy",
"\\.(css)$": "identity-obj-proxy",
"@assets(.*)$": "<rootDir>/assets/$1",
"@locale(.*)$": "<rootDir>/locale/$1",
Expand Down
11 changes: 9 additions & 2 deletions src/components/AppLayout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React from "react";
import { DevModePanel } from "../DevModePanel/DevModePanel";
import NavigatorSearch from "../NavigatorSearch";
import { useSavebarRef } from "../Savebar/SavebarRefContext";
import { ServiceWorkerPrompt } from "../ServiceWorkerPrompt";
import { Sidebar } from "../Sidebar";
import { useStyles } from "./styles";

Expand All @@ -24,10 +25,16 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
<DevModePanel />
<NavigatorSearch />

<Box display="grid" __gridTemplateColumns="auto 1fr">
<Box
display="grid"
height="100vh"
__gridTemplateColumns="auto 1fr"
__gridTemplateRows="auto 1fr"
>
{appState.loading && <LinearProgress className={classes.appLoader} color="primary" />}
<ServiceWorkerPrompt />
<Box
height="100vh"
height="100%"
borderColor="default1"
borderRightWidth={1}
backgroundColor="default2"
Expand Down
45 changes: 45 additions & 0 deletions src/components/ServiceWorkerPrompt/Animated.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";

import { Animated } from "./Animated";

describe("Animated Component", () => {
test("renders children when show is true", () => {
// Arrange & Act
render(<Animated show={true}>Test Child</Animated>);

// Assert
expect(screen.getByText("Test Child")).toBeInTheDocument();
});

test("does not render children when show is false", () => {
// Arrange & Act
const { rerender } = render(<Animated show={false}>Test Child</Animated>);

// Assert
expect(screen.queryByText("Test Child")).not.toBeInTheDocument();

// Arrange & Act
rerender(<Animated show={true}>Test Child</Animated>);

// Assert
expect(screen.getByText("Test Child")).toBeInTheDocument();
});

test("handles transition end correctly", () => {
// Arrange & Act
const { rerender } = render(<Animated show={false}>Test Child</Animated>);

rerender(<Animated show={true}>Test Child</Animated>);

// Assert
expect(screen.getByText("Test Child")).toBeInTheDocument();

// Arrange & Act
rerender(<Animated show={false}>Test Child</Animated>);
fireEvent.transitionEnd(screen.getByText("Test Child"));

// Assert
expect(screen.queryByText("Test Child")).not.toBeInTheDocument();
});
});
38 changes: 38 additions & 0 deletions src/components/ServiceWorkerPrompt/Animated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Box } from "@saleor/macaw-ui-next";
import React, { useEffect, useState } from "react";

interface FadeProps {
children: React.ReactNode;
show: boolean;
}

export const Animated = ({ children, show }: FadeProps) => {
const [shouldRender, setShouldRender] = useState(show);
const [isVisible, setIsVisible] = useState(show);

useEffect(() => {
if (show) {
setShouldRender(true);
setTimeout(() => setIsVisible(true), 10);
} else {
setIsVisible(false);
}
}, [show]);

const onTransitionEnd = () => {
if (!show) {
setShouldRender(false);
}
};

return shouldRender ? (
<Box
__transition="opacity 0.3s ease-in-out, margin-top 0.3s ease-in-out"
__opacity={isVisible ? 1 : 0}
__marginTop={isVisible ? 0 : -24}
onTransitionEnd={onTransitionEnd}
>
{children}
</Box>
) : null;
};
10 changes: 10 additions & 0 deletions src/components/ServiceWorkerPrompt/Container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Box } from "@saleor/macaw-ui-next";
import React from "react";

interface ContainerProps {
children: React.ReactNode;
}

export const Container = ({ children }: ContainerProps) => {
return <Box __gridColumn="1 / span 2">{children}</Box>;
};
39 changes: 39 additions & 0 deletions src/components/ServiceWorkerPrompt/NewVersionAvailable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Box, Button, Text } from "@saleor/macaw-ui-next";
import React from "react";
import { FormattedMessage } from "react-intl";

export const NewVersionAvailable = ({ onUpdate }: { onUpdate: () => void }) => {
return (
<Box
backgroundColor="warning1"
paddingX={3}
height={6}
display="flex"
justifyContent="center"
alignItems="center"
>
<Text size={1} fontWeight="medium">
<FormattedMessage
id="sngdVO"
defaultMessage="New dashboard version available."
description="New dashboard version available."
/>
</Text>
<Button
onClick={onUpdate}
size="small"
variant="tertiary"
color="accent1"
textDecoration="underline"
__paddingRight="3px"
__paddingLeft="3px"
>
<FormattedMessage
id="t1Qs2z"
defaultMessage="Click here to reload."
description="Click here to reload."
/>
</Button>
</Box>
);
};
57 changes: 57 additions & 0 deletions src/components/ServiceWorkerPrompt/ServiceWorkerPrompt.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import { useRegisterSW } from "virtual:pwa-register/react";

import { ServiceWorkerPrompt } from "./index";
import { useNavigatorOnline } from "./useNavigatorOnline";

jest.mock("virtual:pwa-register/react");
jest.mock("./useNavigatorOnline");
jest.mock("react-intl", () => ({
FormattedMessage: ({ defaultMessage }: { defaultMessage: string }) => <>{defaultMessage}</>,
}));

describe("ServiceWorkerPrompt", () => {
const mockUpdateServiceWorker = jest.fn();
const mockNeedRefresh = [true];

beforeEach(() => {
(useRegisterSW as jest.Mock).mockReturnValue({
needRefresh: mockNeedRefresh,
updateServiceWorker: mockUpdateServiceWorker,
});
(useNavigatorOnline as jest.Mock).mockReturnValue(true);
});

it("renders YouAreOffline when offline", () => {
// Arrange
(useNavigatorOnline as jest.Mock).mockReturnValue(false);

// Act
render(<ServiceWorkerPrompt />);

// Assert
expect(screen.getByText(/You are currently offline/i)).toBeInTheDocument();
});

it("renders NewVersionAvailable when a new version is available", () => {
// Arrange & Act
render(<ServiceWorkerPrompt />);

// Assert
expect(screen.getByText(/New dashboard version available/i)).toBeInTheDocument();
});

it("calls updateServiceWorker and reloads the page on update", async () => {
// Arrange
render(<ServiceWorkerPrompt />);

const updateButton = screen.getByText(/Click here to reload/i);

// Act
fireEvent.click(updateButton);

// Assert
expect(mockUpdateServiceWorker).toHaveBeenCalled();
});
});
26 changes: 26 additions & 0 deletions src/components/ServiceWorkerPrompt/YouAreOffline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { WifiOff } from "@dashboard/icons/WifiOff";
import { Box, Text } from "@saleor/macaw-ui-next";
import React from "react";
import { FormattedMessage } from "react-intl";

export const YouAreOffline = () => {
return (
<Box
backgroundColor="info1"
paddingX={3}
height={6}
display="flex"
justifyContent="center"
alignItems="center"
>
<WifiOff width={3.5} />
<Text marginLeft={2} size={1} fontWeight="medium">
<FormattedMessage
id="cLnkZn"
defaultMessage="You are currently offline. Some features may not be available."
description="You are currently offline. Some features may not be available."
/>
</Text>
</Box>
);
};
67 changes: 67 additions & 0 deletions src/components/ServiceWorkerPrompt/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from "react";
import { useRegisterSW } from "virtual:pwa-register/react";

import { Animated } from "./Animated";
import { Container } from "./Container";
import { NewVersionAvailable } from "./NewVersionAvailable";
import { useNavigatorOnline } from "./useNavigatorOnline";
import { YouAreOffline } from "./YouAreOffline";

const SW_PING_INTERVAL = 5000;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temporary, testing purposes


const ping = async (swScriptUrl: string, registration: ServiceWorkerRegistration) => {
const resp = await fetch(swScriptUrl, {
cache: "no-store",
headers: {
cache: "no-store",
"cache-control": "no-cache",
},
});

if (resp?.status === 200) {
await registration.update();
}
};

const createPingHandler =
(swScriptUrl: string, registration: ServiceWorkerRegistration) => async () => {
if (registration.installing || !navigator) return;

if ("connection" in navigator && !navigator.onLine) return;

await ping(swScriptUrl, registration);
};

export const ServiceWorkerPrompt = () => {
const onRegisteredSW = async (
swScriptUrl: string,
registration: ServiceWorkerRegistration | undefined,
) => {
if (!registration) return;

setInterval(createPingHandler(swScriptUrl, registration), SW_PING_INTERVAL);
};

const onUpdate = async () => {
await updateServiceWorker();
window.location.reload();
};

const {
needRefresh: [needRefresh],
updateServiceWorker,
} = useRegisterSW({ onRegisteredSW });

const isOnline = useNavigatorOnline(navigator);

return (
<Container>
<Animated show={!isOnline}>
<YouAreOffline />
</Animated>
<Animated show={needRefresh && isOnline}>
<NewVersionAvailable onUpdate={onUpdate} />
</Animated>
</Container>
);
};
21 changes: 21 additions & 0 deletions src/components/ServiceWorkerPrompt/useNavigatorOnline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { renderHook } from "@testing-library/react-hooks";

import { useNavigatorOnline } from "./useNavigatorOnline";

describe("useNavigatorOnline", () => {
it("should return true when online", () => {
const navigator = { onLine: true } as Navigator;

const { result } = renderHook(() => useNavigatorOnline(navigator));

expect(result.current).toBe(true);
});

it("should return false when offline", () => {
const navigator = { onLine: false } as Navigator;

const { result } = renderHook(() => useNavigatorOnline(navigator));

expect(result.current).toBe(false);
});
});
Loading
Loading