mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-20 02:00:49 +00:00
feat: Twitch configuration steps for Onboarding (almost!)
This commit is contained in:
parent
8947ccfedb
commit
8e9fc433f6
4 changed files with 337 additions and 18 deletions
|
@ -96,7 +96,7 @@
|
||||||
},
|
},
|
||||||
"app-category": "Category",
|
"app-category": "Category",
|
||||||
"app-oauth-redirect-url": "OAuth Redirect URLs",
|
"app-oauth-redirect-url": "OAuth Redirect URLs",
|
||||||
"test-button": "Test",
|
"test-button": "Test connection",
|
||||||
"test-failed": "Test failed: \"{{0}}\". Check your app client IDs and secret!",
|
"test-failed": "Test failed: \"{{0}}\". Check your app client IDs and secret!",
|
||||||
"test-succeeded": "Test succeeded!"
|
"test-succeeded": "Test succeeded!"
|
||||||
},
|
},
|
||||||
|
@ -269,9 +269,14 @@
|
||||||
"sections": {
|
"sections": {
|
||||||
"landing": "Welcome",
|
"landing": "Welcome",
|
||||||
"twitch-config": "Twitch integration",
|
"twitch-config": "Twitch integration",
|
||||||
|
"twitch-events": "Twitch events",
|
||||||
"twitch-bot": "Twitch bot",
|
"twitch-bot": "Twitch bot",
|
||||||
"done": "All done!"
|
"done": "All done!"
|
||||||
}
|
},
|
||||||
|
"twitch-p1": "To set-up Twitch, you will need to create an application on the Developer portal, follow the instructions below or click the button at the bottom to skip this step.",
|
||||||
|
"twitch-p2": "Click \"Test connection\" to make sure the Client ID and secret are valid, if the test is successful you will be brought to the next step automatically.",
|
||||||
|
"twitch-skip": "Skip Twitch integration",
|
||||||
|
"twitch-ev-p1": "Now that you've made an app, you need to authenticate your Twitch account to it so we can access your user data like your channel name or events like new followers or raids."
|
||||||
},
|
},
|
||||||
"uiconfig": {
|
"uiconfig": {
|
||||||
"title": "User interface settings",
|
"title": "User interface settings",
|
||||||
|
|
|
@ -318,7 +318,8 @@
|
||||||
"channel.cheer": "Tifo",
|
"channel.cheer": "Tifo",
|
||||||
"channel.raid": "Raid"
|
"channel.raid": "Raid"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"test-button": "Fai un test"
|
||||||
},
|
},
|
||||||
"uiconfig": {
|
"uiconfig": {
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
|
|
|
@ -1,31 +1,40 @@
|
||||||
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
import {
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
|
} from '@radix-ui/react-icons';
|
||||||
import { keyframes } from '@stitches/react';
|
import { keyframes } from '@stitches/react';
|
||||||
|
import { GetTwitchLoggedUser, GetTwitchAuthURL } from '@wailsapp/go/main/App';
|
||||||
|
import { helix } from '@wailsapp/go/models';
|
||||||
|
import { BrowserOpenURL } from '@wailsapp/runtime/runtime';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useModule } from '~/lib/react';
|
||||||
|
import { checkTwitchKeys } from '~/lib/twitch';
|
||||||
|
import { languages } from '~/locale/languages';
|
||||||
|
import { RootState, useAppDispatch } from '~/store';
|
||||||
|
import apiReducer, { modules } from '~/store/api/reducer';
|
||||||
|
|
||||||
// @ts-expect-error Asset import
|
// @ts-expect-error Asset import
|
||||||
import spinner from '~/assets/icon-logo.svg';
|
import spinner from '~/assets/icon-logo.svg';
|
||||||
import { useModule, useStatus } from '~/lib/react';
|
|
||||||
import { languages } from '~/locale/languages';
|
|
||||||
import { useAppDispatch } from '~/store';
|
|
||||||
import apiReducer, { modules } from '~/store/api/reducer';
|
|
||||||
import AlertContent from '../components/AlertContent';
|
import AlertContent from '../components/AlertContent';
|
||||||
import SaveButton from '../components/forms/SaveButton';
|
import BrowserLink from '../components/BrowserLink';
|
||||||
|
import DefinitionTable from '../components/DefinitionTable';
|
||||||
import RevealLink from '../components/utils/RevealLink';
|
import RevealLink from '../components/utils/RevealLink';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
Field,
|
Field,
|
||||||
FieldNote,
|
|
||||||
InputBox,
|
InputBox,
|
||||||
Label,
|
Label,
|
||||||
MultiToggle,
|
MultiToggle,
|
||||||
MultiToggleItem,
|
MultiToggleItem,
|
||||||
PageContainer,
|
PageContainer,
|
||||||
PageHeader,
|
|
||||||
PageTitle,
|
|
||||||
PasswordInputBox,
|
PasswordInputBox,
|
||||||
|
SectionHeader,
|
||||||
styled,
|
styled,
|
||||||
TextBlock,
|
TextBlock,
|
||||||
} from '../theme';
|
} from '../theme';
|
||||||
|
@ -183,14 +192,14 @@ const StepName = styled('div', {
|
||||||
enum OnboardingSteps {
|
enum OnboardingSteps {
|
||||||
Landing = 0,
|
Landing = 0,
|
||||||
TwitchIntegration = 1,
|
TwitchIntegration = 1,
|
||||||
TwitchBot = 2,
|
TwitchEvents = 2,
|
||||||
Done = 999,
|
Done = 999,
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
OnboardingSteps.Landing,
|
OnboardingSteps.Landing,
|
||||||
OnboardingSteps.TwitchIntegration,
|
OnboardingSteps.TwitchIntegration,
|
||||||
OnboardingSteps.TwitchBot,
|
OnboardingSteps.TwitchEvents,
|
||||||
OnboardingSteps.Done,
|
OnboardingSteps.Done,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -198,7 +207,7 @@ const stepI18n = {
|
||||||
[OnboardingSteps.Landing]: 'pages.onboarding.sections.landing',
|
[OnboardingSteps.Landing]: 'pages.onboarding.sections.landing',
|
||||||
[OnboardingSteps.TwitchIntegration]:
|
[OnboardingSteps.TwitchIntegration]:
|
||||||
'pages.onboarding.sections.twitch-config',
|
'pages.onboarding.sections.twitch-config',
|
||||||
[OnboardingSteps.TwitchBot]: 'pages.onboarding.sections.twitch-bot',
|
[OnboardingSteps.TwitchEvents]: 'pages.onboarding.sections.twitch-events',
|
||||||
[OnboardingSteps.Done]: 'pages.onboarding.sections.done',
|
[OnboardingSteps.Done]: 'pages.onboarding.sections.done',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -207,6 +216,296 @@ const maxKeys = languages.reduce(
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type TestResult = { open: boolean; error?: Error };
|
||||||
|
|
||||||
|
const TwitchStepList = styled('ul', {
|
||||||
|
lineHeight: '1.5',
|
||||||
|
listStyleType: 'none',
|
||||||
|
listStylePosition: 'outside',
|
||||||
|
});
|
||||||
|
const TwitchStep = styled('li', {
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
paddingLeft: '1rem',
|
||||||
|
'&::marker': {
|
||||||
|
color: '$teal11',
|
||||||
|
content: '▧',
|
||||||
|
display: 'inline-block',
|
||||||
|
marginLeft: '-0.5rem',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function TwitchIntegrationStep() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [httpConfig] = useModule(modules.httpConfig);
|
||||||
|
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||||
|
const [uiConfig, setUiConfig] = useModule(modules.uiConfig);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [revealClientSecret, setRevealClientSecret] = useState(false);
|
||||||
|
const [testing, setTesting] = useState(false);
|
||||||
|
const [testResult, setTestResult] = useState<TestResult>({
|
||||||
|
open: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkCredentials = async () => {
|
||||||
|
setTesting(true);
|
||||||
|
if (twitchConfig) {
|
||||||
|
try {
|
||||||
|
await checkTwitchKeys(
|
||||||
|
twitchConfig.api_client_id,
|
||||||
|
twitchConfig.api_client_secret,
|
||||||
|
);
|
||||||
|
void dispatch(setTwitchConfig(twitchConfig));
|
||||||
|
void dispatch(
|
||||||
|
setUiConfig({
|
||||||
|
...uiConfig,
|
||||||
|
onboardingStatus: uiConfig.onboardingStatus + 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
setTestResult({ open: true, error: e as Error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTesting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
function skipTwitch() {
|
||||||
|
void dispatch(
|
||||||
|
setUiConfig({
|
||||||
|
...uiConfig,
|
||||||
|
onboardingStatus:
|
||||||
|
steps.findIndex((val) => val === OnboardingSteps.TwitchEvents) + 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={(ev) => {
|
||||||
|
void dispatch(setTwitchConfig(twitchConfig));
|
||||||
|
ev.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextBlock>{t('pages.onboarding.twitch-p1')}</TextBlock>
|
||||||
|
<TwitchStepList>
|
||||||
|
<TwitchStep>
|
||||||
|
<Trans i18nKey="pages.twitch-settings.apiguide-2">
|
||||||
|
{' '}
|
||||||
|
<BrowserLink href="https://dev.twitch.tv/console/apps/create">
|
||||||
|
https://dev.twitch.tv/console/apps/create
|
||||||
|
</BrowserLink>
|
||||||
|
</Trans>
|
||||||
|
</TwitchStep>
|
||||||
|
<TwitchStep>
|
||||||
|
{t('pages.twitch-settings.apiguide-3')}
|
||||||
|
|
||||||
|
<DefinitionTable
|
||||||
|
entries={{
|
||||||
|
[t('pages.twitch-settings.app-oauth-redirect-url')]: `http://${
|
||||||
|
httpConfig?.bind.indexOf(':') > 0
|
||||||
|
? httpConfig.bind
|
||||||
|
: `localhost${httpConfig?.bind ?? ':4337'}`
|
||||||
|
}/twitch/callback`,
|
||||||
|
[t('pages.twitch-settings.app-category')]: 'Broadcasting Suite',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TwitchStep>
|
||||||
|
<TwitchStep>
|
||||||
|
<Trans i18nKey="pages.twitch-settings.apiguide-4">
|
||||||
|
{'str1 '}
|
||||||
|
<b>str2</b>
|
||||||
|
</Trans>
|
||||||
|
</TwitchStep>
|
||||||
|
</TwitchStepList>
|
||||||
|
<Field size="fullWidth" css={{ marginTop: '2rem' }}>
|
||||||
|
<Label htmlFor="clientid">
|
||||||
|
{t('pages.twitch-settings.app-client-id')}
|
||||||
|
</Label>
|
||||||
|
<InputBox
|
||||||
|
type="text"
|
||||||
|
id="clientid"
|
||||||
|
placeholder={t('pages.twitch-settings.app-client-id')}
|
||||||
|
required={true}
|
||||||
|
value={twitchConfig?.api_client_id ?? ''}
|
||||||
|
onChange={(ev) =>
|
||||||
|
dispatch(
|
||||||
|
apiReducer.actions.twitchConfigChanged({
|
||||||
|
...twitchConfig,
|
||||||
|
api_client_id: ev.target.value,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field size="fullWidth">
|
||||||
|
<Label htmlFor="clientsecret">
|
||||||
|
{t('pages.twitch-settings.app-client-secret')}
|
||||||
|
<RevealLink
|
||||||
|
value={revealClientSecret}
|
||||||
|
setter={setRevealClientSecret}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
<PasswordInputBox
|
||||||
|
reveal={revealClientSecret}
|
||||||
|
id="clientsecret"
|
||||||
|
placeholder={t('pages.twitch-settings.app-client-secret')}
|
||||||
|
required={true}
|
||||||
|
value={twitchConfig?.api_client_secret ?? ''}
|
||||||
|
onChange={(ev) =>
|
||||||
|
dispatch(
|
||||||
|
apiReducer.actions.twitchConfigChanged({
|
||||||
|
...twitchConfig,
|
||||||
|
api_client_secret: ev.target.value,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<TextBlock>{t('pages.onboarding.twitch-p2')}</TextBlock>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variation={'primary'}
|
||||||
|
onClick={() => {
|
||||||
|
void checkCredentials();
|
||||||
|
}}
|
||||||
|
disabled={testing}
|
||||||
|
>
|
||||||
|
{t('pages.twitch-settings.test-button')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
skipTwitch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('pages.onboarding.twitch-skip')}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Alert
|
||||||
|
defaultOpen={false}
|
||||||
|
open={testResult.open}
|
||||||
|
onOpenChange={(val: boolean) => {
|
||||||
|
setTestResult({ ...testResult, open: val });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertContent
|
||||||
|
variation={testResult.error ? 'danger' : 'default'}
|
||||||
|
description={
|
||||||
|
testResult.error
|
||||||
|
? t('pages.twitch-settings.test-failed', [
|
||||||
|
testResult.error.message,
|
||||||
|
])
|
||||||
|
: t('pages.twitch-settings.test-succeeded')
|
||||||
|
}
|
||||||
|
actionText={t('form-actions.ok')}
|
||||||
|
onAction={() => {
|
||||||
|
setTestResult({ ...testResult, open: false });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SyncError {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TwitchUser = styled('div', {
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.8rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: '14pt',
|
||||||
|
fontWeight: '300',
|
||||||
|
});
|
||||||
|
const TwitchPic = styled('img', {
|
||||||
|
width: '48px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
});
|
||||||
|
const TwitchName = styled('p', { fontWeight: 'bold' });
|
||||||
|
|
||||||
|
function TwitchEventsStep() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [userStatus, setUserStatus] = useState<helix.User | SyncError>(null);
|
||||||
|
const [twitchAuthLink, setTwitchAuthLink] = useState('');
|
||||||
|
const kv = useSelector((state: RootState) => state.api.client);
|
||||||
|
|
||||||
|
const getUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const res = await GetTwitchLoggedUser();
|
||||||
|
setUserStatus(res);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setUserStatus({ ok: false, error: (e as Error).message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startAuthFlow = async () => {
|
||||||
|
const url = await GetTwitchAuthURL();
|
||||||
|
BrowserOpenURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Get user info
|
||||||
|
void getUserInfo();
|
||||||
|
void GetTwitchAuthURL().then(setTwitchAuthLink);
|
||||||
|
|
||||||
|
const onKeyChange = () => {
|
||||||
|
void getUserInfo();
|
||||||
|
};
|
||||||
|
void kv.subscribeKey('twitch/auth-keys', onKeyChange);
|
||||||
|
return () => {
|
||||||
|
void kv.unsubscribeKey('twitch/auth-keys', onKeyChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let userBlock = <i>{t('pages.twitch-settings.events.loading-data')}</i>;
|
||||||
|
if (userStatus !== null) {
|
||||||
|
if ('id' in userStatus) {
|
||||||
|
userBlock = (
|
||||||
|
<>
|
||||||
|
<TwitchUser>
|
||||||
|
<TextBlock>
|
||||||
|
{t('pages.twitch-settings.events.authenticated-as')}
|
||||||
|
</TextBlock>
|
||||||
|
<TwitchPic
|
||||||
|
src={userStatus.profile_image_url}
|
||||||
|
alt={t('pages.twitch-settings.events.profile-picture')}
|
||||||
|
/>
|
||||||
|
<TwitchName>{userStatus.display_name}</TwitchName>
|
||||||
|
</TwitchUser>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
userBlock = <span>{t('pages.twitch-settings.events.err-no-user')}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TextBlock>{t('pages.onboarding.twitch-ev-p1')}</TextBlock>
|
||||||
|
<TextBlock>{t('pages.twitch-settings.events.auth-message')}</TextBlock>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
variation="primary"
|
||||||
|
onClick={() => {
|
||||||
|
void startAuthFlow();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon /> {t('pages.twitch-settings.events.auth-button')}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<TextBlock>or use the following link: </TextBlock>
|
||||||
|
<BrowserLink href={twitchAuthLink}>{twitchAuthLink}</BrowserLink>
|
||||||
|
<SectionHeader>
|
||||||
|
{t('pages.twitch-settings.events.current-status')}
|
||||||
|
</SectionHeader>
|
||||||
|
{userBlock}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function OnboardingPage() {
|
export default function OnboardingPage() {
|
||||||
const [t, i18n] = useTranslation();
|
const [t, i18n] = useTranslation();
|
||||||
const [animationItems, setAnimationItems] = useState<JSX.Element[]>([]);
|
const [animationItems, setAnimationItems] = useState<JSX.Element[]>([]);
|
||||||
|
@ -284,6 +583,12 @@ export default function OnboardingPage() {
|
||||||
</ActionContainer>
|
</ActionContainer>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case OnboardingSteps.TwitchIntegration:
|
||||||
|
currentStepBody = <TwitchIntegrationStep />;
|
||||||
|
break;
|
||||||
|
case OnboardingSteps.TwitchEvents:
|
||||||
|
currentStepBody = <TwitchEventsStep />;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -214,7 +214,7 @@ function TwitchAPISettings() {
|
||||||
open: false,
|
open: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function checkCredentials() {
|
const checkCredentials = async () => {
|
||||||
setTesting(true);
|
setTesting(true);
|
||||||
if (twitchConfig) {
|
if (twitchConfig) {
|
||||||
try {
|
try {
|
||||||
|
@ -229,7 +229,7 @@ function TwitchAPISettings() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTesting(false);
|
setTesting(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
|
@ -407,6 +407,14 @@ function TwitchEventSubSettings() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Get user info
|
// Get user info
|
||||||
void getUserInfo();
|
void getUserInfo();
|
||||||
|
|
||||||
|
const onKeyChange = () => {
|
||||||
|
void getUserInfo();
|
||||||
|
};
|
||||||
|
void kv.subscribeKey('twitch/auth-keys', onKeyChange);
|
||||||
|
return () => {
|
||||||
|
void kv.unsubscribeKey('twitch/auth-keys', onKeyChange);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let userBlock = <i>{t('pages.twitch-settings.events.loading-data')}</i>;
|
let userBlock = <i>{t('pages.twitch-settings.events.loading-data')}</i>;
|
||||||
|
|
Loading…
Reference in a new issue