diff --git a/src/components/LoginForm/LoginForm.tsx b/src/components/LoginForm/LoginForm.tsx index 47245a6ab..48ced7871 100644 --- a/src/components/LoginForm/LoginForm.tsx +++ b/src/components/LoginForm/LoginForm.tsx @@ -13,6 +13,7 @@ import Visibility from '#src/icons/Visibility'; import VisibilityOff from '#src/icons/VisibilityOff'; import FormFeedback from '#components/FormFeedback/FormFeedback'; import LoadingOverlay from '#components/LoadingOverlay/LoadingOverlay'; +import SocialButtonsList from '#components/SocialButtonsList/SocialButtonsList'; import { testId } from '#src/utils/common'; import { addQueryParam } from '#src/utils/location'; import type { FormErrors } from '#types/form'; @@ -35,6 +36,7 @@ const LoginForm: React.FC = ({ onSubmit, onChange, values, errors, submit return (
+

{t('login.sign_in')}

{errors.form ? {errors.form} : null} { const { t } = useTranslation('error'); @@ -45,6 +48,36 @@ const Root: FC = () => { registerCustomScreens(); }, []); + const userLoading = useAccountStore((s) => s.loading); + + const navigate = useNavigate(); + const location = useLocation(); + + const getAuthParams = useCallback(() => { + const queryParams = new URLSearchParams(window.location.href.split('#')[1]); + const token = queryParams.get('token'); + const refreshToken = queryParams.get('refresh_token'); + const expires = queryParams.get('expires'); + return { token, refreshToken, expires: parseInt(expires ?? '') }; + }, []); + + useEffect(() => { + const getUserInfo = async () => { + const auth = getAuthParams(); + if (!auth.token || !auth.refreshToken || !auth.expires) { + return; + } + InPlayer.Account.setToken(auth.token, auth.refreshToken, auth.expires); + await getAccount({ + jwt: auth.token, + refreshToken: auth.refreshToken, + customerToken: '', + }); + navigate(location.pathname); + }; + getUserInfo(); + }, [getAuthParams, location.pathname, navigate, userLoading]); + const IS_DEMO_OR_PREVIEW = IS_DEMO_MODE || IS_PREVIEW_MODE; // Show the spinner while loading except in demo mode (the demo config shows its own loading status) diff --git a/src/components/SocialButton/SocialButton.module.scss b/src/components/SocialButton/SocialButton.module.scss index eff4212f7..cb3f5c2f2 100644 --- a/src/components/SocialButton/SocialButton.module.scss +++ b/src/components/SocialButton/SocialButton.module.scss @@ -1,16 +1,57 @@ +@use 'src/styles/variables'; +@use 'src/styles/theme'; + .socialButtonContainer { + position: relative; /* Add relative positioning to the container */ display: flex; - justify-content: center; + justify-content: center; /* Center the contents horizontally and vertically */ align-items: center; - width: fit-content; + width: 100%; height: fit-content; + padding: 0.5rem; + color: variables.$gray-darker; + text-decoration: none; + background-color: variables.$white; + border: 1px solid #979797; + border-radius: 0.25rem; + gap: 0.5rem; + transition: background-color 0.1s ease-in-out; } .socialButtonContainer:hover { - transform: scale(1.05); + background-color: variables.$gray-lighter; cursor: pointer; } .socialButtonContainer:active { - transform: scale(0.95); + background-color: variables.$gray-light; +} + +.socialButtonIconContainer { + position: absolute; + left: 0.5rem; + display: flex; + justify-content: center; + align-items: center; + width: 2.5rem; + height: 100%; +} + +.socialButtonIcon { + width: 100%; + height: 100%; +} + +.socialButtonTextContainer { + display: flex; + flex-grow: 1; + justify-content: center; + align-items: center; + height: 100%; + padding: 0.5rem; + font-weight: 600; + font-size: 1.25rem; + text-decoration: none; + border-radius: 0.5rem; + gap: 0.5rem; } diff --git a/src/components/SocialButton/SocialButton.tsx b/src/components/SocialButton/SocialButton.tsx index 4a053c79d..d0df76bc7 100644 --- a/src/components/SocialButton/SocialButton.tsx +++ b/src/components/SocialButton/SocialButton.tsx @@ -6,10 +6,10 @@ export type SocialButtonVariant = 'facebook' | 'google' | 'twitter'; interface SocialButtonProps { variant: SocialButtonVariant; - onClick: () => void; + href: string; } -const SocialButton = ({ variant, onClick }: SocialButtonProps) => { +const SocialButton = ({ variant, href }: SocialButtonProps) => { const [icon, setIcon] = useState(null); useEffect(() => { @@ -21,9 +21,12 @@ const SocialButton = ({ variant, onClick }: SocialButtonProps) => { }, [variant]); return ( -
- {`${variant} -
+ +
+ {`${variant} +
+ Sign in with {variant.charAt(0).toUpperCase() + variant.slice(1)} +
); }; diff --git a/src/components/SocialButtonsList/SocialButtonsList.module.scss b/src/components/SocialButtonsList/SocialButtonsList.module.scss index 6ca484cab..7f50dde49 100644 --- a/src/components/SocialButtonsList/SocialButtonsList.module.scss +++ b/src/components/SocialButtonsList/SocialButtonsList.module.scss @@ -1,9 +1,9 @@ .socialButtonsListContainer { display: flex; - flex-direction: row; + flex-direction: column; justify-content: center; align-items: center; width: 100%; - padding: 1rem; + padding: 1rem 0; gap: 1rem; } diff --git a/src/components/SocialButtonsList/SocialButtonsList.tsx b/src/components/SocialButtonsList/SocialButtonsList.tsx index d989339b4..f8eb0e0f4 100644 --- a/src/components/SocialButtonsList/SocialButtonsList.tsx +++ b/src/components/SocialButtonsList/SocialButtonsList.tsx @@ -1,21 +1,32 @@ +import { useQuery } from 'react-query'; + import SocialButton, { SocialButtonVariant } from '../SocialButton/SocialButton'; import styles from './SocialButtonsList.module.scss'; -type SocialButtonsListProps = { - buttonProps: { - [key in SocialButtonVariant]: { - onClick: () => void; - }; - }; -}; +import { getSocialLoginUrls } from '#src/stores/AccountController'; + +const SocialButtonsList = () => { + const urls = useQuery('socialUrls', getSocialLoginUrls); + + if (urls.error || !urls.data) { + return null; + } + + const formattedData = urls.data.reduce( + (acc, url) => ({ + ...acc, + ...url, + }), + {} as { + [key in SocialButtonVariant]: string; + }, + ); -const SocialButtonsList = ({ buttonProps }: SocialButtonsListProps) => { - const variants: SocialButtonVariant[] = ['facebook', 'google', 'twitter']; return (
- {variants.map((variant) => ( - + {Object.entries(formattedData).map(([variant, url]) => ( + ))}
); diff --git a/src/services/cleeng.account.service.ts b/src/services/cleeng.account.service.ts index c5ba17680..a5c7374be 100644 --- a/src/services/cleeng.account.service.ts +++ b/src/services/cleeng.account.service.ts @@ -222,6 +222,8 @@ export const updatePersonalShelves: UpdatePersonalShelves = async (payload, sand }; export const exportAccountData = () => null; +export const getSocialUrls = () => null; + export const canUpdateEmail = true; export const canSupportEmptyFullName = true; diff --git a/src/services/inplayer.account.service.ts b/src/services/inplayer.account.service.ts index 20e1f4368..9803b39df 100644 --- a/src/services/inplayer.account.service.ts +++ b/src/services/inplayer.account.service.ts @@ -327,6 +327,23 @@ export const exportAccountData: ExportAccountData = async () => { }; }; +export const getSocialUrls = async (config: Config) => { + const socialState = window.btoa( + JSON.stringify({ + client_id: config.integrations.jwp?.clientId || '', + redirect: window.location.href.split('u=')[0], + }), + ); + + const socialResponse = await InPlayer.Account.getSocialLoginUrls(socialState); + + if (socialResponse.status !== 200) { + throw new Error('Failed to fetch social urls'); + } + + return socialResponse.data.social_urls; +}; + const getCustomerExternalData = async (): Promise => { const [favoritesData, historyData] = await Promise.all([InPlayer.Account.getFavorites(), await InPlayer.Account.getWatchHistory({})]); diff --git a/src/stores/AccountController.ts b/src/stores/AccountController.ts index 6b8f42e6b..9d2172a98 100644 --- a/src/stores/AccountController.ts +++ b/src/stores/AccountController.ts @@ -466,6 +466,12 @@ export async function exportAccountData() { }); } +export async function getSocialLoginUrls() { + return await useService(async ({ accountService, config }) => { + return await accountService.getSocialUrls(config); + }); +} + /** * Get multiple media items for the given IDs. This function uses watchlists to get several medias via just one request. *