From 3d0e824b4bbffadc20fad9fbf2ab70dc7d776184 Mon Sep 17 00:00:00 2001 From: Ash Keel Date: Tue, 2 Apr 2024 23:15:57 +0200 Subject: [PATCH] refactor: move logic around! --- frontend/src/lib/twitch.ts | 12 + frontend/src/ui/App.tsx | 2 +- .../src/ui/components/TwitchUserBlock.tsx | 73 +++ frontend/src/ui/pages/Onboarding.tsx | 108 +---- frontend/src/ui/pages/TwitchSettings.tsx | 441 ------------------ frontend/src/ui/pages/TwitchSettings/Page.tsx | 85 ++++ .../TwitchSettings/TwitchAPISettings.tsx | 195 ++++++++ .../TwitchSettings/TwitchChatSettings.tsx | 54 +++ .../TwitchSettings/TwitchEventSubSettings.tsx | 57 +++ 9 files changed, 494 insertions(+), 533 deletions(-) create mode 100644 frontend/src/ui/components/TwitchUserBlock.tsx delete mode 100644 frontend/src/ui/pages/TwitchSettings.tsx create mode 100644 frontend/src/ui/pages/TwitchSettings/Page.tsx create mode 100644 frontend/src/ui/pages/TwitchSettings/TwitchAPISettings.tsx create mode 100644 frontend/src/ui/pages/TwitchSettings/TwitchChatSettings.tsx create mode 100644 frontend/src/ui/pages/TwitchSettings/TwitchEventSubSettings.tsx diff --git a/frontend/src/lib/twitch.ts b/frontend/src/lib/twitch.ts index 0d34ddc..42c4295 100644 --- a/frontend/src/lib/twitch.ts +++ b/frontend/src/lib/twitch.ts @@ -1,3 +1,6 @@ +import { GetTwitchAuthURL } from '@wailsapp/go/main/App'; +import { BrowserOpenURL } from '@wailsapp/runtime/runtime'; + export interface TwitchCredentials { access_token: string; refresh_token: string; @@ -57,3 +60,12 @@ export async function checkTwitchKeys( throw new Error(`API test call failed: ${err.message}`); } } + +/** + * Open the user's browser to authenticate with Twitch + * @param target What's the target of the authentication (stream/chat account) + */ +export async function startAuthFlow(target: string) { + const url = await GetTwitchAuthURL(target); + BrowserOpenURL(url); +} diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx index 5634309..4679d42 100644 --- a/frontend/src/ui/App.tsx +++ b/frontend/src/ui/App.tsx @@ -42,7 +42,7 @@ import LoyaltyRewardsPage from './pages/LoyaltyRewards'; import OnboardingPage from './pages/Onboarding'; import ServerSettingsPage from './pages/ServerSettings'; import StrimertulPage from './pages/Strimertul'; -import TwitchSettingsPage from './pages/TwitchSettings'; +import TwitchSettingsPage from './pages/TwitchSettings/Page'; import UISettingsPage from './pages/UISettingsPage'; import ExtensionsPage from './pages/Extensions'; import { getTheme, styled } from './theme'; diff --git a/frontend/src/ui/components/TwitchUserBlock.tsx b/frontend/src/ui/components/TwitchUserBlock.tsx new file mode 100644 index 0000000..eca1e0f --- /dev/null +++ b/frontend/src/ui/components/TwitchUserBlock.tsx @@ -0,0 +1,73 @@ +import { GetTwitchLoggedUser } from '@wailsapp/go/main/App'; +import { helix } from '@wailsapp/go/models'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAppSelector } from '~/store'; +import { TextBlock, styled } from '../theme'; + +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' }); + +export default function TwitchUserBlock({ authKey }: { authKey: string }) { + const { t } = useTranslation(); + const [user, setUser] = useState(null); + const kv = useAppSelector((state) => state.api.client); + + const getUserInfo = async () => { + try { + const res = await GetTwitchLoggedUser(authKey); + setUser(res); + } catch (e) { + console.error(e); + setUser({ ok: false, error: (e as Error).message }); + } + }; + + useEffect(() => { + // Get user info + void getUserInfo(); + + const onKeyChange = () => { + void getUserInfo(); + }; + void kv.subscribeKey(authKey, onKeyChange); + return () => { + void kv.unsubscribeKey(authKey, onKeyChange); + }; + }, []); + + if (user !== null) { + if ('id' in user) { + return ( + + + {t('pages.twitch-settings.events.authenticated-as')} + + + {user.display_name} + + ); + } + return {t('pages.twitch-settings.events.err-no-user')}; + } + + return {t('pages.twitch-settings.events.loading-data')}; +} diff --git a/frontend/src/ui/pages/Onboarding.tsx b/frontend/src/ui/pages/Onboarding.tsx index a3480c0..2db67d1 100644 --- a/frontend/src/ui/pages/Onboarding.tsx +++ b/frontend/src/ui/pages/Onboarding.tsx @@ -11,7 +11,11 @@ import { useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useModule } from '~/lib/react'; -import { checkTwitchKeys, TwitchCredentials } from '~/lib/twitch'; +import { + checkTwitchKeys, + startAuthFlow, + TwitchCredentials, +} from '~/lib/twitch'; import { languages } from '~/locale/languages'; import { RootState, useAppDispatch } from '~/store'; import apiReducer, { modules } from '~/store/api/reducer'; @@ -42,6 +46,7 @@ import { themes, } from '../theme'; import { Alert } from '../theme/alert'; +import TwitchUserBlock from '../components/TwitchUserBlock'; const Container = styled('div', { display: 'flex', @@ -428,56 +433,12 @@ function TwitchIntegrationStep() { ); } -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 [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig); const [uiConfig, setUiConfig] = useModule(modules.uiConfig); - const kv = useSelector((state: RootState) => state.api.client); const dispatch = useAppDispatch(); - const getUserInfo = async () => { - try { - const res = await GetTwitchLoggedUser('twitch/auth-keys'); - setUserStatus(res); - } catch (e) { - console.error(e); - setUserStatus({ ok: false, error: (e as Error).message }); - } - }; - - const startAuthFlow = async () => { - const url = await GetTwitchAuthURL('stream'); - BrowserOpenURL(url); - }; - const finishStep = async () => { - if ('id' in userStatus) { - // Set bot config to sane defaults - await dispatch( - setTwitchConfig({ - ...twitchConfig, - }), - ); - } await dispatch( setUiConfig({ ...uiConfig, @@ -487,50 +448,6 @@ function TwitchEventsStep() { ); }; - useEffect(() => { - // 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')}; - if (userStatus !== null) { - if ('id' in userStatus) { - userBlock = ( - <> - - - {t('pages.twitch-settings.events.authenticated-as')} - - - {userStatus.display_name} - - {t('pages.onboarding.twitch-ev-p3')} - - - ); - } else { - userBlock = {t('pages.twitch-settings.events.err-no-user')}; - } - } return (
{t('pages.onboarding.twitch-ev-p1')} @@ -539,7 +456,7 @@ function TwitchEventsStep() {
); } diff --git a/frontend/src/ui/pages/TwitchSettings.tsx b/frontend/src/ui/pages/TwitchSettings.tsx deleted file mode 100644 index 9449d8a..0000000 --- a/frontend/src/ui/pages/TwitchSettings.tsx +++ /dev/null @@ -1,441 +0,0 @@ -import { CheckIcon, ExternalLinkIcon } from '@radix-ui/react-icons'; -import { GetTwitchAuthURL, GetTwitchLoggedUser } from '@wailsapp/go/main/App'; -import { helix } from '@wailsapp/go/models'; -import { BrowserOpenURL } from '@wailsapp/runtime/runtime'; -import React, { useEffect, useState } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import eventsubTests from '~/data/eventsub-tests'; -import { useModule, useStatus } from '~/lib/react'; -import { useAppDispatch, useAppSelector } from '~/store'; -import apiReducer, { modules } from '~/store/api/reducer'; -import { checkTwitchKeys } from '~/lib/twitch'; -import BrowserLink from '../components/BrowserLink'; -import DefinitionTable from '../components/DefinitionTable'; -import RevealLink from '../components/utils/RevealLink'; -import SaveButton from '../components/forms/SaveButton'; -import { - Button, - ButtonGroup, - Checkbox, - CheckboxIndicator, - Field, - FlexRow, - InputBox, - Label, - PageContainer, - PageHeader, - PageTitle, - PasswordInputBox, - SectionHeader, - styled, - TabButton, - TabContainer, - TabContent, - TabList, - TextBlock, -} from '../theme'; -import AlertContent from '../components/AlertContent'; -import { Alert } from '../theme/alert'; - -const StepList = styled('ul', { - lineHeight: '1.5', - listStyleType: 'none', - listStylePosition: 'outside', -}); -const Step = styled('li', { - marginBottom: '0.5rem', - paddingLeft: '1rem', - '&::marker': { - color: '$teal11', - content: '▧', - display: 'inline-block', - marginLeft: '-0.5rem', - }, -}); - -type TestResult = { open: boolean; error?: Error }; - -function TwitchAPISettings() { - const { t } = useTranslation(); - const [httpConfig] = useModule(modules.httpConfig); - const [twitchConfig, setTwitchConfig, loadStatus] = useModule( - modules.twitchConfig, - ); - const status = useStatus(loadStatus.save); - 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, - ); - setTestResult({ open: true }); - } catch (e: unknown) { - setTestResult({ open: true, error: e as Error }); - } - } - setTesting(false); - }; - - return ( -
{ - void dispatch(setTwitchConfig(twitchConfig)); - ev.preventDefault(); - }} - > - - {t('pages.twitch-settings.api-subheader')} - - {t('pages.twitch-settings.apiguide-1')} - - - - {' '} - - 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, - }), - ) - } - /> - - - - - - { - 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' }); - -async function startAuthFlow(target: string) { - const url = await GetTwitchAuthURL(target); - BrowserOpenURL(url); -} - -function TwitchUserBlock({ authKey }: { authKey: string }) { - const { t } = useTranslation(); - const [user, setUser] = useState(null); - const kv = useAppSelector((state) => state.api.client); - - const getUserInfo = async () => { - try { - const res = await GetTwitchLoggedUser(authKey); - setUser(res); - } catch (e) { - console.error(e); - setUser({ ok: false, error: (e as Error).message }); - } - }; - - useEffect(() => { - // Get user info - void getUserInfo(); - - const onKeyChange = () => { - void getUserInfo(); - }; - void kv.subscribeKey(authKey, onKeyChange); - return () => { - void kv.unsubscribeKey(authKey, onKeyChange); - }; - }, []); - - if (user !== null) { - if ('id' in user) { - return ( - - - {t('pages.twitch-settings.events.authenticated-as')} - - - {user.display_name} - - ); - } - return {t('pages.twitch-settings.events.err-no-user')}; - } - - return {t('pages.twitch-settings.events.loading-data')}; -} - -function TwitchEventSubSettings() { - const { t } = useTranslation(); - const kv = useAppSelector((state) => state.api.client); - - const sendFakeEvent = async (event: keyof typeof eventsubTests) => { - const data = eventsubTests[event]; - await kv.putJSON(`twitch/ev/eventsub-event/${event}`, { - ...data, - subscription: { - ...data.subscription, - created_at: new Date().toISOString(), - }, - date: new Date().toISOString(), - }); - }; - - return ( - <> - {t('pages.twitch-settings.events.auth-message')} - - - {t('pages.twitch-settings.events.current-status')} - - - - {t('pages.twitch-settings.events.sim-events')} - - - {Object.keys(eventsubTests).map((ev: keyof typeof eventsubTests) => ( - - ))} - - - ); -} - -function TwitchChatSettings() { - const [chatConfig, setChatConfig, loadStatus] = useModule( - modules.twitchChatConfig, - ); - const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig); - const status = useStatus(loadStatus.save); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const disabled = status?.type === 'pending'; - - return ( -
{ - void dispatch(setTwitchConfig(twitchConfig)); - void dispatch(setChatConfig(chatConfig)); - ev.preventDefault(); - }} - > - - {t('pages.twitch-settings.bot-chat-header')} - - - - - dispatch( - apiReducer.actions.twitchChatConfigChanged({ - ...chatConfig, - command_cooldown: parseInt(ev.target.value, 10), - }), - ) - } - /> - - - - ); -} - -export default function TwitchSettingsPage(): React.ReactElement { - const { t } = useTranslation(); - const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig); - const dispatch = useAppDispatch(); - - const active = twitchConfig?.enabled ?? false; - - return ( - - - {t('pages.twitch-settings.title')} - {t('pages.twitch-settings.subtitle')} - - - { - void dispatch( - setTwitchConfig({ - ...twitchConfig, - enabled: !!ev, - }), - ); - }} - id="enable" - > - {active && } - - - - - - -
- - - - {t('pages.twitch-settings.api-configuration')} - - - {t('pages.twitch-settings.eventsub')} - - - {t('pages.twitch-settings.chat-settings')} - - - - - - - - - - - - -
-
- ); -} diff --git a/frontend/src/ui/pages/TwitchSettings/Page.tsx b/frontend/src/ui/pages/TwitchSettings/Page.tsx new file mode 100644 index 0000000..cac5e38 --- /dev/null +++ b/frontend/src/ui/pages/TwitchSettings/Page.tsx @@ -0,0 +1,85 @@ +import { CheckIcon } from '@radix-ui/react-icons'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useModule } from '~/lib/react'; +import { useAppDispatch } from '~/store'; +import { modules } from '~/store/api/reducer'; +import { + Checkbox, + CheckboxIndicator, + Field, + FlexRow, + Label, + PageContainer, + PageHeader, + PageTitle, + TabButton, + TabContainer, + TabContent, + TabList, + TextBlock, +} from '../../theme'; +import TwitchAPISettings from './TwitchAPISettings'; +import TwitchEventSubSettings from './TwitchEventSubSettings'; +import TwitchChatSettings from './TwitchChatSettings'; + +export default function TwitchSettingsPage(): React.ReactElement { + const { t } = useTranslation(); + const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig); + const dispatch = useAppDispatch(); + + const active = twitchConfig?.enabled ?? false; + + return ( + + + {t('pages.twitch-settings.title')} + {t('pages.twitch-settings.subtitle')} + + + { + void dispatch( + setTwitchConfig({ + ...twitchConfig, + enabled: !!ev, + }), + ); + }} + id="enable" + > + {active && } + + + + + + +
+ + + + {t('pages.twitch-settings.api-configuration')} + + + {t('pages.twitch-settings.eventsub')} + + + {t('pages.twitch-settings.chat-settings')} + + + + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/ui/pages/TwitchSettings/TwitchAPISettings.tsx b/frontend/src/ui/pages/TwitchSettings/TwitchAPISettings.tsx new file mode 100644 index 0000000..ce71e58 --- /dev/null +++ b/frontend/src/ui/pages/TwitchSettings/TwitchAPISettings.tsx @@ -0,0 +1,195 @@ +import { useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useModule, useStatus } from '~/lib/react'; +import { useAppDispatch } from '~/store'; +import apiReducer, { modules } from '~/store/api/reducer'; +import { checkTwitchKeys } from '~/lib/twitch'; +import BrowserLink from '../../components/BrowserLink'; +import DefinitionTable from '../../components/DefinitionTable'; +import RevealLink from '../../components/utils/RevealLink'; +import SaveButton from '../../components/forms/SaveButton'; +import { + Button, + ButtonGroup, + Field, + InputBox, + Label, + PasswordInputBox, + SectionHeader, + styled, + TextBlock, +} from '../../theme'; +import AlertContent from '../../components/AlertContent'; +import { Alert } from '../../theme/alert'; + +const StepList = styled('ul', { + lineHeight: '1.5', + listStyleType: 'none', + listStylePosition: 'outside', +}); +const Step = styled('li', { + marginBottom: '0.5rem', + paddingLeft: '1rem', + '&::marker': { + color: '$teal11', + content: '▧', + display: 'inline-block', + marginLeft: '-0.5rem', + }, +}); + +type TestResult = { open: boolean; error?: Error }; + +export default function TwitchAPISettings() { + const { t } = useTranslation(); + const [httpConfig] = useModule(modules.httpConfig); + const [twitchConfig, setTwitchConfig, loadStatus] = useModule( + modules.twitchConfig, + ); + const status = useStatus(loadStatus.save); + 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, + ); + setTestResult({ open: true }); + } catch (e: unknown) { + setTestResult({ open: true, error: e as Error }); + } + } + setTesting(false); + }; + + return ( +
{ + void dispatch(setTwitchConfig(twitchConfig)); + ev.preventDefault(); + }} + > + + {t('pages.twitch-settings.api-subheader')} + + {t('pages.twitch-settings.apiguide-1')} + + + + {' '} + + 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, + }), + ) + } + /> + + + + + + { + setTestResult({ ...testResult, open: val }); + }} + > + { + setTestResult({ ...testResult, open: false }); + }} + /> + +
+ ); +} diff --git a/frontend/src/ui/pages/TwitchSettings/TwitchChatSettings.tsx b/frontend/src/ui/pages/TwitchSettings/TwitchChatSettings.tsx new file mode 100644 index 0000000..fafb913 --- /dev/null +++ b/frontend/src/ui/pages/TwitchSettings/TwitchChatSettings.tsx @@ -0,0 +1,54 @@ +import { useTranslation } from 'react-i18next'; +import { useModule, useStatus } from '~/lib/react'; +import { useAppDispatch } from '~/store'; +import apiReducer, { modules } from '~/store/api/reducer'; +import SaveButton from '../../components/forms/SaveButton'; +import { Field, InputBox, Label, SectionHeader } from '../../theme'; + +export default function TwitchChatSettings() { + const [chatConfig, setChatConfig, loadStatus] = useModule( + modules.twitchChatConfig, + ); + const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig); + const status = useStatus(loadStatus.save); + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const disabled = status?.type === 'pending'; + + return ( +
{ + void dispatch(setTwitchConfig(twitchConfig)); + void dispatch(setChatConfig(chatConfig)); + ev.preventDefault(); + }} + > + + {t('pages.twitch-settings.bot-chat-header')} + + + + + dispatch( + apiReducer.actions.twitchChatConfigChanged({ + ...chatConfig, + command_cooldown: parseInt(ev.target.value, 10), + }), + ) + } + /> + + + + ); +} diff --git a/frontend/src/ui/pages/TwitchSettings/TwitchEventSubSettings.tsx b/frontend/src/ui/pages/TwitchSettings/TwitchEventSubSettings.tsx new file mode 100644 index 0000000..8599084 --- /dev/null +++ b/frontend/src/ui/pages/TwitchSettings/TwitchEventSubSettings.tsx @@ -0,0 +1,57 @@ +import { ExternalLinkIcon } from '@radix-ui/react-icons'; +import { useTranslation } from 'react-i18next'; +import eventsubTests from '~/data/eventsub-tests'; +import { useAppSelector } from '~/store'; +import { startAuthFlow } from '~/lib/twitch'; +import { Button, ButtonGroup, SectionHeader, TextBlock } from '../../theme'; +import TwitchUserBlock from '../../components/TwitchUserBlock'; + +export default function TwitchEventSubSettings() { + const { t } = useTranslation(); + const kv = useAppSelector((state) => state.api.client); + + const sendFakeEvent = async (event: keyof typeof eventsubTests) => { + const data = eventsubTests[event]; + await kv.putJSON(`twitch/ev/eventsub-event/${event}`, { + ...data, + subscription: { + ...data.subscription, + created_at: new Date().toISOString(), + }, + date: new Date().toISOString(), + }); + }; + + return ( + <> + {t('pages.twitch-settings.events.auth-message')} + + + {t('pages.twitch-settings.events.current-status')} + + + + {t('pages.twitch-settings.events.sim-events')} + + + {Object.keys(eventsubTests).map((ev: keyof typeof eventsubTests) => ( + + ))} + + + ); +}