Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions frontends/mit-learn/src/common/feature_flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Feature flags for the app. These should correspond to the flag that's set up
// in PostHog.

export enum FeatureFlags {
EnableEcommerce = "enable-ecommerce",
}
2 changes: 2 additions & 0 deletions frontends/mit-learn/src/common/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ export const SEARCH_PROGRAM = querifiedSearchUrl({
export const SEARCH_LEARNING_MATERIAL = querifiedSearchUrl({
resource_category: "learning_material",
})

export const ECOMMERCE_CART = "/cart/" as const
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react"
import { useFeatureFlagEnabled } from "posthog-js/react"
import { ForbiddenError } from "@/common/permissions"
import { FeatureFlags } from "@/common/feature_flags"

type EcommerceFeatureProps = {
children: React.ReactNode
}

/**
* Simple wrapper to standardize the feature flag check for ecommerce UI pages.
* If the flag is enabled, display the children; if not, throw a ForbiddenError
* like you'd get for an unauthenticated route.
*
* There's a PostHogFeature component that is provided but went this route
* because it seemed to be inconsistent - sometimes having the flag enabled
* resulted in it tossing to the error page.
*
* Set the feature flag here using the enum, and then make sure it's also
* defined in commmon/feature_flags too.
*/

const EcommerceFeature: React.FC<EcommerceFeatureProps> = ({ children }) => {
const ecommFlag = useFeatureFlagEnabled(FeatureFlags.EnableEcommerce)

if (ecommFlag === false) {
throw new ForbiddenError("Not enabled.")
}
Comment on lines +26 to +28
Copy link
Contributor

@ChristopherChudzicki ChristopherChudzicki Oct 4, 2024

Choose a reason for hiding this comment

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

Post-merge comment

I noticed some odd behavior with PostHog and feature flags that I thought was worth mentioning. Essentially, I think there's no reliable way—with useFeatureFlagEnabled—to determine whether a flag is "off" or "not yet loaded". (Note: the useFeatureFlagEnabled is really simple. Here's its source.)

useFeatureFlagEnabled returns boolean | undefined. Its behavior is:

  1. useFeatureFlagEnabled("flag-A") returns undefined before any flags have loaded
  2. When any flags have loaded, useFeatureFlagEnabled("flag-A") returns true if the flag is on, and false if the flag is off or has not loaded yet.
    • "When any flags have loaded" includes:
      • bootstrapped flags
      • flags determined via API call to posthog site

So the following is possible:

Screenshot 2024-10-04 at 12 30 41 PM

In this scenario, we were attempting to:

  1. If flags not loaded yet, return null
  2. If flags loaded, either
    • redirect
    • or render the cart page

But this doesn't seem to work quite right if any flags are bootstrapped, as shown in the sreenshot.


return ecommFlag ? children : null
}

export default EcommerceFeature
65 changes: 65 additions & 0 deletions frontends/mit-learn/src/pages/EcommercePages/CartPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { renderTestApp, waitFor, setMockResponse } from "../../test-utils"
import { urls } from "api/test-utils"
import * as commonUrls from "@/common/urls"
import { Permissions } from "@/common/permissions"
import { login } from "@/common/urls"
import { useFeatureFlagEnabled } from "posthog-js/react"

jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest.mocked(useFeatureFlagEnabled)

const oldWindowLocation = window.location

beforeAll(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (window as any).location

window.location = Object.defineProperties({} as Location, {
...Object.getOwnPropertyDescriptors(oldWindowLocation),
assign: {
configurable: true,
value: jest.fn(),
},
})
})

afterAll(() => {
window.location = oldWindowLocation
})

describe("CartPage", () => {
;["on", "off"].forEach((testCase: string) => {
test(`Renders when logged in and feature flag is ${testCase}`, async () => {
setMockResponse.get(urls.userMe.get(), {
[Permissions.Authenticated]: true,
})
mockedUseFeatureFlagEnabled.mockReturnValue(testCase === "on")

renderTestApp({
url: commonUrls.ECOMMERCE_CART,
})
await waitFor(() => {
testCase === "on"
? expect(document.title).toBe("Shopping Cart | MIT Learn")
: expect(document.title).not.toBe("Shopping Cart | MIT Learn")
})
})
})

test("Sends to login page when logged out", async () => {
setMockResponse.get(urls.userMe.get(), {
[Permissions.Authenticated]: false,
})
const expectedUrl = login({
pathname: "/cart/",
})

renderTestApp({
url: commonUrls.ECOMMERCE_CART,
})

await waitFor(() => {
expect(window.location.assign).toHaveBeenCalledWith(expectedUrl)
})
})
})
31 changes: 31 additions & 0 deletions frontends/mit-learn/src/pages/EcommercePages/CartPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react"
import { Breadcrumbs, Container, Typography } from "ol-components"
import EcommerceFeature from "@/page-components/EcommerceFeature/EcommerceFeature"
import MetaTags from "@/page-components/MetaTags/MetaTags"
import * as urls from "@/common/urls"

const CartPage: React.FC = () => {
return (
<EcommerceFeature>
<Container>
<MetaTags title="Shopping Cart" />
<Breadcrumbs
variant="light"
ancestors={[{ href: urls.HOME, label: "Home" }]}
current="Shopping Cart"
/>

<Typography component="h1" variant="h3">
Shopping Cart
</Typography>

<Typography>
The shopping cart layout should go here, if you're allowed to see
this.
</Typography>
</Container>
</EcommerceFeature>
)
}

export default CartPage
9 changes: 9 additions & 0 deletions frontends/mit-learn/src/pages/EcommercePages/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Unified Ecommerce in MIT Learn

The front end for the Unified Ecommerce system lives in MIT Learn. So, pages that exist here are designed to talk to Unified Ecommerce rather than to the Learn system.

There's a few functional pieces here:

- **Cart** - Displays the user's cart, and provides some additional functionality for that (item management, discount application, etc.)
- **Receipts** - Allows the user to display their order history and view receipts from their purchases, including historical ones from other systems.
- **Financial Assistance** - For learning resources that support it, the learner side of the financial assistance request system lives here. (Approvals do not.)
11 changes: 11 additions & 0 deletions frontends/mit-learn/src/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react"
import { RouteObject, Outlet } from "react-router"
import { ScrollRestoration } from "react-router-dom"

import HomePage from "@/pages/HomePage/HomePage"
import RestrictedRoute from "@/components/RestrictedRoute/RestrictedRoute"
import LearningPathListingPage from "@/pages/LearningPathListingPage/LearningPathListingPage"
Expand All @@ -26,6 +27,7 @@ import DepartmentListingPage from "./pages/DepartmentListingPage/DepartmentListi
import TopicsListingPage from "./pages/TopicListingPage/TopicsListingPage"
import UnitsListingPage from "./pages/UnitsListingPage/UnitsListingPage"
import OnboardingPage from "./pages/OnboardingPage/OnboardingPage"
import CartPage from "./pages/EcommercePages/CartPage"

import { styled } from "ol-components"

Expand Down Expand Up @@ -190,6 +192,15 @@ const routes: RouteObject[] = [
},
],
},
{
element: <RestrictedRoute requires={Permissions.Authenticated} />,
children: [
{
path: urls.ECOMMERCE_CART,
element: <CartPage />,
},
],
},
],
},
]
Expand Down
Loading