diff --git a/common/user.ts b/common/user.ts
index e3c9d181..b130be02 100644
--- a/common/user.ts
+++ b/common/user.ts
@@ -63,6 +63,10 @@ export type PrivateUser = {
initialIpAddress?: string
apiKey?: string
notificationPreferences?: notification_subscribe_types
+ twitchInfo?: {
+ twitchName: string
+ controlToken: string
+ }
}
export type notification_subscribe_types = 'all' | 'less' | 'none'
diff --git a/firestore.rules b/firestore.rules
index fe45071b..e3514deb 100644
--- a/firestore.rules
+++ b/firestore.rules
@@ -70,7 +70,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' ]);
+ .hasOnly(['apiKey', 'twitchInfo', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences', 'unsubscribedFromWeeklyTrendingEmails' ]);
}
match /private-users/{userId}/views/{viewId} {
diff --git a/web/components/profile/twitch-panel.tsx b/web/components/profile/twitch-panel.tsx
new file mode 100644
index 00000000..8415a06e
--- /dev/null
+++ b/web/components/profile/twitch-panel.tsx
@@ -0,0 +1,96 @@
+import clsx from 'clsx'
+import React, { useState } from 'react'
+import toast from 'react-hot-toast'
+
+import { copyToClipboard } from 'web/lib/util/copy'
+import { linkTwitchAccount } from 'web/lib/twitch/link-twitch-account'
+import { LinkIcon } from '@heroicons/react/solid'
+import { track } from 'web/lib/service/analytics'
+import { Button } from './../button'
+import { LoadingIndicator } from './../loading-indicator'
+import { Row } from './../layout/row'
+import { usePrivateUser, useUser } from 'web/hooks/use-user'
+
+export function TwitchPanel() {
+ const user = useUser()
+ const privateUser = usePrivateUser()
+
+ const twitchName = privateUser?.twitchInfo?.twitchName
+ const twitchToken = privateUser?.twitchInfo?.controlToken
+
+ const linkIcon =
+
+ const copyOverlayLink = async () => {
+ copyToClipboard(`http://localhost:1000/overlay?t=${twitchToken}`)
+ toast.success('Overlay link copied!', {
+ icon: linkIcon,
+ })
+ }
+
+ const copyDockLink = async () => {
+ copyToClipboard(`http://localhost:1000/dock?t=${twitchToken}`)
+ toast.success('Dock link copied!', {
+ icon: linkIcon,
+ })
+ }
+
+ const [twitchLoading, setTwitchLoading] = useState(false)
+
+ const createLink = async () => {
+ if (!user || !privateUser) return
+ setTwitchLoading(true)
+
+ const promise = linkTwitchAccount(user, privateUser)
+ track('link twitch from profile')
+ await promise
+
+ setTwitchLoading(false)
+ }
+
+ return (
+ <>
+
+
+
+ {!twitchName ? (
+
+
+ {twitchLoading && }
+
+ ) : (
+
+ Linked Twitch account{' '}
+ {twitchName}
+
+ )}
+
+
+ {twitchToken && (
+
+
+
+
+
+
+
+
+ )}
+ >
+ )
+}
diff --git a/web/lib/api/api-key.ts b/web/lib/api/api-key.ts
new file mode 100644
index 00000000..1a8c84c1
--- /dev/null
+++ b/web/lib/api/api-key.ts
@@ -0,0 +1,9 @@
+import { updatePrivateUser } from '../firebase/users'
+
+export const generateNewApiKey = async (userId: string) => {
+ const newApiKey = crypto.randomUUID()
+
+ return await updatePrivateUser(userId, { apiKey: newApiKey })
+ .then(() => newApiKey)
+ .catch(() => undefined)
+}
diff --git a/web/lib/twitch/link-twitch-account.ts b/web/lib/twitch/link-twitch-account.ts
new file mode 100644
index 00000000..d604c9b1
--- /dev/null
+++ b/web/lib/twitch/link-twitch-account.ts
@@ -0,0 +1,47 @@
+import { User, PrivateUser } from 'common/lib/user'
+import { generateNewApiKey } from '../api/api-key'
+import { updatePrivateUser } from '../firebase/users'
+
+const TWITCH_BOT_PUBLIC_URL = 'https://king-prawn-app-5btyw.ondigitalocean.app'
+
+export async function initLinkTwitchAccount(
+ manifoldUserID: string,
+ manifoldUserAPIKey: string
+): Promise<[string, Promise<{ twitchName: string; controlToken: string }>]> {
+ const response = await fetch(`${TWITCH_BOT_PUBLIC_URL}/api/linkInit`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ manifoldID: manifoldUserID,
+ apiKey: manifoldUserAPIKey,
+ }),
+ })
+ const responseData = await response.json()
+ if (!response.ok) {
+ throw new Error(responseData.message)
+ }
+ const responseFetch = fetch(
+ `${TWITCH_BOT_PUBLIC_URL}/api/linkResult?userID=${manifoldUserID}`
+ )
+ return [responseData.twitchAuthURL, responseFetch.then((r) => r.json())]
+}
+
+export async function linkTwitchAccount(user: User, privateUser: PrivateUser) {
+ const apiKey = privateUser.apiKey ?? (await generateNewApiKey(user.id))
+ if (!apiKey) throw new Error("Couldn't retrieve or create Manifold api key")
+
+ const [twitchAuthURL, linkSuccessPromise] = await initLinkTwitchAccount(
+ user.id,
+ apiKey
+ )
+
+ console.log('opening twitch link', twitchAuthURL)
+ window.open(twitchAuthURL)
+
+ const twitchInfo = await linkSuccessPromise
+ await updatePrivateUser(user.id, { twitchInfo })
+
+ console.log(`Successfully linked Twitch account '${twitchInfo.twitchName}'`)
+}
diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx
index ca1f3489..fa384603 100644
--- a/web/pages/profile.tsx
+++ b/web/pages/profile.tsx
@@ -12,15 +12,13 @@ import { uploadImage } from 'web/lib/firebase/storage'
import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row'
import { User, PrivateUser } from 'common/user'
-import {
- getUserAndPrivateUser,
- updateUser,
- updatePrivateUser,
-} from 'web/lib/firebase/users'
+import { getUserAndPrivateUser, updateUser } from 'web/lib/firebase/users'
import { defaultBannerUrl } from 'web/components/user-page'
import { SiteLink } from 'web/components/site-link'
import Textarea from 'react-expanding-textarea'
import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'
+import { generateNewApiKey } from 'web/lib/api/api-key'
+import { TwitchPanel } from 'web/components/profile/twitch-panel'
export const getServerSideProps = redirectIfLoggedOut('/', async (_, creds) => {
return { props: { auth: await getUserAndPrivateUser(creds.user.uid) } }
@@ -96,11 +94,8 @@ export default function ProfilePage(props: {
}
const updateApiKey = async (e: React.MouseEvent) => {
- const newApiKey = crypto.randomUUID()
- setApiKey(newApiKey)
- await updatePrivateUser(user.id, { apiKey: newApiKey }).catch(() => {
- setApiKey(privateUser.apiKey || '')
- })
+ const newApiKey = await generateNewApiKey(user.id)
+ setApiKey(newApiKey ?? '')
e.preventDefault()
}
@@ -242,6 +237,8 @@ export default function ProfilePage(props: {
+
+
diff --git a/web/pages/twitch.tsx b/web/pages/twitch.tsx
new file mode 100644
index 00000000..ba32f748
--- /dev/null
+++ b/web/pages/twitch.tsx
@@ -0,0 +1,115 @@
+import { useState } from 'react'
+
+import { Page } from 'web/components/page'
+import { Col } from 'web/components/layout/col'
+import { ManifoldLogo } from 'web/components/nav/manifold-logo'
+import { useSaveReferral } from 'web/hooks/use-save-referral'
+import { SEO } from 'web/components/SEO'
+import { Spacer } from 'web/components/layout/spacer'
+import { firebaseLogin, getUserAndPrivateUser } from 'web/lib/firebase/users'
+import { track } from 'web/lib/service/analytics'
+import { Row } from 'web/components/layout/row'
+import { Button } from 'web/components/button'
+import { useTracking } from 'web/hooks/use-tracking'
+import { linkTwitchAccount } from 'web/lib/twitch/link-twitch-account'
+import { usePrivateUser, useUser } from 'web/hooks/use-user'
+import { LoadingIndicator } from 'web/components/loading-indicator'
+
+export default function TwitchLandingPage() {
+ useSaveReferral()
+ useTracking('view twitch landing page')
+
+ const user = useUser()
+ const privateUser = usePrivateUser()
+ const twitchUser = privateUser?.twitchInfo?.twitchName
+
+ const callback =
+ user && privateUser
+ ? () => linkTwitchAccount(user, privateUser)
+ : async () => {
+ const result = await firebaseLogin()
+
+ const userId = result.user.uid
+ const { user, privateUser } = await getUserAndPrivateUser(userId)
+ if (!user || !privateUser) return
+
+ await linkTwitchAccount(user, privateUser)
+ }
+
+ const [isLoading, setLoading] = useState(false)
+
+ const getStarted = async () => {
+ setLoading(true)
+
+ const promise = callback()
+ track('twitch page button click')
+ await promise
+
+ setLoading(false)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bet
+ {' '}
+ on your favorite streams
+
+
+
+
+ Get more out of Twitch with play-money betting markets.{' '}
+ {!twitchUser &&
+ 'Click the button below to link your Twitch account.'}
+
+
+
+
+
+
+ {twitchUser ? (
+
+
+
+ Twitch account linked
+
+
+ {twitchUser}
+
+
+
+ ) : isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ )
+}