From a2d61a1daa2b3276c8e1d60682a0161db8d54593 Mon Sep 17 00:00:00 2001 From: mantikoros <95266179+mantikoros@users.noreply.github.com> Date: Wed, 14 Sep 2022 03:52:31 -0500 Subject: [PATCH] Twitch integration (#815) * twitch account linking; profile page twitch panel; twitch landing page * fix import * twitch logo * save twitch credentials cloud function * use user id instead of bot id, add manifold api endpoint * properly add function to index * Added support for new redirect Twitch auth. * Added clean error handling in case of Twitch link fail. * remove simulator * Removed legacy non-redirect Twitch auth code. Added "add bot to channel" button in user profile and relevant data to user type. * Removed unnecessary imports. * Fixed line endings. * Allow users to modify private user twitchInfo firestore object * Local dev on savetwitchcredentials function Co-authored-by: Phil Co-authored-by: Marshall Polaris --- common/user.ts | 5 + firestore.rules | 4 +- functions/src/index.ts | 3 + functions/src/save-twitch-credentials.ts | 22 ++++ functions/src/serve.ts | 2 + web/components/profile/twitch-panel.tsx | 133 +++++++++++++++++++++++ web/lib/api/api-key.ts | 9 ++ web/lib/twitch/link-twitch-account.ts | 41 +++++++ web/pages/api/v0/twitch/save.ts | 23 ++++ web/pages/profile.tsx | 17 ++- web/pages/twitch.tsx | 120 ++++++++++++++++++++ web/public/twitch-logo.png | Bin 0 -> 23022 bytes 12 files changed, 367 insertions(+), 12 deletions(-) create mode 100644 functions/src/save-twitch-credentials.ts create mode 100644 web/components/profile/twitch-panel.tsx create mode 100644 web/lib/api/api-key.ts create mode 100644 web/lib/twitch/link-twitch-account.ts create mode 100644 web/pages/api/v0/twitch/save.ts create mode 100644 web/pages/twitch.tsx create mode 100644 web/public/twitch-logo.png diff --git a/common/user.ts b/common/user.ts index f8b4f8d8..5d427744 100644 --- a/common/user.ts +++ b/common/user.ts @@ -68,6 +68,11 @@ export type PrivateUser = { /** @deprecated - use notificationSubscriptionTypes */ notificationPreferences?: notification_subscribe_types notificationSubscriptionTypes: notification_subscription_types + twitchInfo?: { + twitchName: string + controlToken: string + botEnabled?: boolean + } } export type notification_destination_types = 'email' | 'browser' diff --git a/firestore.rules b/firestore.rules index d24d4097..82392787 100644 --- a/firestore.rules +++ b/firestore.rules @@ -77,7 +77,7 @@ service cloud.firestore { allow read: if userId == request.auth.uid || isAdmin(); allow update: if (userId == request.auth.uid || isAdmin()) && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences', 'unsubscribedFromWeeklyTrendingEmails','notificationSubscriptionTypes' ]); + .hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences', 'unsubscribedFromWeeklyTrendingEmails', 'notificationSubscriptionTypes', 'twitchInfo']); } match /private-users/{userId}/views/{viewId} { @@ -161,7 +161,7 @@ service cloud.firestore { && request.resource.data.diff(resource.data).affectedKeys() .hasOnly(['isSeen', 'viewTime']); } - + match /{somePath=**}/groupMembers/{memberId} { allow read; } diff --git a/functions/src/index.ts b/functions/src/index.ts index be73b6af..adfee75e 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -71,6 +71,7 @@ import { stripewebhook, createcheckoutsession } from './stripe' import { getcurrentuser } from './get-current-user' import { acceptchallenge } from './accept-challenge' import { createpost } from './create-post' +import { savetwitchcredentials } from './save-twitch-credentials' const toCloudFunction = ({ opts, handler }: EndpointDefinition) => { return onRequest(opts, handler as any) @@ -96,6 +97,7 @@ const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession) const getCurrentUserFunction = toCloudFunction(getcurrentuser) const acceptChallenge = toCloudFunction(acceptchallenge) const createPostFunction = toCloudFunction(createpost) +const saveTwitchCredentials = toCloudFunction(savetwitchcredentials) export { healthFunction as health, @@ -119,4 +121,5 @@ export { getCurrentUserFunction as getcurrentuser, acceptChallenge as acceptchallenge, createPostFunction as createpost, + saveTwitchCredentials as savetwitchcredentials } diff --git a/functions/src/save-twitch-credentials.ts b/functions/src/save-twitch-credentials.ts new file mode 100644 index 00000000..80dc86a6 --- /dev/null +++ b/functions/src/save-twitch-credentials.ts @@ -0,0 +1,22 @@ +import * as admin from 'firebase-admin' +import { z } from 'zod' + +import { newEndpoint, validate } from './api' + +const bodySchema = z.object({ + twitchInfo: z.object({ + twitchName: z.string(), + controlToken: z.string(), + }), +}) + + +export const savetwitchcredentials = newEndpoint({}, async (req, auth) => { + const { twitchInfo } = validate(bodySchema, req.body) + const userId = auth.uid + + await firestore.doc(`private-users/${userId}`).update({ twitchInfo }) + return { success: true } +}) + +const firestore = admin.firestore() diff --git a/functions/src/serve.ts b/functions/src/serve.ts index a5291f19..6d062d40 100644 --- a/functions/src/serve.ts +++ b/functions/src/serve.ts @@ -27,6 +27,7 @@ import { unsubscribe } from './unsubscribe' import { stripewebhook, createcheckoutsession } from './stripe' import { getcurrentuser } from './get-current-user' import { createpost } from './create-post' +import { savetwitchcredentials } from './save-twitch-credentials' type Middleware = (req: Request, res: Response, next: NextFunction) => void const app = express() @@ -65,6 +66,7 @@ addJsonEndpointRoute('/resolvemarket', resolvemarket) addJsonEndpointRoute('/unsubscribe', unsubscribe) addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession) addJsonEndpointRoute('/getcurrentuser', getcurrentuser) +addJsonEndpointRoute('/savetwitchcredentials', savetwitchcredentials) addEndpointRoute('/stripewebhook', stripewebhook, express.raw()) addEndpointRoute('/createpost', createpost) diff --git a/web/components/profile/twitch-panel.tsx b/web/components/profile/twitch-panel.tsx new file mode 100644 index 00000000..b284b242 --- /dev/null +++ b/web/components/profile/twitch-panel.tsx @@ -0,0 +1,133 @@ +import clsx from 'clsx' +import { MouseEventHandler, ReactNode, useState } from 'react' +import toast from 'react-hot-toast' + +import { LinkIcon } from '@heroicons/react/solid' +import { usePrivateUser, useUser } from 'web/hooks/use-user' +import { updatePrivateUser } from 'web/lib/firebase/users' +import { track } from 'web/lib/service/analytics' +import { linkTwitchAccountRedirect } from 'web/lib/twitch/link-twitch-account' +import { copyToClipboard } from 'web/lib/util/copy' +import { Button, ColorType } from './../button' +import { Row } from './../layout/row' +import { LoadingIndicator } from './../loading-indicator' + +function BouncyButton(props: { + children: ReactNode + onClick?: MouseEventHandler + color?: ColorType +}) { + const { children, onClick, color } = props + return ( + + ) +} + +export function TwitchPanel() { + const user = useUser() + const privateUser = usePrivateUser() + + const twitchInfo = privateUser?.twitchInfo + const twitchName = privateUser?.twitchInfo?.twitchName + const twitchToken = privateUser?.twitchInfo?.controlToken + const twitchBotConnected = privateUser?.twitchInfo?.botEnabled + + const linkIcon =