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

Pwa install prompt for mobile users #1321

Merged
merged 12 commits into from
Nov 6, 2024
19 changes: 19 additions & 0 deletions app/components/icons/library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1549,3 +1549,22 @@ export const LockIcon = (props: SVGProps<SVGSVGElement>) => (
/>
</svg>
);

export const InstallIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width="20"
hunar1312 marked this conversation as resolved.
Show resolved Hide resolved
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M6.66667 10L10 13.3333M10 13.3333L13.3333 10M10 13.3333V6.66667M6.5 17.5H13.5C14.9001 17.5 15.6002 17.5 16.135 17.2275C16.6054 16.9878 16.9878 16.6054 17.2275 16.135C17.5 15.6002 17.5 14.9001 17.5 13.5V6.5C17.5 5.09987 17.5 4.3998 17.2275 3.86502C16.9878 3.39462 16.6054 3.01217 16.135 2.77248C15.6002 2.5 14.9001 2.5 13.5 2.5H6.5C5.09987 2.5 4.3998 2.5 3.86502 2.77248C3.39462 3.01217 3.01217 3.39462 2.77248 3.86502C2.5 4.3998 2.5 5.09987 2.5 6.5V13.5C2.5 14.9001 2.5 15.6002 2.77248 16.135C3.01217 16.6054 3.39462 16.9878 3.86502 17.2275C4.3998 17.5 5.09987 17.5 6.5 17.5Z"
stroke="#667085"
stroke-width="1.75"
hunar1312 marked this conversation as resolved.
Show resolved Hide resolved
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
49 changes: 49 additions & 0 deletions app/components/layout/install-pwa-prompt-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useLoaderData, useFetcher } from "@remix-run/react";
import { AnimatePresence } from "framer-motion";

import type { loader } from "~/routes/_layout+/_layout";
import { Button } from "../shared/button";

export default function InstallPwaPromptModal() {
hunar1312 marked this conversation as resolved.
Show resolved Hide resolved
const { hideInstallPwaPrompt } = useLoaderData<typeof loader>();
const fetcher = useFetcher();
let optimisticHideInstallPwaPrompt = hideInstallPwaPrompt;
if (fetcher.formData) {
optimisticHideInstallPwaPrompt =
fetcher.formData.get("pwaPromptVisibility") === "hidden";
}
return optimisticHideInstallPwaPrompt ? null : (
<AnimatePresence>
hunar1312 marked this conversation as resolved.
Show resolved Hide resolved
<div className="dialog-backdrop !items-end !bg-[#364054]/70">
<dialog
className="dialog m-auto h-auto w-[90%] pb-8 sm:w-[400px]"
open={true}
>
<div className="relative z-10 rounded-xl bg-white p-4 shadow-lg">
<video height="200" autoPlay loop muted className="mb-6 rounded-lg">
hunar1312 marked this conversation as resolved.
Show resolved Hide resolved
<source src="/static/videos/celebration.mp4" type="video/mp4" />
</video>
<div className="mb-8 text-center">
<h4 className="mb-1 text-[18px] font-semibold">
Install shelf for mobile
</h4>
<p className="text-gray-600">
Always available access to shelf, with all features you have on
desktop.
</p>
</div>
<fetcher.Form
method="post"
action="/api/user/prefs/hide-install-pwa-prompt-modal"
>
<input type="hidden" name="pwaPromptVisibility" value="hidden" />
<Button type="submit" width="full" variant="secondary">
Skip
hunar1312 marked this conversation as resolved.
Show resolved Hide resolved
</Button>
</fetcher.Form>
</div>
</dialog>
</div>
</AnimatePresence>
);
}
6 changes: 3 additions & 3 deletions app/components/layout/sidebar/menu-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useMainMenuItems } from "~/hooks/use-main-menu-items";
import type { loader } from "~/routes/_layout+/_layout";
import { tw } from "~/utils/tw";
import { toggleMobileNavAtom } from "./atoms";
import { ChatWithAnExpert } from "./chat-with-an-expert";
import { SidebarNoticeCard } from "./notice-card";

const MenuItems = ({ fetcher }: { fetcher: FetcherWithComponents<any> }) => {
const [, toggleMobileNav] = useAtom(toggleMobileNavAtom);
Expand Down Expand Up @@ -141,14 +141,14 @@ const MenuItems = ({ fetcher }: { fetcher: FetcherWithComponents<any> }) => {
</ul>

<div className="lower-menu">
{/* ChatWithAnExpert component will be visible when uncollapsed sidebar is selected and hidden when minimizing sidebar form is processing */}
{/* Sidebar notice card component will be visible when uncollapsed sidebar is selected and hidden when minimizing sidebar form is processing */}
{fetcher.state == "idle" ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
>
<ChatWithAnExpert />
<SidebarNoticeCard />
</motion.div>
) : null}
<ul className="menu mb-6">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@ import { XIcon } from "~/components/icons/library";
import { Button } from "~/components/shared/button";
import type { loader } from "~/routes/_layout+/_layout";

export const ChatWithAnExpert = () => {
const { hideSupportBanner } = useLoaderData<typeof loader>();
export const SidebarNoticeCard = () => {
const { hideNoticeCard } = useLoaderData<typeof loader>();
const fetcher = useFetcher();

let optimisticHideSupportBanner = hideSupportBanner;
let optimisticHideNoticeCard = hideNoticeCard;
if (fetcher.formData) {
optimisticHideSupportBanner =
fetcher.formData.get("bannerVisibility") === "hidden";
optimisticHideNoticeCard =
fetcher.formData.get("noticeCardVisibility") === "hidden";
}

return optimisticHideSupportBanner ? null : (
return optimisticHideNoticeCard ? null : (
<div className="support-banner mb-6 hidden rounded bg-gray-50 px-4 py-5 md:mt-10 md:block">
<div className="flex justify-between align-middle">
<h5 className="mb-1 font-semibold text-gray-900">
New: Order Asset Labels
Install Shelf for Mobile
</h5>
<div className="mt-[-6px]">
<fetcher.Form
method="post"
action="/api/user/prefs/dismiss-support-banner"
action="/api/user/prefs/dismiss-notice-card"
>
<input type="hidden" name="bannerVisibility" value="hidden" />
<input type="hidden" name="noticeCardVisibility" value="hidden" />
<button type="submit">
<XIcon />
</button>
Expand All @@ -33,9 +33,7 @@ export const ChatWithAnExpert = () => {
</div>

<p className="text-gray-600">
We are happy to announce that we have the infrastructure to produce
custom branded labels for your business. Affordable rates, fast
turnaround, global shipping, various materials.
Always available access to shelf, with all features you have on desktop.
</p>
<img
src="/static/images/carlos-support.jpg"
Expand All @@ -45,9 +43,9 @@ export const ChatWithAnExpert = () => {
<p>
<Button
variant="link"
to="https://www.shelf.nu/blog/introducing-shelfs-sticker-studio"
to="https://www.shelf.nu/blog/new-shelf-pwa-progresive-web-application-live-learn-how-to-use"
>
View offer
Written Tutorial
</Button>
</p>
</div>
Expand Down
5 changes: 4 additions & 1 deletion app/components/shared/icons-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
ActiveSwitchIcon,
MapIcon,
ToolIcon,
InstallIcon,
} from "../icons/library";

/** The possible options for icons to be rendered in the button */
Expand Down Expand Up @@ -93,7 +94,8 @@ export type IconType =
| "lock"
| "activate"
| "deactivate"
| "tool";
| "tool"
| "install";

type IconsMap = {
[key in IconType]: JSX.Element;
Expand Down Expand Up @@ -146,6 +148,7 @@ export const iconsMap: IconsMap = {
activate: <ActiveSwitchIcon />,
deactivate: <XIcon />,
tool: <ToolIcon />,
install: <InstallIcon />,
};

export default iconsMap;
13 changes: 11 additions & 2 deletions app/routes/_layout+/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Outlet, useLoaderData } from "@remix-run/react";
import { useAtom } from "jotai";
import { ClientOnly } from "remix-utils/client-only";
import { switchingWorkspaceAtom } from "~/atoms/switching-workspace";
import { ErrorContent } from "~/components/errors";

import InstallPwaPromptModal from "~/components/layout/install-pwa-prompt-modal";
import Sidebar from "~/components/layout/sidebar/sidebar";
import { useCrisp } from "~/components/marketing/crisp";
import { Spinner } from "~/components/shared/spinner";
Expand Down Expand Up @@ -96,8 +98,9 @@ export async function loader({ context, request }: LoaderFunctionArgs) {
)?.roles,
subscription,
enablePremium: config.enablePremiumFeatures,
hideSupportBanner: cookie.hideSupportBanner,
hideNoticeCard: cookie.hideNoticeCard,
minimizedSidebar: cookie.minimizedSidebar,
hideInstallPwaPrompt: cookie.hideInstallPwaPrompt,
isAdmin,
canUseBookings: canUseBookings(currentOrganization),
/** THis is used to disable team organizations when the currentOrg is Team and no subscription is present */
Expand All @@ -124,7 +127,6 @@ export default function App() {
const { currentOrganizationId, disabledTeamOrg } =
useLoaderData<typeof loader>();
const [workspaceSwitching] = useAtom(switchingWorkspaceAtom);

return (
<>
<div
Expand All @@ -148,6 +150,13 @@ export default function App() {
)}
</div>
<Toaster />
<ClientOnly fallback={null}>
{() =>
window.matchMedia("(max-width: 640px)").matches ? (
<InstallPwaPromptModal />
) : null
}
</ClientOnly>
</main>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export async function action({ context, request }: ActionFunctionArgs) {
const cookie = (await userPrefs.parse(cookieHeader)) || {};
const bodyParams = await request.formData();

if (bodyParams.get("bannerVisibility") === "hidden") {
cookie.hideSupportBanner = true;
if (bodyParams.get("noticeCardVisibility") === "hidden") {
cookie.hideNoticeCard = true;
}

return json(data({ success: true }), {
Expand Down
26 changes: 26 additions & 0 deletions app/routes/api+/user.prefs.hide-install-pwa-prompt-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type ActionFunctionArgs, json } from "@remix-run/node";
import { setCookie, userPrefs } from "~/utils/cookies.server";
import { makeShelfError } from "~/utils/error";
import { data, error } from "~/utils/http.server";

export async function action({ context, request }: ActionFunctionArgs) {
const authSession = context.getSession();
const { userId } = authSession;

try {
const cookieHeader = request.headers.get("Cookie");
const cookie = (await userPrefs.parse(cookieHeader)) || {};
const bodyParams = await request.formData();

if (bodyParams.get("pwaPromptVisibility") === "hidden") {
cookie.hideInstallPwaPrompt = true;
}

return json(data({ success: true }), {
headers: [setCookie(await userPrefs.serialize(cookie))],
});
} catch (cause) {
const reason = makeShelfError(cause, { userId });
return json(error(reason), { status: reason.status });
}
}
Loading