Skip to content

Commit

Permalink
feat: impl createBrowserRouter, add routes dict
Browse files Browse the repository at this point in the history
  • Loading branch information
trevor-anderson committed Feb 20, 2024
1 parent f40ea79 commit 8cfc1bd
Show file tree
Hide file tree
Showing 6 changed files with 622 additions and 0 deletions.
159 changes: 159 additions & 0 deletions src/routes/RootAppRouter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { createBrowserRouter, redirect, RouterProvider } from "react-router-dom";
import { toast } from "react-toastify";
import * as Sentry from "@sentry/react";
import { ErrorBoundary } from "@/components/ErrorBoundary";
import { RootAppLayout } from "@/layouts/RootAppLayout";
import { checkoutValuesStore } from "@/stores/checkoutValuesStore";
import { APP_PATHS, APP_PATH_COMPONENTS } from "./appPaths";
import { getProtectedRouteLoader } from "./getProtectedRouteLoader";

const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouter(createBrowserRouter);

const rootAppBrowserRouter = sentryCreateBrowserRouter(
[
{
path: APP_PATHS.ROOT,
element: <RootAppLayout />,
errorElement: <ErrorBoundary />,
children: [
{
index: true,
lazy: () => import(/* webpackChunkName: "LandingPage" */ "@/pages/LandingPage"),
},
{
path: APP_PATHS.REGISTER,
lazy: () => import(/* webpackChunkName: "RegisterPage" */ "@/pages/RegisterPage"),
},
{
path: APP_PATHS.LOGIN,
lazy: () => import(/* webpackChunkName: "LoginPage" */ "@/pages/LoginPage"),
},
{
path: APP_PATHS.ToS,
lazy: () =>
import(/* webpackChunkName: "TermsOfServicePage" */ "@/pages/TermsOfServicePage"),
},
{
path: APP_PATHS.PRIVACY,
lazy: () =>
import(/* webpackChunkName: "PrivacyPolicyPage" */ "@/pages/PrivacyPolicyPage"),
},
{
path: APP_PATHS.PRODUCTS,
lazy: () => import(/* webpackChunkName: "ProductsPage" */ "@/pages/ProductsPage"),
},
{
path: APP_PATHS.CHECKOUT,
lazy: () => import(/* webpackChunkName: "CheckoutPage" */ "@/pages/CheckoutPage"),
loader: getProtectedRouteLoader(
{ authenticationRequired: true, paymentRequired: false },
() => {
// Custom route requirement: user must have selected a subscription
if (!checkoutValuesStore.get().selectedSubscription) {
toast.info("Please select a subscription.", { toastId: "select-a-sub" });
throw redirect(APP_PATHS.PRODUCTS);
}
}
),
},
{
path: APP_PATH_COMPONENTS.HOME,
lazy: () => import(/* webpackChunkName: "HomePageLayout" */ "@/layouts/HomePageLayout"),
loader: getProtectedRouteLoader({ authenticationRequired: true, paymentRequired: true }),
children: [
{
lazy: () =>
import(
/* webpackChunkName: "StripeConnectOnboardingStateLayer" */ "./StripeConnectOnboardingStateLayer"
),
children: [
{
index: true,
lazy: () => import(/* webpackChunkName: "Dashboard" */ "@/pages/Dashboard"),
},
{
path: APP_PATH_COMPONENTS.WORK_ORDERS,
children: [
{
index: true,
lazy: () =>
import(
/* webpackChunkName: "WorkOrdersListView" */ "@/pages/WorkOrdersListView"
),
},
{
path: APP_PATH_COMPONENTS.FORM_VIEW,
lazy: () =>
import(
/* webpackChunkName: "WorkOrderFormView" */ "@/pages/WorkOrderFormView"
),
},
{
path: APP_PATH_COMPONENTS.ITEM_VIEW,
lazy: () =>
import(
/* webpackChunkName: "WorkOrderItemView" */ "@/pages/WorkOrderItemView"
),
},
],
},
{
path: APP_PATH_COMPONENTS.INVOICES,
children: [
{
index: true,
lazy: () =>
import(
/* webpackChunkName: "InvoicesListView" */ "@/pages/InvoicesListView"
),
},
{
path: APP_PATH_COMPONENTS.FORM_VIEW,
lazy: () =>
import(/* webpackChunkName: "InvoiceFormView" */ "@/pages/InvoiceFormView"),
},
{
path: APP_PATH_COMPONENTS.ITEM_VIEW,
lazy: () =>
import(/* webpackChunkName: "InvoiceItemView" */ "@/pages/InvoiceItemView"),
},
],
},
{
path: APP_PATH_COMPONENTS.CONTACTS,
children: [
{
index: true,
lazy: () =>
import(
/* webpackChunkName: "ContactsListView" */ "@/pages/ContactsListView"
),
},
{
path: APP_PATH_COMPONENTS.ITEM_VIEW,
lazy: () =>
import(/* webpackChunkName: "ContactItemView" */ "@/pages/ContactItemView"),
},
],
},
{
path: APP_PATH_COMPONENTS.PROFILE,
lazy: () => import(/* webpackChunkName: "ProfilePage" */ "@/pages/ProfilePage"),
},
],
},
],
},
{
path: "*",
lazy: () => import(/* webpackChunkName: "PageNotFound" */ "@/pages/PageNotFound"),
},
],
},
],
{
basename: APP_PATHS.ROOT,
}
);

export const RootAppRouter = () => <RouterProvider router={rootAppBrowserRouter} />;
115 changes: 115 additions & 0 deletions src/routes/StripeConnectOnboardingStateLayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useState, useEffect } from "react";
import { useSearchParams, Outlet } from "react-router-dom";
import Box from "@mui/material/Box";
import Text from "@mui/material/Typography";
import AnnouncementIcon from "@mui/icons-material/Announcement";
import { Dialog } from "@/components/Dialog";
import { stripeService } from "@/services/stripeService";
import { isConnectOnboardingCompleteStore } from "@/stores";

/**
* This component is responsible for managing the state of the user's Stripe
* Connect onboarding process.
*
* During authentication, `isConnectOnboardingCompleteStore` will be initialized
* using the `user.stripeConnectAccount.detailsSubmitted` property.
*
* If `isConnectOnboardingComplete` is false upon successful authentication, the
* user is presented with an alert dialog notifying them of the need to complete
* the Connect onboarding process.
*
* Once the Connect onboarding flow has begun, there are two possible actions
* that need to be handled:
*
* 1. RETURN: Upon completion of Stripe Connect onboarding process, the user
* will be redirected to the Fixit route from which they originally began
* the onboarding flow, along with a URL search param of "?connect-return"
* (e.g., "/home/workorders?connect-return"), at which time the value of
* isConnectOnboardingComplete can be flipped to `true`.
*
* 2. REFRESH: If the user fails to complete the onboarding flow within the
* window of time allotted to the temporary portal, or if they try to
* refresh the portal page, they'll be redirected to the Fixit route from
* which they originally began the onboarding flow, along with a URL search
* param of "?connect-refresh" (e.g., "/home/workorders?connect-refresh").
* The user can complete the onboarding flow at any time in the future.
*/
export const StripeConnectOnboardingStateLayer = () => {
const isConnectOnboardingComplete = isConnectOnboardingCompleteStore.useSubToStore();
const [searchParams, setSearchParams] = useSearchParams();

useEffect(() => {
(async () => {
// Do nothing if isConnectOnboardingComplete is true
if (!isConnectOnboardingComplete) {
if (searchParams.has(STRIPE_CONNECT_URL_PARAMS.REFRESH)) {
// Rm the query param and obtain new Stripe Connect onboarding portal link
searchParams.delete(STRIPE_CONNECT_URL_PARAMS.REFRESH);
// Update searchParams with 'REFRESH' query param removed
setSearchParams(searchParams);
// Get the onboarding link (run in bg without loading/error indicators)
const { stripeLink } = await stripeService.getConnectOnboardingLink();
if (stripeLink) window.open(stripeLink);
} else if (searchParams.has(STRIPE_CONNECT_URL_PARAMS.RETURN)) {
// Rm the query param and set isConnectOnboardingComplete to true
searchParams.delete(STRIPE_CONNECT_URL_PARAMS.RETURN);
// Update searchParams with 'RETURN' query param removed
setSearchParams(searchParams);
isConnectOnboardingCompleteStore.set(true);
}
}
})();
}, [isConnectOnboardingComplete, searchParams, setSearchParams]);

// Local/internal Dialog state vars (init isDialogVisible set to !isConnectOnboardingComplete)
const [hasAlertedUser, setHasAlertedUser] = useState(false);
const { isDialogVisible, closeDialog } = Dialog.use(!isConnectOnboardingComplete);

const handleDialogAccept = async () => {
closeDialog();
setHasAlertedUser(true);
const { stripeLink } = await stripeService.getConnectOnboardingLink();
if (stripeLink) window.open(stripeLink);
};

const handleDialogCancel = () => {
closeDialog();
setHasAlertedUser(true);
};

return (
<>
<Outlet />
{!isConnectOnboardingComplete && !hasAlertedUser && (
<Dialog
isVisible={isDialogVisible}
title="Start Getting Paid Today!"
handleAccept={handleDialogAccept}
handleCancel={handleDialogCancel}
acceptLabel="Complete Setup"
cancelLabel="I'll do this later"
>
<Text>
Complete your account setup to start sending and receiving payments. If you'd like to do
this later - no problem! You can always manage your payment settings in the account
menu.
</Text>
<Box style={{ display: "flex" }}>
<AnnouncementIcon style={{ marginRight: "0.5rem" }} />
<Text>
You won't be able to send or receive invoices until your account setup is complete.
</Text>
</Box>
</Dialog>
)}
</>
);
};

// Exported as "Component" for react-router-dom lazy loading
export const Component = StripeConnectOnboardingStateLayer;

export const STRIPE_CONNECT_URL_PARAMS = {
RETURN: "connect-return",
REFRESH: "connect-refresh",
} as const;
Loading

0 comments on commit 8cfc1bd

Please sign in to comment.