Skip to content

Commit

Permalink
Merge pull request #296 from jwplayer/social-buttons
Browse files Browse the repository at this point in the history
Feat/social login buttons
  • Loading branch information
naumovski-filip authored Jun 15, 2023
2 parents ba4bf91 + 29226b3 commit 13b151d
Show file tree
Hide file tree
Showing 14 changed files with 190 additions and 1 deletion.
3 changes: 3 additions & 0 deletions public/locales/en/account.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,17 @@
},
"login": {
"email": "Email",
"facebook": "Sign in with Facebook",
"field_is_not_valid_email": "Please re-enter your email details and try again.",
"field_required": "This field is required",
"forgot_password": "Forgot password?",
"google": "Sign in with Google",
"hide_password": "Hide password",
"not_registered": "New to {{siteName}}?",
"password": "Password",
"sign_in": "Sign in",
"sign_up": "Sign up",
"twitter": "Sign in with Twitter",
"view_password": "View password",
"wrong_combination": "Incorrect email/password combination",
"wrong_email": "Please check your email and try again."
Expand Down
3 changes: 3 additions & 0 deletions public/locales/es/account.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,17 @@
},
"login": {
"email": "Correo electrónico",
"facebook": "",
"field_is_not_valid_email": "Por favor, vuelve a ingresar tus datos de correo electrónico y prueba nuevamente.",
"field_required": "Este campo es obligatorio",
"forgot_password": "¿Olvidaste tu contraseña?",
"google": "",
"hide_password": "Ocultar contraseña",
"not_registered": "¿Nuevo/a en {{siteName}}?",
"password": "Contraseña",
"sign_in": "Iniciar sesión",
"sign_up": "Registrarse",
"twitter": "",
"view_password": "Ver contraseña",
"wrong_combination": "Combinación de correo electrónico/contraseña incorrecta",
"wrong_email": "Por favor, verifica tu correo electrónico e intenta nuevamente."
Expand Down
1 change: 1 addition & 0 deletions src/assets/icons/facebook.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/icons/google.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/icons/twitter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/components/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -35,6 +36,7 @@ const LoginForm: React.FC<Props> = ({ onSubmit, onChange, values, errors, submit

return (
<form onSubmit={onSubmit} data-testid={testId('login-form')} noValidate>
<SocialButtonsList />
<h2 className={styles.title}>{t('login.sign_in')}</h2>
{errors.form ? <FormFeedback variant="error">{errors.form}</FormFeedback> : null}
<TextField
Expand Down
9 changes: 8 additions & 1 deletion src/components/Root/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { FC, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useSearchParams } from 'react-router-dom';
import { Navigate, useSearchParams } from 'react-router-dom';

import ErrorPage from '#components/ErrorPage/ErrorPage';
import AccountModal from '#src/containers/AccountModal/AccountModal';
Expand All @@ -14,6 +14,7 @@ import { loadAndValidateConfig } from '#src/utils/configLoad';
import { initSettings } from '#src/stores/SettingsController';
import AppRoutes from '#src/containers/AppRoutes/AppRoutes';
import registerCustomScreens from '#src/screenMapping';
import { useAccountStore } from '#src/stores/AccountStore';

const Root: FC = () => {
const { t } = useTranslation('error');
Expand Down Expand Up @@ -45,6 +46,12 @@ const Root: FC = () => {
registerCustomScreens();
}, []);

const userData = useAccountStore((s) => ({ loading: s.loading, user: s.user }));

if (userData.user && !userData.loading && window.location.href.includes('#token')) {
return <Navigate to="/" />; // component instead of hook to prevent extra re-renders
}

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)
Expand Down
57 changes: 57 additions & 0 deletions src/components/SocialButton/SocialButton.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@use 'src/styles/variables';
@use 'src/styles/theme';

.socialButtonContainer {
position: relative;
display: flex;
justify-content: center;
align-items: center;
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 {
background-color: variables.$gray-lighter;
cursor: pointer;
}

.socialButtonContainer:active {
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;
}
39 changes: 39 additions & 0 deletions src/components/SocialButton/SocialButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

import styles from './SocialButton.module.scss';

export type SocialButtonVariant = 'facebook' | 'google' | 'twitter';

interface SocialButtonProps {
variant: SocialButtonVariant;
href: string;
}

const SocialButton = ({ variant, href }: SocialButtonProps) => {
const [icon, setIcon] = useState<string | null>(null);

const { t } = useTranslation('account');

useEffect(() => {
const getIcon = async () => {
const iconSvg = await (import(`../../assets/icons/${variant}.svg`) as Promise<{ default: string }>);
setIcon(iconSvg.default);
};
getIcon();
}, [variant]);

return (
// t('login.facebook');
// t('login.google');
// t('login.twitter');
<a href={href} className={styles.socialButtonContainer} aria-label={t(`login.${variant}`)}>
<div className={styles.socialButtonIconContainer}>
<img className={styles.socialButtonIcon} src={icon ?? ''} alt={`${variant} icon`} />
</div>
<span className={styles.socialButtonTextContainer}>{t(`login.${variant}`)}</span>
</a>
);
};

export default SocialButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.socialButtonsListContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
padding: 1rem 0;
gap: 1rem;
}
35 changes: 35 additions & 0 deletions src/components/SocialButtonsList/SocialButtonsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useQuery } from 'react-query';

import SocialButton, { SocialButtonVariant } from '../SocialButton/SocialButton';

import styles from './SocialButtonsList.module.scss';

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;
},
);

return (
<div className={styles.socialButtonsListContainer}>
{Object.entries(formattedData).map(([variant, url]) => (
<SocialButton key={variant} variant={variant as SocialButtonVariant} href={url} />
))}
</div>
);
};

export default SocialButtonsList;
1 change: 1 addition & 0 deletions src/services/cleeng.account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export const updatePersonalShelves: UpdatePersonalShelves = async (payload, sand

export const exportAccountData = () => null;

export const getSocialUrls = () => null;
export const deleteAccount = () => null;

export const canUpdateEmail = true;
Expand Down
25 changes: 25 additions & 0 deletions src/services/inplayer.account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ enum InPlayerEnv {
export const initialize = async (config: Config, _logoutFn: () => Promise<void>) => {
const env: string = config.integrations?.jwp?.useSandbox ? InPlayerEnv.Development : InPlayerEnv.Production;
InPlayer.setConfig(env as Env);
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');
if (!token || !refreshToken || !expires) {
return;
}
InPlayer.Account.setToken(token, refreshToken, parseInt(expires));
};

export const getAuthData = async () => {
Expand Down Expand Up @@ -343,6 +351,23 @@ export const deleteAccount: DeleteAccount = async ({ password }) => {
}
};

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<ExternalData> => {
const [favoritesData, historyData] = await Promise.all([InPlayer.Account.getFavorites(), await InPlayer.Account.getWatchHistory({})]);

Expand Down
5 changes: 5 additions & 0 deletions src/stores/AccountController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,11 @@ export async function exportAccountData() {
});
}

export async function getSocialLoginUrls() {
return await useService(async ({ accountService, config }) => {
return await accountService.getSocialUrls(config);
});
}
export async function deleteAccountData(password: string) {
return await useAccount(async () => {
return await useService(async ({ accountService }) => {
Expand Down

0 comments on commit 13b151d

Please sign in to comment.