Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
34 changes: 1 addition & 33 deletions src/components/PurchaseSummary/PurchaseSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Card, Stack } from '@openedx/paragon';
import React, { useEffect, useRef, useState } from 'react';
import React from 'react';

import { usePurchaseSummaryPricing } from '@/components/app/data';
import useTestimonials from '@/components/app/data/hooks/useTestimonials';
import { DataStoreKey } from '@/constants/checkout';
import { useCheckoutFormStore } from '@/hooks/index';

Expand All @@ -12,7 +11,6 @@ import LicensesRow from './LicensesRow';
import PricePerUserRow from './PricePerUserRow';
import PurchaseSummaryCardButton from './PurchaseSummaryCardButton';
import PurchaseSummaryHeader from './PurchaseSummaryHeader';
import TestimonialCard, { Testimonial } from './TestimonialCard';
import TotalAfterTrialRow from './TotalAfterTrialRow';

const PurchaseSummary = () => {
Expand All @@ -31,32 +29,6 @@ const PurchaseSummary = () => {

const normalizedQuantity = parseInt(quantity, 10) === 0 ? null : quantity;

// ✅ Move testimonials API call to client hook
const { data: testimonials = [] } = useTestimonials();
const [currentTestimonial, setCurrentTestimonial] = useState<Testimonial | null>(null);
const shownTestimonialsRef = useRef<string[]>([]);

useEffect(() => {
if (!testimonials.length) { return; }

let available = testimonials.filter(
(t) => t.uuid && !shownTestimonialsRef.current.includes(t.uuid),
);

if (available.length === 0) {
available = testimonials;
shownTestimonialsRef.current = [];
}

const random = available[Math.floor(Math.random() * available.length)];

setCurrentTestimonial(random);

if (random.uuid) {
shownTestimonialsRef.current = [...shownTestimonialsRef.current, random.uuid];
}
}, [testimonials]);

return (
<Card>
<PurchaseSummaryHeader companyName={companyName} />
Expand All @@ -79,10 +51,6 @@ const PurchaseSummary = () => {
/>

<DueTodayRow amountDue={0} />

{currentTestimonial && (
<TestimonialCard testimonial={currentTestimonial} />
)}
</Stack>
</Card.Section>

Expand Down
41 changes: 20 additions & 21 deletions src/components/PurchaseSummary/TestimonialCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Card, Stack } from '@openedx/paragon';
import React from 'react';

export interface Testimonial {
Expand All @@ -24,29 +23,29 @@ const TestimonialCard = ({ testimonial }: Props) => {
} = testimonial;

return (
<Card className="mt-4 border-light" data-testid="testimonial-card">
<Card.Body>
<Stack gap={2}>
<div className="h3" aria-hidden="true">
</div>

<blockquote className="mb-0" data-testid="testimonial-quote">
{quoteText}
</blockquote>

<div className="mt-2">
<strong data-testid="testimonial-name">
<div className="testimonial-card mt-4" data-testid="testimonial-card">
<div className="testimonial-card__body">
<div className="testimonial-card__quote-icon" aria-hidden="true">
<span className="testimonial-card__quote-glyph">&#10077;</span>
</div>

<p className="testimonial-card__quote mt-4" data-testid="testimonial-quote">
{quoteText}
</p>
Comment thread
gshivajibiradar marked this conversation as resolved.
Comment thread
gshivajibiradar marked this conversation as resolved.

<div className="testimonial-card__attribution mt-5">
<span className="testimonial-card__attribution-dash" aria-hidden="true">&#8212;</span>
<div className="testimonial-card__attribution-copy">
<span className="testimonial-card__name" data-testid="testimonial-name">
{attributionName}
</strong>

<div className="text-muted" data-testid="testimonial-title">
</span>
<span className="testimonial-card__title d-block" data-testid="testimonial-title">
{attributionTitle}
</div>
</span>
</div>
</Stack>
</Card.Body>
</Card>
</div>
</div>
</div>
);
};

Expand Down
30 changes: 1 addition & 29 deletions src/components/PurchaseSummary/tests/PurchaseSummary.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'whatwg-fetch';

import { usePurchaseSummaryPricing } from '@/components/app/data';
import { SUBSCRIPTION_TRIAL_LENGTH_DAYS } from '@/components/app/data/constants';
import useTestimonials from '@/components/app/data/hooks/useTestimonials';
import { DataStoreKey } from '@/constants/checkout';
import { checkoutFormStore } from '@/hooks/useCheckoutFormStore';

Expand All @@ -21,9 +20,6 @@ jest.mock('@/components/app/data', () => ({
useCheckoutIntent: jest.fn(() => ({ data: { id: 123 } })),
}));

// Mock testimonials hook
jest.mock('@/components/app/data/hooks/useTestimonials');

const renderWithProviders = () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
Expand Down Expand Up @@ -52,8 +48,6 @@ describe('PurchaseSummary', () => {
yearlySubscriptionCostForQuantity: 150,
yearlyCostPerSubscriptionPerUser: 50,
});

(useTestimonials as jest.Mock).mockReturnValue({ data: [], isLoading: false });
});

it('renders header and rows with computed values', () => {
Expand All @@ -72,33 +66,11 @@ describe('PurchaseSummary', () => {
expect(screen.getByText('$0')).toBeInTheDocument();
});

it('renders correctly when testimonials are empty', async () => {
it('renders correctly', async () => {
renderWithProviders();

await waitFor(() => {
expect(screen.getByText('Purchase summary')).toBeInTheDocument();
});
});

it('renders correctly when testimonials exist', async () => {
(useTestimonials as jest.Mock).mockReturnValue({
data: [
{
uuid: '123',
quote_text: 'Great product!',
attribution_name: 'John Doe',
attribution_title: 'CEO',
},
],
isLoading: false,
});

renderWithProviders();

await waitFor(() => {
expect(screen.getByText('Great product!')).toBeInTheDocument();
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('CEO')).toBeInTheDocument();
});
});
});
24 changes: 23 additions & 1 deletion src/components/Stepper/CheckoutStepperContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Col, Row, Stack, Stepper } from '@openedx/paragon';
import { ReactElement, useEffect } from 'react';
import { ReactElement, useEffect, useState } from 'react';

import useTestimonials, {
getShownTestimonialUuids,
pickNextTestimonial,
setShownTestimonialUuids,
} from '@/components/app/data/hooks/useTestimonials';
import { PurchaseSummary } from '@/components/PurchaseSummary';
import TestimonialCard, { Testimonial } from '@/components/PurchaseSummary/TestimonialCard';
import { StepperTitle } from '@/components/Stepper/StepperTitle';
import { AccountDetails, BillingDetails, PlanDetails } from '@/components/Stepper/Steps';
import { CheckoutSubstepKey } from '@/constants/checkout';
Expand All @@ -17,6 +23,21 @@ const Steps = (): ReactElement => (

const CheckoutStepperContainer = (): ReactElement => {
const { currentStepKey, currentSubstepKey } = useCurrentStep();
const { data: testimonials = [] } = useTestimonials();
const [currentTestimonial, setCurrentTestimonial] = useState<Testimonial | null>(null);

useEffect(() => {
if (!testimonials.length) {
setCurrentTestimonial(null);
return;
}
Comment thread
gshivajibiradar marked this conversation as resolved.
Outdated

const shownUuids = getShownTestimonialUuids();
const nextTestimonial = pickNextTestimonial(testimonials, shownUuids);
setShownTestimonialUuids(shownUuids);
setCurrentTestimonial(nextTestimonial);
}, [currentStepKey, testimonials]);

useEffect(() => {
const preventUnload = (e: BeforeUnloadEvent) => {
if (currentSubstepKey !== CheckoutSubstepKey.Success) {
Expand Down Expand Up @@ -47,6 +68,7 @@ const CheckoutStepperContainer = (): ReactElement => {
</Col>
<Col md={12} lg={4}>
<PurchaseSummary />
<TestimonialCard testimonial={currentTestimonial} />
</Col>
</Row>
</Stack>
Expand Down
94 changes: 89 additions & 5 deletions src/components/app/data/hooks/useTestimonials.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,98 @@
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform/config';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

import { Testimonial } from '@/components/PurchaseSummary/TestimonialCard';

const fetchTestimonials = async (): Promise<Testimonial[]> => {
export const DEFAULT_TESTIMONIALS: Testimonial[] = [
{
uuid: 'fallback-1',
quote_text: 'The need for qualified IT workers is at an unprecedented level, and our partnership with edX is providing the skills needed to be successful in an IT career.',
attribution_name: 'Eric Westphal',
attribution_title: 'Leader of Global Workforce Strategy and Economic Development, Cognizant',
},
{
uuid: 'fallback-2',
quote_text: 'The subscription has been a game-changer for upskilling our workforce quickly and efficiently.',
attribution_name: 'Michael Chen',
attribution_title: 'VP of Engineering, Innovate Inc.',
},
{
uuid: 'fallback-3',
quote_text: 'Our employees love the flexibility and breadth of courses available on edX.',
attribution_name: 'Emily Rodriguez',
attribution_title: 'Chief People Officer, GrowthCo',
},
];

export const TESTIMONIALS_SESSION_KEY = 'sspc.testimonials.shown.uuids';

export const getShownTestimonialUuids = (): Set<string> => {
if (typeof window === 'undefined') {
return new Set<string>();
}

try {
const raw = window.sessionStorage.getItem(TESTIMONIALS_SESSION_KEY);
if (!raw) {
return new Set<string>();
}
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
return new Set<string>();
}
return new Set<string>(parsed.filter((value) => typeof value === 'string'));
} catch {
return new Set<string>();
}
};

export const setShownTestimonialUuids = (shownUuids: Set<string>): void => {
if (typeof window === 'undefined') {
return;
}

window.sessionStorage.setItem(TESTIMONIALS_SESSION_KEY, JSON.stringify(Array.from(shownUuids)));
};

export const pickNextTestimonial = (
testimonials: Testimonial[],
shownUuids: Set<string>,
): Testimonial | null => {
if (!testimonials.length) {
return null;
}

const testimonialKey = ({ uuid, quote_text: quoteText, attribution_name: attributionName }: Testimonial) => (
uuid || `${attributionName}::${quoteText}`
);

let available = testimonials.filter((testimonial) => !shownUuids.has(testimonialKey(testimonial)));

if (!available.length) {
shownUuids.clear();
available = testimonials;
}

const next = available[Math.floor(Math.random() * available.length)];
shownUuids.add(testimonialKey(next));

return next;
};
Comment thread
gshivajibiradar marked this conversation as resolved.

export const fetchTestimonials = async (): Promise<Testimonial[]> => {
const { ENTERPRISE_ACCESS_BASE_URL } = getConfig();
const res = await fetch(`${ENTERPRISE_ACCESS_BASE_URL}/api/v1/testimonials/`);
if (!res.ok) { return []; }
const data = await res.json();
return data.results || [];
const url = `${ENTERPRISE_ACCESS_BASE_URL}/api/v1/testimonials/`;
try {
const user = getAuthenticatedUser();
const res = user
? await getAuthenticatedHttpClient().get(url)
: await axios.get(url);
return res.data?.results?.length ? res.data.results : DEFAULT_TESTIMONIALS;
} catch {
return DEFAULT_TESTIMONIALS;
}
Comment thread
gshivajibiradar marked this conversation as resolved.
};

const useTestimonials = () => useQuery({
Expand Down
Loading
Loading