mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-01-11 22:40:31 +00:00
feat: add multi-account switching support
Implements functionality to switch between multiple Stremio accounts stored locally without logging out. Key changes: - Added AccountManager to handle localStorage persistence, token management, and settings. - Added ProfilePicker component with UI for selecting, managing, and deleting profiles. - Integrated "Change Profile" and "Log out" actions in the NavMenu. - Improved accessibility (aria-labels, semantic buttons) and UI consistency (CSS variables). - Fixed race conditions in authentication flow and improved hook dependencies. - Added graceful handling of token expiration.
This commit is contained in:
parent
da675cd56c
commit
482f2e77f7
8 changed files with 628 additions and 24 deletions
|
|
@ -15,7 +15,9 @@ const withProtectedRoutes = (Component) => {
|
|||
previousAuthRef.current = profile.auth;
|
||||
}, [profile]);
|
||||
const onRouteChange = React.useCallback((routeConfig) => {
|
||||
if (profile.auth !== null && routeConfig.component === Intro) {
|
||||
// Allow access to /intro when ?profiles=true (for profile switching)
|
||||
const urlParams = new URLSearchParams(window.location.hash.split('?')[1]);
|
||||
if (profile.auth !== null && routeConfig.component === Intro && urlParams.get('profiles') !== 'true') {
|
||||
window.location.replace('#/');
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
86
src/common/AccountManager.js
Normal file
86
src/common/AccountManager.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (C) 2017-2026 Smart code 203358507
|
||||
|
||||
const STORAGE_KEY = 'stremio-multi-accounts';
|
||||
|
||||
const AccountManager = {
|
||||
getAccounts: () => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
|
||||
} catch (_e) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
addAccount: (profile, authKey, settings = null) => {
|
||||
try {
|
||||
const accounts = AccountManager.getAccounts();
|
||||
const existing = accounts.find((a) => a.email === profile.email);
|
||||
const filtered = accounts.filter((a) => a.email !== profile.email);
|
||||
|
||||
const newAccount = {
|
||||
email: profile.email,
|
||||
avatar: profile.avatar,
|
||||
authKey: authKey,
|
||||
settings: settings || existing?.settings || null,
|
||||
lastActive: Date.now()
|
||||
};
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify([...filtered, newAccount]));
|
||||
} catch (_e) {
|
||||
console.error('Failed to add account', _e);
|
||||
}
|
||||
},
|
||||
|
||||
updateSettings: (email, settings) => {
|
||||
try {
|
||||
const accounts = AccountManager.getAccounts();
|
||||
const updated = accounts.map((a) =>
|
||||
a.email === email ? { ...a, settings } : a
|
||||
);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
||||
} catch (_e) {
|
||||
console.error('Failed to update settings', _e);
|
||||
}
|
||||
},
|
||||
|
||||
getSettings: (email) => {
|
||||
const accounts = AccountManager.getAccounts();
|
||||
const account = accounts.find((a) => a.email === email);
|
||||
return account?.settings || null;
|
||||
},
|
||||
|
||||
removeAccount: (email) => {
|
||||
try {
|
||||
const accounts = AccountManager.getAccounts();
|
||||
const filtered = accounts.filter((a) => a.email !== email);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
|
||||
} catch (_e) {
|
||||
console.error('Failed to remove account', _e);
|
||||
}
|
||||
},
|
||||
|
||||
switchTo: (email) => {
|
||||
const accounts = AccountManager.getAccounts();
|
||||
const target = accounts.find((a) => a.email === email);
|
||||
if (target && target.authKey) {
|
||||
let updatedTarget = target;
|
||||
const updatedAccounts = accounts.map((a) => {
|
||||
if (a.email === email) {
|
||||
const newAccount = { ...a, lastActive: Date.now() };
|
||||
updatedTarget = newAccount;
|
||||
return newAccount;
|
||||
}
|
||||
return a;
|
||||
});
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedAccounts));
|
||||
} catch (_e) {
|
||||
console.error('Failed to switch account', _e);
|
||||
}
|
||||
return updatedTarget;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AccountManager;
|
||||
|
|
@ -8,6 +8,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
|
|||
const { useServices } = require('stremio/services');
|
||||
const { Button } = require('stremio/components');
|
||||
const { default: useFullscreen } = require('stremio/common/useFullscreen');
|
||||
const AccountManager = require('stremio/common/AccountManager');
|
||||
const useProfile = require('stremio/common/useProfile');
|
||||
const usePWA = require('stremio/common/usePWA');
|
||||
const useTorrent = require('stremio/common/useTorrent');
|
||||
|
|
@ -29,14 +30,33 @@ const NavMenuContent = ({ onClick }) => {
|
|||
profile.settings.streamingServerWarningDismissed.getTime() > Date.now()
|
||||
);
|
||||
}, [profile.settings, streamingServer.settings]);
|
||||
const onChangeProfile = React.useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
if (profile.auth && profile.auth.user && profile.auth.key) {
|
||||
AccountManager.addAccount(profile.auth.user, profile.auth.key, profile.settings);
|
||||
}
|
||||
window.location.hash = '#/intro?profiles=true';
|
||||
}, [profile.auth, profile.settings]);
|
||||
const logoutButtonOnClick = React.useCallback(() => {
|
||||
const currentEmail = profile.auth?.user?.email;
|
||||
const accounts = AccountManager.getAccounts();
|
||||
const remainingAccounts = accounts.filter((acc) => acc.email !== currentEmail);
|
||||
|
||||
if (currentEmail) {
|
||||
AccountManager.removeAccount(currentEmail);
|
||||
}
|
||||
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'Logout'
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (remainingAccounts.length > 0) {
|
||||
window.location.hash = '#/intro?profiles=true';
|
||||
}
|
||||
}, [profile.auth, core]);
|
||||
const onPlayMagnetLinkClick = React.useCallback(async () => {
|
||||
try {
|
||||
const clipboardText = await navigator.clipboard.readText();
|
||||
|
|
@ -64,9 +84,25 @@ const NavMenuContent = ({ onClick }) => {
|
|||
<div className={styles['email-container']}>
|
||||
<div className={styles['email-label']}>{profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}</div>
|
||||
</div>
|
||||
<Button className={styles['logout-button-container']} title={profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')} href={profile.auth === null ? '#/intro' : null} onClick={profile.auth !== null ? logoutButtonOnClick : null}>
|
||||
<div className={styles['logout-label']}>{profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')}</div>
|
||||
</Button>
|
||||
<div className={styles['user-actions-container']}>
|
||||
{
|
||||
profile.auth !== null ?
|
||||
<Button className={styles['user-action-button']} title={t('CHANGE_PROFILE', { defaultValue: 'Change Profile' })} onClick={onChangeProfile}>
|
||||
<div className={styles['user-action-label']}>{t('CHANGE_PROFILE', { defaultValue: 'Change Profile' })}</div>
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
profile.auth !== null ?
|
||||
<div className={styles['user-actions-separator']}>/</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<Button className={styles['user-action-button']} title={profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')} href={profile.auth === null ? '#/intro' : null} onClick={profile.auth !== null ? logoutButtonOnClick : null}>
|
||||
<div className={styles['user-action-label']}>{profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')}</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -61,22 +61,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
.logout-button-container {
|
||||
.user-actions-container {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&:hover, &:focus {
|
||||
outline: none;
|
||||
|
||||
.logout-label {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.user-actions-separator {
|
||||
color: var(--primary-foreground-color);
|
||||
opacity: 0.5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.logout-label {
|
||||
flex: 1;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--primary-foreground-color);
|
||||
.user-action-button {
|
||||
flex: none;
|
||||
|
||||
&:hover, &:focus {
|
||||
outline: none;
|
||||
|
||||
.user-action-label {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.user-action-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
152
src/components/ProfilePicker/ProfilePicker.js
Normal file
152
src/components/ProfilePicker/ProfilePicker.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const { Image } = require('stremio/components');
|
||||
const styles = require('./styles.less');
|
||||
|
||||
const ProfileItem = ({ profile, isEditing, onSelect, onDelete }) => {
|
||||
const { t } = useTranslation();
|
||||
const displayName = profile.email
|
||||
? profile.email.split('@')[0]
|
||||
: t('USER', { defaultValue: 'User' });
|
||||
|
||||
const handleClick = React.useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
if (!isEditing) {
|
||||
onSelect(profile);
|
||||
}
|
||||
}, [isEditing, onSelect, profile]);
|
||||
|
||||
const handleDelete = React.useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
onDelete(profile);
|
||||
}, [onDelete, profile]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={classnames(styles['profile-item'], { [styles['editing']]: isEditing })}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={styles['avatar-wrapper']}>
|
||||
<Image
|
||||
className={styles['avatar']}
|
||||
src={profile.avatar || require('/images/default_avatar.png')}
|
||||
alt={profile.email}
|
||||
/>
|
||||
{isEditing && (
|
||||
<button
|
||||
className={styles['delete-button']}
|
||||
onClick={handleDelete}
|
||||
title={t('DELETE_PROFILE', { defaultValue: 'Delete profile' })}
|
||||
aria-label={t('DELETE_PROFILE', { defaultValue: 'Delete profile' })}
|
||||
>
|
||||
<Icon className={styles['delete-icon']} name={'close'} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<span className={styles['profile-name']}>{displayName}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ProfileItem.propTypes = {
|
||||
profile: PropTypes.shape({
|
||||
email: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
authKey: PropTypes.string,
|
||||
settings: PropTypes.object,
|
||||
lastActive: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
])
|
||||
}).isRequired,
|
||||
isEditing: PropTypes.bool,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const AddProfileButton = ({ onClick, t }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={styles['profile-item']}
|
||||
onClick={onClick}
|
||||
aria-label={t('ADD_PROFILE', { defaultValue: 'Add new profile' })}
|
||||
>
|
||||
<div className={classnames(styles['avatar-wrapper'], styles['add-profile-wrapper'])}>
|
||||
<Icon className={styles['add-icon']} name={'add'} />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
AddProfileButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const ProfilePicker = ({ profiles = [], onSelectProfile, onAddProfile, onDeleteProfile }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
|
||||
const toggleEditMode = React.useCallback(() => {
|
||||
setIsEditing((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const handleDelete = React.useCallback((profile) => {
|
||||
if (onDeleteProfile) {
|
||||
onDeleteProfile(profile);
|
||||
}
|
||||
}, [onDeleteProfile]);
|
||||
|
||||
const title = isEditing
|
||||
? t('MANAGE_PROFILES', { defaultValue: 'Manage Profiles' })
|
||||
: t('WHO_IS_WATCHING', { defaultValue: 'Who is watching?' });
|
||||
|
||||
const buttonText = isEditing
|
||||
? t('DONE', { defaultValue: 'DONE' })
|
||||
: t('MANAGE_PROFILES', { defaultValue: 'MANAGE PROFILES' });
|
||||
|
||||
return (
|
||||
<div className={styles['profile-picker-overlay']}>
|
||||
<div className={styles['background-container']} />
|
||||
<div className={styles['profile-picker-container']}>
|
||||
<h1 className={styles['title']}>{title}</h1>
|
||||
|
||||
<div className={styles['profiles-grid']}>
|
||||
{profiles.map((profile, index) => (
|
||||
<ProfileItem
|
||||
key={profile.email || index}
|
||||
profile={profile}
|
||||
isEditing={isEditing}
|
||||
onSelect={onSelectProfile}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
{!isEditing && <AddProfileButton onClick={onAddProfile} t={t} />}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={classnames(styles['manage-button'], { [styles['done-button']]: isEditing })}
|
||||
onClick={toggleEditMode}
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProfilePicker.propTypes = {
|
||||
profiles: PropTypes.array,
|
||||
onSelectProfile: PropTypes.func,
|
||||
onAddProfile: PropTypes.func,
|
||||
onDeleteProfile: PropTypes.func
|
||||
};
|
||||
|
||||
module.exports = ProfilePicker;
|
||||
232
src/components/ProfilePicker/styles.less
Normal file
232
src/components/ProfilePicker/styles.less
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
|
||||
.profile-picker-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'PlusJakartaSans', sans-serif;
|
||||
user-select: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.background-container {
|
||||
z-index: -1;
|
||||
position: fixed;
|
||||
top: -1rem;
|
||||
bottom: -1rem;
|
||||
left: -1rem;
|
||||
right: -1rem;
|
||||
background: url('/images/background_1.svg'), url('/images/background_2.svg');
|
||||
background-color: var(--primary-background-color);
|
||||
background-position: bottom left, top right;
|
||||
background-size: 53%, 54%;
|
||||
background-repeat: no-repeat;
|
||||
filter: blur(6rem);
|
||||
}
|
||||
|
||||
.profile-picker-container {
|
||||
text-align: center;
|
||||
animation: fadeIn 0.4s ease-in-out;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--primary-foreground-color);
|
||||
font-size: 3rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2rem;
|
||||
user-select: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.profiles-grid {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
min-height: 200px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.profile-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition: transform 0.2s ease;
|
||||
overflow: visible;
|
||||
padding: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
.avatar-wrapper {
|
||||
border-color: var(--primary-foreground-color);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.profile-name {
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
.avatar-wrapper {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
&.editing {
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
.avatar-wrapper {
|
||||
transform: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
width: 10vw;
|
||||
height: 10vw;
|
||||
min-width: 84px;
|
||||
min-height: 84px;
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
border: 3px solid transparent;
|
||||
transition: border-color 0.2s, transform 0.2s, opacity 0.2s;
|
||||
position: relative;
|
||||
|
||||
&.add-profile-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--overlay-color);
|
||||
border: 3px solid var(--secondary-foreground-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-foreground-color);
|
||||
border-color: var(--primary-foreground-color);
|
||||
.add-icon {
|
||||
fill: var(--primary-background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--signal-error-color);
|
||||
border: 2px solid var(--primary-background-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||
z-index: 10;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: var(--primary-foreground-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
fill: var(--primary-foreground-color);
|
||||
transition: fill 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
color: var(--primary-foreground-color);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
max-width: 12vw;
|
||||
min-width: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.manage-button {
|
||||
margin-top: 3rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--primary-foreground-color);
|
||||
color: var(--primary-foreground-color);
|
||||
padding: 0.5rem 1.5rem;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 3.5rem;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.done-button {
|
||||
background-color: var(--primary-accent-color);
|
||||
color: var(--primary-foreground-color);
|
||||
border-color: var(--primary-accent-color);
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: scale(1.1); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
|
@ -30,8 +30,10 @@ import TextInput from './TextInput';
|
|||
import Toggle from './Toggle';
|
||||
import Transition from './Transition';
|
||||
import Video from './Video';
|
||||
import ProfilePicker from './ProfilePicker/ProfilePicker';
|
||||
|
||||
export {
|
||||
ProfilePicker,
|
||||
AddonDetailsModal,
|
||||
BottomSheet,
|
||||
Button,
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
|
|||
const { Modal, useRouteFocused } = require('stremio-router');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useBinaryState } = require('stremio/common');
|
||||
const { Button, Image, Checkbox } = require('stremio/components');
|
||||
const { Button, Image, Checkbox, ProfilePicker } = require('stremio/components');
|
||||
const CredentialsTextInput = require('./CredentialsTextInput');
|
||||
const PasswordResetModal = require('./PasswordResetModal');
|
||||
const useFacebookLogin = require('./useFacebookLogin');
|
||||
const { default: useAppleLogin } = require('./useAppleLogin');
|
||||
const AccountManager = require('stremio/common/AccountManager');
|
||||
|
||||
const styles = require('./styles');
|
||||
|
||||
|
|
@ -249,8 +250,10 @@ const Intro = ({ queryParams }) => {
|
|||
dispatch({ type: 'toggle-checkbox', name: 'marketingAccepted' });
|
||||
}, []);
|
||||
const switchFormOnClick = React.useCallback(() => {
|
||||
const queryParams = new URLSearchParams([['form', state.form === SIGNUP_FORM ? LOGIN_FORM : SIGNUP_FORM]]);
|
||||
window.location = `#/intro?${queryParams.toString()}`;
|
||||
const newForm = state.form === SIGNUP_FORM ? LOGIN_FORM : SIGNUP_FORM;
|
||||
const currentParams = new URLSearchParams(window.location.hash.split('?')[1]);
|
||||
currentParams.set('form', newForm);
|
||||
window.location = `#/intro?${currentParams.toString()}`;
|
||||
}, [state.form]);
|
||||
React.useEffect(() => {
|
||||
if ([LOGIN_FORM, SIGNUP_FORM].includes(queryParams.get('form'))) {
|
||||
|
|
@ -262,15 +265,32 @@ const Intro = ({ queryParams }) => {
|
|||
errorRef.current.scrollIntoView();
|
||||
}
|
||||
}, [state.error]);
|
||||
const [savedAccounts, setSavedAccounts] = React.useState(() => AccountManager.getAccounts());
|
||||
const [pendingProfile, setPendingProfile] = React.useState(null);
|
||||
const showProfilesParam = queryParams.get('profiles') === 'true';
|
||||
const [showProfiles, setShowProfiles] = React.useState(showProfilesParam || savedAccounts.length > 0);
|
||||
React.useEffect(() => {
|
||||
if (routeFocused) {
|
||||
if (routeFocused && !showProfiles && emailRef.current) {
|
||||
emailRef.current.focus();
|
||||
}
|
||||
}, [state.form, routeFocused]);
|
||||
}, [state.form, routeFocused, showProfiles]);
|
||||
React.useEffect(() => {
|
||||
const onCoreEvent = ({ event, args }) => {
|
||||
switch (event) {
|
||||
case 'UserAuthenticated': {
|
||||
if (args.auth && args.auth.key) {
|
||||
AccountManager.addAccount(args.user, args.auth.key);
|
||||
}
|
||||
if (pendingProfile && pendingProfile.settings) {
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'UpdateSettings',
|
||||
args: pendingProfile.settings
|
||||
}
|
||||
});
|
||||
}
|
||||
setPendingProfile(null);
|
||||
closeLoaderModal();
|
||||
if (routeFocused) {
|
||||
window.location = '#/';
|
||||
|
|
@ -280,8 +300,23 @@ const Intro = ({ queryParams }) => {
|
|||
case 'Error': {
|
||||
if (args.source.event === 'UserAuthenticated') {
|
||||
closeLoaderModal();
|
||||
if (pendingProfile) {
|
||||
AccountManager.removeAccount(pendingProfile.email);
|
||||
setSavedAccounts(AccountManager.getAccounts());
|
||||
dispatch({ type: 'set-form', form: LOGIN_FORM });
|
||||
dispatch({
|
||||
type: 'error',
|
||||
error: t('SESSION_EXPIRED', { defaultValue: 'Session expired. Please log in again.' })
|
||||
});
|
||||
dispatch({
|
||||
type: 'change-credentials',
|
||||
name: 'email',
|
||||
value: pendingProfile.email
|
||||
});
|
||||
setShowProfiles(false);
|
||||
setPendingProfile(null);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -290,7 +325,53 @@ const Intro = ({ queryParams }) => {
|
|||
return () => {
|
||||
core.transport.off('CoreEvent', onCoreEvent);
|
||||
};
|
||||
}, [routeFocused]);
|
||||
}, [routeFocused, pendingProfile, t, closeLoaderModal, dispatch, core]);
|
||||
const onSelectProfile = React.useCallback((profile) => {
|
||||
if (pendingProfile) return;
|
||||
const account = AccountManager.switchTo(profile.email);
|
||||
if (account && account.authKey) {
|
||||
setPendingProfile({ ...profile, settings: account.settings });
|
||||
openLoaderModal();
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'Authenticate',
|
||||
args: {
|
||||
type: 'LoginWithToken',
|
||||
token: account.authKey
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'change-credentials',
|
||||
name: 'email',
|
||||
value: profile.email
|
||||
});
|
||||
setShowProfiles(false);
|
||||
}
|
||||
}, [core, openLoaderModal, pendingProfile, dispatch]);
|
||||
const onAddProfile = React.useCallback(() => {
|
||||
setShowProfiles(false);
|
||||
}, []);
|
||||
const onDeleteProfile = React.useCallback((profile) => {
|
||||
AccountManager.removeAccount(profile.email);
|
||||
const updatedAccounts = AccountManager.getAccounts();
|
||||
setSavedAccounts(updatedAccounts);
|
||||
if (updatedAccounts.length === 0) {
|
||||
setShowProfiles(false);
|
||||
}
|
||||
}, []);
|
||||
if (showProfiles && savedAccounts.length > 0) {
|
||||
return (
|
||||
<ProfilePicker
|
||||
profiles={savedAccounts}
|
||||
onSelectProfile={onSelectProfile}
|
||||
onAddProfile={onAddProfile}
|
||||
onDeleteProfile={onDeleteProfile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={styles['intro-container']}>
|
||||
<div className={styles['background-container']} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue