From 8e9fc433f63ffe4a1e32327dfe68c881db731af8 Mon Sep 17 00:00:00 2001 From: Ash Keel Date: Tue, 20 Dec 2022 16:15:42 +0100 Subject: [PATCH] feat: Twitch configuration steps for Onboarding (almost!) --- frontend/src/locale/en/translation.json | 9 +- frontend/src/locale/it/translation.json | 3 +- frontend/src/ui/pages/Onboarding.tsx | 331 ++++++++++++++++++++++- frontend/src/ui/pages/TwitchSettings.tsx | 12 +- 4 files changed, 337 insertions(+), 18 deletions(-) diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json index e0c4ffd..450992f 100644 --- a/frontend/src/locale/en/translation.json +++ b/frontend/src/locale/en/translation.json @@ -96,7 +96,7 @@ }, "app-category": "Category", "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-succeeded": "Test succeeded!" }, @@ -269,9 +269,14 @@ "sections": { "landing": "Welcome", "twitch-config": "Twitch integration", + "twitch-events": "Twitch events", "twitch-bot": "Twitch bot", "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": { "title": "User interface settings", diff --git a/frontend/src/locale/it/translation.json b/frontend/src/locale/it/translation.json index 5359c10..7c4683b 100644 --- a/frontend/src/locale/it/translation.json +++ b/frontend/src/locale/it/translation.json @@ -318,7 +318,8 @@ "channel.cheer": "Tifo", "channel.raid": "Raid" } - } + }, + "test-button": "Fai un test" }, "uiconfig": { "language": "Lingua", diff --git a/frontend/src/ui/pages/Onboarding.tsx b/frontend/src/ui/pages/Onboarding.tsx index 007dd38..88fe76e 100644 --- a/frontend/src/ui/pages/Onboarding.tsx +++ b/frontend/src/ui/pages/Onboarding.tsx @@ -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 { 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 { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; 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 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 SaveButton from '../components/forms/SaveButton'; +import BrowserLink from '../components/BrowserLink'; +import DefinitionTable from '../components/DefinitionTable'; import RevealLink from '../components/utils/RevealLink'; import { Button, + ButtonGroup, Field, - FieldNote, InputBox, Label, MultiToggle, MultiToggleItem, PageContainer, - PageHeader, - PageTitle, PasswordInputBox, + SectionHeader, styled, TextBlock, } from '../theme'; @@ -183,14 +192,14 @@ const StepName = styled('div', { enum OnboardingSteps { Landing = 0, TwitchIntegration = 1, - TwitchBot = 2, + TwitchEvents = 2, Done = 999, } const steps = [ OnboardingSteps.Landing, OnboardingSteps.TwitchIntegration, - OnboardingSteps.TwitchBot, + OnboardingSteps.TwitchEvents, OnboardingSteps.Done, ]; @@ -198,7 +207,7 @@ const stepI18n = { [OnboardingSteps.Landing]: 'pages.onboarding.sections.landing', [OnboardingSteps.TwitchIntegration]: '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', }; @@ -207,6 +216,296 @@ const maxKeys = languages.reduce( 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({ + 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 ( +
{ + void dispatch(setTwitchConfig(twitchConfig)); + ev.preventDefault(); + }} + > + {t('pages.onboarding.twitch-p1')} + + + + {' '} + + https://dev.twitch.tv/console/apps/create + + + + + {t('pages.twitch-settings.apiguide-3')} + + 0 + ? httpConfig.bind + : `localhost${httpConfig?.bind ?? ':4337'}` + }/twitch/callback`, + [t('pages.twitch-settings.app-category')]: 'Broadcasting Suite', + }} + /> + + + + {'str1 '} + str2 + + + + + + + dispatch( + apiReducer.actions.twitchConfigChanged({ + ...twitchConfig, + api_client_id: ev.target.value, + }), + ) + } + /> + + + + + + dispatch( + apiReducer.actions.twitchConfigChanged({ + ...twitchConfig, + api_client_secret: ev.target.value, + }), + ) + } + /> + + {t('pages.onboarding.twitch-p2')} + + + + + { + setTestResult({ ...testResult, open: val }); + }} + > + { + setTestResult({ ...testResult, open: false }); + }} + /> + +
+ ); +} + +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(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 = {t('pages.twitch-settings.events.loading-data')}; + if (userStatus !== null) { + if ('id' in userStatus) { + userBlock = ( + <> + + + {t('pages.twitch-settings.events.authenticated-as')} + + + {userStatus.display_name} + + + ); + } else { + userBlock = {t('pages.twitch-settings.events.err-no-user')}; + } + } + return ( +
+ {t('pages.onboarding.twitch-ev-p1')} + {t('pages.twitch-settings.events.auth-message')} + + + + or use the following link: + {twitchAuthLink} + + {t('pages.twitch-settings.events.current-status')} + + {userBlock} +
+ ); +} + export default function OnboardingPage() { const [t, i18n] = useTranslation(); const [animationItems, setAnimationItems] = useState([]); @@ -284,6 +583,12 @@ export default function OnboardingPage() { ); break; + case OnboardingSteps.TwitchIntegration: + currentStepBody = ; + break; + case OnboardingSteps.TwitchEvents: + currentStepBody = ; + break; } return ( diff --git a/frontend/src/ui/pages/TwitchSettings.tsx b/frontend/src/ui/pages/TwitchSettings.tsx index 710ba2d..9b8225f 100644 --- a/frontend/src/ui/pages/TwitchSettings.tsx +++ b/frontend/src/ui/pages/TwitchSettings.tsx @@ -214,7 +214,7 @@ function TwitchAPISettings() { open: false, }); - async function checkCredentials() { + const checkCredentials = async () => { setTesting(true); if (twitchConfig) { try { @@ -229,7 +229,7 @@ function TwitchAPISettings() { } } setTesting(false); - } + }; return (
{ // Get user info void getUserInfo(); + + const onKeyChange = () => { + void getUserInfo(); + }; + void kv.subscribeKey('twitch/auth-keys', onKeyChange); + return () => { + void kv.unsubscribeKey('twitch/auth-keys', onKeyChange); + }; }, []); let userBlock = {t('pages.twitch-settings.events.loading-data')};