Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sweets/spotify #10

Merged
merged 12 commits into from
Oct 17, 2023
24 changes: 20 additions & 4 deletions components/PowerUpsButton/PowerUpsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,39 @@ import { useConnectModal } from "@rainbow-me/rainbowkit"
import useBalanceOf from "../../hooks/useBalanceOf"
import useZoraMint from "../../hooks/useZoraMint"
import Spinner from "../Spinner"
import Button from "../Button"
import { useSpotifyProvider } from "../../providers/SpotifyProvider"

const PowerUpsButton = ({ onClick }) => {
const [clicked, setClicked] = useState(false)
const { mintWithRewards } = useZoraMint()
const { balance, fetchBalance, cameraCount, moneyCount, heartCount } = useBalanceOf()
const { isConnected } = useAccount()
const { openConnectModal } = useConnectModal()
const { deviceId, login } = useSpotifyProvider()
const channel = new MessageChannel()

function callGodotFunction() {
const iframe = document.querySelector("#godotGame") as HTMLIFrameElement
if (!iframe) {
return
}
iframe.contentWindow.postMessage([heartCount, cameraCount, moneyCount], "*", [channel.port2])
const spotifyMoney = deviceId ? 1 : 0
iframe.contentWindow.postMessage([heartCount, cameraCount, moneyCount + spotifyMoney], "*", [
channel.port2,
])
}

const handleClick = async () => {
if (!deviceId) {
login()
}

if (!isConnected) {
openConnectModal()
return
}

if (_.isNull(balance)) return
setClicked(true)
if (balance === 0) {
Expand All @@ -50,9 +61,14 @@ const PowerUpsButton = ({ onClick }) => {
}, [balance, isConnected, heartCount, moneyCount, cameraCount])

return (
<button onClick={handleClick} type="button" className="text-xs md:text-xl text-white">
{clicked ? <Spinner /> : "play with power-ups"}
</button>
<Button
id="power-up"
onClick={handleClick}
type="button"
className="text-lg md:text-2xl pb-4 md:pb-8"
>
{clicked ? <Spinner /> : `play with power-ups`}
</Button>
)
}

Expand Down
4 changes: 2 additions & 2 deletions components/StartModal/StartModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const StartModal = ({ handleClick, children }) => (
bg-black"
>
<div className="flex flex-col items-center gap-2">
<div className="text-xl md:text-4xl text-white uppercase">Hypersurveilled</div>
<div className="text-lg md:text-xl text-white uppercase">by Heno</div>
<div className="text-xl md:text-4xl text-white uppercase">Relief</div>
<div className="text-lg md:text-xl text-white uppercase">by Heno.</div>
</div>
<Button className="text-xl md:text-4xl pb-4 md:pb-8" id="play-button" onClick={handleClick}>
Start Game
Expand Down
6 changes: 6 additions & 0 deletions lib/consts.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { zora, zoraTestnet } from "@wagmi/core/chains"

export const CHAIN_ID = process.env.NEXT_PUBLIC_TESTNET ? zoraTestnet.id : zora.id
export const SPOTIFY_CLIENT_ID = process.env.NEXT_PUBLIC_CLIENT_ID
export const SPOTIFY_CLIENT_SECRET = process.env.NEXT_PUBLIC_CLIENT_SECRET
export const SPOTIFY_REDIRECT_URI = process.env.NEXT_PUBLIC_REDIRECT_URI
export const SPOTIFY_STATE_KEY = "spotify_auth_state"
export const RELIEF_TRACK_ID = "5aDNHHNXc16VktqV1gSq23"
export const HENO_ARTIST_ID = "3mr6jeVpPIXBp8IMMb60aD"
27 changes: 27 additions & 0 deletions lib/spotify/createPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-disable camelcase */
const createPlayer = (accessToken, { onReady }) => {
const script = document.createElement("script")
script.src = "https://sdk.scdn.co/spotify-player.js"
script.async = true

document.body.appendChild(script)

const anyWin = window as any
anyWin.onSpotifyWebPlaybackSDKReady = () => {
const newPlayer = new anyWin.Spotify.Player({
name: "Web Playback SDK",
getOAuthToken: (cb) => {
cb(accessToken)
},
volume: 0.5,
})

newPlayer.addListener("ready", ({ device_id }) => {
onReady(device_id)
})

newPlayer.connect()
}
}

export default createPlayer
14 changes: 14 additions & 0 deletions lib/spotify/followArtist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { HENO_ARTIST_ID } from "../consts"

const followArtist = async (accessToken) => {
await fetch(`https://api.spotify.com/v1/me/following?type=artist&ids=${HENO_ARTIST_ID}`, {
method: "PUT",
body: JSON.stringify({ ids: [HENO_ARTIST_ID] }),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
})
}

export default followArtist
16 changes: 16 additions & 0 deletions lib/spotify/generateCodeChallenge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const generateCodeChallenge = async (codeVerifier) => {
function base64encode(string) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(string)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "")
}

const encoder = new TextEncoder()
const data = encoder.encode(codeVerifier)
const digest = await window.crypto.subtle.digest("SHA-256", data)

return base64encode(digest)
}

export default generateCodeChallenge
11 changes: 11 additions & 0 deletions lib/spotify/generateRandomString.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const generateRandomString = (length) => {
let text = ""
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"

for (let i = 0; i < length; i += 1) {
text += possible.charAt(Math.floor(Math.random() * possible.length))
}
return text
}

export default generateRandomString
36 changes: 36 additions & 0 deletions lib/spotify/getAccessToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { SPOTIFY_CLIENT_ID, SPOTIFY_REDIRECT_URI } from "../consts"

const getAccessToken = async (code) => {
const codeVerifier = localStorage.getItem("code_verifier")
const body = new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: SPOTIFY_REDIRECT_URI,
client_id: SPOTIFY_CLIENT_ID,
code_verifier: codeVerifier,
})
return fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body,
})
.then(async (response) => {
if (!response.ok) {
// eslint-disable-next-line no-console
console.error(response)
}
return response.json()
})
.then(async (data) => {
localStorage.setItem("access_token", data.access_token)
return data.access_token
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error("Error:", error)
})
}

export default getAccessToken
31 changes: 31 additions & 0 deletions lib/spotify/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SPOTIFY_CLIENT_ID, SPOTIFY_REDIRECT_URI } from "../consts"
import generateCodeChallenge from "./generateCodeChallenge"
import generateRandomString from "./generateRandomString"

const login = () => {
const clientId = SPOTIFY_CLIENT_ID
const redirectUri = SPOTIFY_REDIRECT_URI

const codeVerifier = generateRandomString(128)

generateCodeChallenge(codeVerifier).then((codeChallenge) => {
const state = generateRandomString(16)
const scope = "user-read-private user-read-email streaming"

localStorage.setItem("code_verifier", codeVerifier)

const args = new URLSearchParams({
response_type: "code",
client_id: clientId,
scope,
redirect_uri: redirectUri,
state,
code_challenge_method: "S256",
code_challenge: codeChallenge,
})

window.location = `https://accounts.spotify.com/authorize?${args}` as any
})
}

export default login
14 changes: 14 additions & 0 deletions lib/spotify/playTrack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { RELIEF_TRACK_ID } from "../consts"

const playTrack = async (accessToken, deviceId) => {
await fetch(`https://api.spotify.com/v1/me/player/play?device_id=${deviceId}`, {
method: "PUT",
body: JSON.stringify({ uris: [`spotify:track:${RELIEF_TRACK_ID}`] }),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
})
}

export default playTrack
14 changes: 14 additions & 0 deletions lib/spotify/saveTrack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { RELIEF_TRACK_ID } from "../consts"

const saveTrack = async (accessToken) => {
await fetch(`https://api.spotify.com/v1/me/tracks?ids=${RELIEF_TRACK_ID}`, {
method: "PUT",
body: JSON.stringify({ ids: [RELIEF_TRACK_ID] }),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
})
}

export default saveTrack
7 changes: 6 additions & 1 deletion pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import GamePage from "../components/Pages/GamePage"
import SpotifyProvider from "../providers/SpotifyProvider"

const Game = () => <GamePage />
const Game = () => (
<SpotifyProvider>
<GamePage />
</SpotifyProvider>
)

export default Game
53 changes: 53 additions & 0 deletions providers/SpotifyProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useRouter } from "next/router"
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
import playTrack from "../lib/spotify/playTrack"
import followArtist from "../lib/spotify/followArtist"
import saveTrack from "../lib/spotify/saveTrack"
import getAccessToken from "../lib/spotify/getAccessToken"
import createPlayer from "../lib/spotify/createPlayer"
import login from "../lib/spotify/login"

const SpotifyContext = createContext(null)

const SpotifyProvider = ({ children }) => {
const [accessToken, setAccessToken] = useState(null)
const [deviceId, setDeviceId] = useState(null)
const { query } = useRouter()

const playSong = useCallback(async () => {
await playTrack(accessToken, deviceId)
await followArtist(accessToken)
await saveTrack(accessToken)
}, [accessToken, deviceId])

const onReady = (deviceIdentity) => {
setDeviceId(deviceIdentity)
}

useEffect(() => {
if (!accessToken) return
createPlayer(accessToken, { onReady })
}, [accessToken])

useEffect(() => {
const init = async () => {
const response = await getAccessToken(query.code)
setAccessToken(response)
}
init()
}, [query])

const value = useMemo(() => ({ deviceId, login, playSong }), [deviceId, playSong])

return <SpotifyContext.Provider value={value}>{children}</SpotifyContext.Provider>
}

export const useSpotifyProvider = () => {
const context = useContext(SpotifyContext)
if (!context) {
throw new Error("useSpotifyProvider must be used within a SpotifyProvider")
}
return context
}

export default SpotifyProvider
2 changes: 1 addition & 1 deletion public/game/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
<script type='text/javascript' src='v6.js'></script>
<script type='text/javascript'>//<![CDATA[

const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"executable":"v6","experimentalVK":false,"fileSizes":{"v6.pck":11950016,"v6.wasm":17867504},"focusCanvas":true,"gdnativeLibs":[]};
const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"executable":"v6","experimentalVK":false,"fileSizes":{"v6.pck":11955952,"v6.wasm":17867504},"focusCanvas":true,"gdnativeLibs":[]};
var engine = new Engine(GODOT_CONFIG);

(function() {
Expand Down
Binary file modified public/game/v6.pck
Binary file not shown.
Loading