Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8745c57
UAS GET -Single Article status retrieval with new feature toggle
jinidev Mar 24, 2026
64b0f38
adding comments
jinidev Mar 24, 2026
46edd7a
Update src/app/hooks/useUASButton/index.ts
jinidev Mar 25, 2026
738b6d5
Merge branch 'latest' of https://github.com/bbc/simorgh into ws-2209-…
jinidev Mar 25, 2026
2c9da88
updated snapshots
jinidev Mar 25, 2026
98da345
Renamed useUASFetchSavestatus hook
jinidev Mar 25, 2026
ebb3206
Merge branch 'ws-2209-GET-UAS-single-article-status' of https://githu…
jinidev Mar 25, 2026
2fc9efd
useUASbutton test updates
jinidev Mar 25, 2026
589b3d3
conflict resolved
jinidev Mar 26, 2026
baba7f3
Merge branch 'latest' of https://github.com/bbc/simorgh into ws-2209-…
jinidev Mar 26, 2026
4cf99af
Renamed Toggle name
jinidev Mar 26, 2026
1b01966
Restricted for isAmp,isLite and isAPP
jinidev Mar 26, 2026
2f7670c
Merge branch 'latest' into ws-2209-GET-UAS-single-article-status
elvinasv Mar 27, 2026
13f2038
fix: brand logo on smaller screens
elvinasv Mar 27, 2026
5989c0a
Revert "fix: brand logo on smaller screens"
elvinasv Mar 27, 2026
183d1b1
Merge branch 'latest' of https://github.com/bbc/simorgh into ws-2209-…
jinidev Mar 27, 2026
a3c0893
Default state of loading as true , removed hardcode isSignedin Value
jinidev Mar 27, 2026
0c1cbe2
review comments based changes
jinidev Mar 27, 2026
4b5b05b
Adding logs to debug
jinidev Mar 30, 2026
b2a51cf
Merge branch 'latest' of https://github.com/bbc/simorgh into ws-2209-…
jinidev Mar 30, 2026
8e5783b
eXtra logs for getAUthHeader
jinidev Mar 30, 2026
a1ee9c8
ENV vars renamed
jinidev Mar 30, 2026
98a471e
Merge branch 'latest' of https://github.com/bbc/simorgh into ws-2209-…
jinidev Mar 30, 2026
baf8b00
all UAS env vars renamed - debugging
jinidev Mar 30, 2026
168ab06
Merge branch 'latest' of https://github.com/bbc/simorgh into ws-2209-…
jinidev Mar 31, 2026
1630391
Debugging default value in apikey
jinidev Mar 31, 2026
759c3d3
Removed logs and hardcoded values
jinidev Mar 31, 2026
cf3af04
Merge branch 'latest' of https://github.com/bbc/simorgh into ws-2209-…
jinidev Mar 31, 2026
02e338b
Style updates to button
jinidev Mar 31, 2026
f4ff5eb
Merge branch 'latest' into ws-2209-GET-UAS-single-article-status
jinidev Mar 31, 2026
528709a
Merge branch 'latest' into ws-2209-GET-UAS-single-article-status
jinidev Mar 31, 2026
eb9a5a3
Merge branch 'latest' into ws-2209-GET-UAS-single-article-status
jinidev Mar 31, 2026
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
10 changes: 10 additions & 0 deletions src/app/components/SaveArticleButton/index.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** Temporary css styling until UX work is complete */
const styles = {
buttonWrapper: {
display: 'flex',
marginTop: '1rem',
marginBottom: '1rem',
},
};

export default styles;
65 changes: 65 additions & 0 deletions src/app/components/SaveArticleButton/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import useUASButton from '#app/hooks/useUASButton';
import { render, screen } from '../react-testing-library-with-providers';
import SaveArticleButton from './index';

jest.mock('#app/hooks/useUASButton');

const mockedUseUASButton = useUASButton as jest.Mock;

describe('SaveArticleButton', () => {
const defaultProps = {
articleId: '123',
service: 'hindi',
};

afterEach(() => {
jest.clearAllMocks();
});

test('does not render button when showButton is false', () => {
mockedUseUASButton.mockReturnValue({
showButton: false,
isSaved: false,
isLoading: false,
});

const { container } = render(<SaveArticleButton {...defaultProps} />);
expect(container.firstChild).toBeNull();
});

test('renders "Save for later" when not saved', () => {
mockedUseUASButton.mockReturnValue({
showButton: true,
isSaved: false,
isLoading: false,
});

render(<SaveArticleButton {...defaultProps} />);
expect(screen.getByRole('button')).toHaveTextContent('Save for later');
});

test('renders "Remove from saved" when saved', () => {
mockedUseUASButton.mockReturnValue({
showButton: true,
isSaved: true,
isLoading: false,
});

render(<SaveArticleButton {...defaultProps} />);
expect(screen.getByRole('button')).toHaveTextContent('Remove from saved');
});

test('renders loading state and disables button', () => {
mockedUseUASButton.mockReturnValue({
showButton: true,
isSaved: false,
isLoading: true,
});

render(<SaveArticleButton {...defaultProps} />);
const button = screen.getByRole('button');

expect(button).toHaveTextContent('Loading...');
expect(button).toBeDisabled();
});
});
49 changes: 49 additions & 0 deletions src/app/components/SaveArticleButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import useUASButton from '#app/hooks/useUASButton';
import styles from './index.styles';

interface SaveArticleButtonProps {
articleId: string;
service: string;
}
/** A button component that allows users to save an article for later reading,
* showing the button based on user sign in status and feature toggles,
* and displaying the saved status, loading state, and handling errors from the UAS API.
* FUTURE TODO : Implement button click handler to toggle saved state */

const SaveArticleButton = ({ articleId, service }: SaveArticleButtonProps) => {
const { showButton, isSaved, isLoading, error } = useUASButton({
articleId,
service,
});

if (!showButton) {
return null;
}
// TODO : Labels and text will be updated in a future PR to support translations and figma designs
const buttonLabel = isSaved ? 'Remove from saved' : 'Save for later';

const getButtonText = () => {
if (isLoading) return 'Loading...';
return isSaved ? 'Remove from saved' : 'Save for later';
};

// TODO : Will modify based on future error handling implementation,
// currently just hides the button if there is an error fetching save status
if (error) {
return null;
}

return (
<button
css={styles.buttonWrapper}
type="button"
disabled={isLoading}
aria-label={buttonLabel}
title={buttonLabel}
>
{getButtonText()}
</button>
);
};

export default SaveArticleButton;
10 changes: 9 additions & 1 deletion src/app/contexts/AccountContext/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { AccountContextProps, IdctaConfig } from '#app/models/types/account';
import appendCtaQueryParams from '#app/lib/idcta/appendCtaQueryParams';
import { ServiceContext } from '#app/contexts/ServiceContext';
import { RequestContext } from '#app/contexts/RequestContext';
import onClient from '#app/lib/utilities/onClient';
import Cookie from 'js-cookie';

Expand All @@ -29,13 +30,20 @@ export const AccountProvider = ({
initialConfig,
}: PropsWithChildren<AccountProviderProps>) => {
const { locale } = use(ServiceContext);
const { isAmp = false, isApp = false, isLite = false } = use(RequestContext);
const [pageToReturnTo, setPageToReturnTo] = useState<string | null>(null);

useEffect(() => {
setPageToReturnTo(window.location.href);
}, []);

const isIdctaAvailable = initialConfig?.['id-availability'] === 'GREEN';
// IDCTA / UAS is not available on AMP, Lite or App platforms — ensure provider
// centralises this logic so individual components don't need to check platform.
const isIdctaAvailable =
initialConfig?.['id-availability'] === 'GREEN' &&
!isAmp &&
!isLite &&
!isApp;

const buildAccountUrl = (url?: string) => {
return isIdctaAvailable && url
Expand Down
125 changes: 125 additions & 0 deletions src/app/hooks/useUASButton/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { use } from 'react';
import { renderHook } from '#app/components/react-testing-library-with-providers';
import useUASFetchSaveStatus from '#app/hooks/useUASFetchSaveStatus';
import isLocal from '#app/lib/utilities/isLocal';
import useUASButton from './index';

import useToggle from '../useToggle';

jest.mock('#app/hooks/useUASFetchSaveStatus');
jest.mock('../useToggle');
jest.mock('#app/lib/utilities/isLocal');
jest.mock('react', () => ({
...jest.requireActual('react'),
use: jest.fn(),
}));

const mockuseUASFetchSaveStatus = useUASFetchSaveStatus as jest.Mock;
const mockUseToggle = useToggle as jest.Mock;
const mockIsLocal = isLocal as jest.Mock;
describe('useUASButton', () => {
const defaultProps = {
articleId: '123',
service: 'hindi',
};

beforeEach(() => {
jest.clearAllMocks();

mockuseUASFetchSaveStatus.mockReturnValue({
isSaved: false,
isLoading: false,
error: null,
});

(use as jest.Mock).mockReturnValue({
isSignedIn: false,
});
});

afterEach(() => {
jest.clearAllMocks();
});

test('returns showButton = false when feature toggle is off', () => {
mockUseToggle.mockReturnValue({ enabled: false });
mockIsLocal.mockReturnValue(false);
(use as jest.Mock).mockReturnValue({ isSignedIn: true });

const { result } = renderHook(() => useUASButton(defaultProps));

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

test('returns showButton = false when user is not signed in', () => {
mockUseToggle.mockReturnValue({ enabled: true });
mockIsLocal.mockReturnValue(false);
(use as jest.Mock).mockReturnValue({ isSignedIn: false });

const { result } = renderHook(() => useUASButton({ ...defaultProps }));

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

test('returns showButton = true when feature enabled and signed in', () => {
mockUseToggle.mockReturnValue({ enabled: true });
mockIsLocal.mockReturnValue(false);
(use as jest.Mock).mockReturnValue({ isSignedIn: true });

mockuseUASFetchSaveStatus.mockReturnValue({
isSaved: true,
isLoading: false,
error: null,
});

const { result } = renderHook(() => useUASButton(defaultProps));

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

test('passes articleId to useUASFetchSaveStatus when showButton is true', () => {
mockUseToggle.mockReturnValue({ enabled: true });
mockIsLocal.mockReturnValue(false);
(use as jest.Mock).mockReturnValue({ isSignedIn: true });

renderHook(() => useUASButton(defaultProps));

expect(mockuseUASFetchSaveStatus).toHaveBeenCalledWith('123');
});

test('passes empty string when showButton is false', () => {
mockUseToggle.mockReturnValue({ enabled: false });
mockIsLocal.mockReturnValue(false);
(use as jest.Mock).mockReturnValue({ isSignedIn: false });

renderHook(() => useUASButton(defaultProps));

expect(mockuseUASFetchSaveStatus).toHaveBeenCalledWith('');
});

test('respects local environment service filtering', () => {
mockUseToggle.mockReturnValue({
enabled: true,
value: 'hindi|sport',
});
mockIsLocal.mockReturnValue(true);
(use as jest.Mock).mockReturnValue({ isSignedIn: true });

const { result } = renderHook(() => useUASButton(defaultProps));

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

test('hides button if service not in toggle value in local', () => {
mockUseToggle.mockReturnValue({
enabled: true,
value: 'mundo',
});
mockIsLocal.mockReturnValue(true);
(use as jest.Mock).mockReturnValue({ isSignedIn: true });

const { result } = renderHook(() => useUASButton(defaultProps));

expect(result.current.showButton).toBe(false);
});
});
50 changes: 50 additions & 0 deletions src/app/hooks/useUASButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { use } from 'react';
import useUASFetchSaveStatus from '#app/hooks/useUASFetchSaveStatus';
import { AccountContext } from '#app/contexts/AccountContext';
import isLocal from '#app/lib/utilities/isLocal';
import useToggle from '../useToggle';

/** A hook that fetches an article’s saved status and controls showing the save UAS button
* based on feature toggles and sign in status,
* with room to later expand for toggling the save state based on user actions. */

interface UseUASButtonProps {
articleId: string;
service: string;
}

interface UseUASButtonReturn {
showButton: boolean;
isSaved: boolean;
isLoading: boolean;
error: Error | null;
}

const useUASButton = ({
service,
articleId,
}: UseUASButtonProps): UseUASButtonReturn => {
const { isSignedIn } = use(AccountContext);
const { enabled: featureToggleOn = false, value: accountService = '' } =
useToggle('uasPersonalization');

const isUASEnabled =
featureToggleOn &&
(isLocal()
? accountService?.toString().split('|').includes(service)
: true);

const showButton = isUASEnabled && isSignedIn;

const { isSaved, isLoading, error } = useUASFetchSaveStatus(
showButton ? articleId : '',
);
return {
showButton,
isSaved,
isLoading,
error,
};
};

export default useUASButton;
Loading
Loading