Skip to content

Commit

Permalink
chore(create-robo): updated react templates
Browse files Browse the repository at this point in the history
Thanks to Matt for detailing how React treats these frames, I was able to rewrite these templates in a much nicer and more stable way that doesn't get stuck at `.ready()` upon HMR changes!

discord/embedded-app-sdk#41
  • Loading branch information
Pkmmte committed Apr 18, 2024
1 parent b230b67 commit c1d475e
Show file tree
Hide file tree
Showing 15 changed files with 540 additions and 223 deletions.
5 changes: 5 additions & 0 deletions .changeset/shiny-wolves-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-robo': patch
---

chore: updated react templates
4 changes: 2 additions & 2 deletions packages/create-robo/templates/app-js-react/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello, World</title>
<title>Hello, World | Powered by Robo.js</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app/main.jsx"></script>
<script type="module" src="/src/app/index.jsx"></script>
</body>
</html>
3 changes: 0 additions & 3 deletions packages/create-robo/templates/app-js-react/src/api/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ export default async (req) => {
code: req.body.code
})
})

// Retrieve the access_token from the response
const { access_token } = await response.json()

// Return the access_token to our client as { access_token: "..."}
return { access_token }
}
37 changes: 37 additions & 0 deletions packages/create-robo/templates/app-js-react/src/app/Activity.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect, useState } from 'react'
import { useDiscordSdk } from '../hooks/useDiscordSdk'

export const Activity = () => {
const { authenticated, discordSdk, status } = useDiscordSdk()
const [channelName, setChannelName] = useState()

useEffect(() => {
// Requesting the channel in GDMs (when the guild ID is null) requires
// the dm_channels.read scope which requires Discord approval.
if (!authenticated || !discordSdk.channelId || !discordSdk.guildId) {
return
}

// Collect channel info over RPC
// Enable authentication to see it! (App.jsx)
discordSdk.commands.getChannel({ channel_id: discordSdk.channelId }).then((channel) => {
if (channel.name) {
setChannelName(channel.name)
}
})
}, [authenticated, discordSdk])

return (
<div>
<img src="/rocket.png" className="logo" alt="Discord" />
<h1>Hello, World</h1>
{ channelName
? <h3>#{channelName}</h3>
: <h3>{status}</h3>
}
<small>
Powered by <strong>Robo.js</strong>
</small>
</div>
)
}
32 changes: 16 additions & 16 deletions packages/create-robo/templates/app-js-react/src/app/App.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { DiscordContextProvider } from '../hooks/useDiscordSdk'
import { Activity } from './Activity'
import './App.css'
import { useDiscordSdk } from './discord-sdk'
// import { useDiscordAuth, useDiscordSdk } from './discord-sdk'

/**
* 🔒 Set `authenticate` to true to enable Discord authentication
* You can also set the `scope` prop to request additional permissions
*
* Example:
* ```
* <DiscordContextProvider authenticate scope={['identify', 'guilds']}>
* <Activity />
* </DiscordContextProvider>
* ```
*/
export default function App() {
const { ready } = useDiscordSdk()
console.log(`SDK Ready:`, ready)

// 🔒 Replace useDiscordSdk with this to enable authentication
// const { authenticated } = useDiscordAuth()
// console.log(`Authenticated:`, authenticated)

return (
<div>
<img src="/rocket.png" className="logo" alt="Discord" />
<h1>Hello, World</h1>
<small>
Powered by <strong>Robo.js</strong>
</small>
</div>
<DiscordContextProvider>
<Activity />
</DiscordContextProvider>
)
}
90 changes: 0 additions & 90 deletions packages/create-robo/templates/app-js-react/src/app/discord-sdk.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { DiscordSDK, DiscordSDKMock } from '@discord/embedded-app-sdk'
import { useState, useEffect, useCallback, useRef, createContext, useContext } from 'react'

const queryParams = new URLSearchParams(window.location.search)
const isEmbedded = queryParams.get('frame_id') != null

let discordSdk

if (isEmbedded) {
discordSdk = new DiscordSDK(import.meta.env.VITE_DISCORD_CLIENT_ID)
} else {
// We're using session storage for user_id, guild_id, and channel_id
// This way the user/guild/channel will be maintained until the tab is closed, even if you refresh
// Session storage will generate new unique mocks for each tab you open
// Any of these values can be overridden via query parameters
// i.e. if you set https://my-tunnel-url.com/?user_id=test_user_id
// this will override this will override the session user_id value
const mockUserId = getOverrideOrRandomSessionValue('user_id')
const mockGuildId = getOverrideOrRandomSessionValue('guild_id')
const mockChannelId = getOverrideOrRandomSessionValue('channel_id')

discordSdk = new DiscordSDKMock(import.meta.env.VITE_DISCORD_CLIENT_ID, mockGuildId, mockChannelId)
const discriminator = String(mockUserId.charCodeAt(0) % 5)

discordSdk._updateCommandMocks({
authenticate: async () => {
return {
access_token: 'mock_token',
user: {
username: mockUserId,
discriminator,
id: mockUserId,
avatar: null,
public_flags: 1
},
scopes: [],
expires: new Date(2112, 1, 1).toString(),
application: {
description: 'mock_app_description',
icon: 'mock_app_icon',
id: 'mock_app_id',
name: 'mock_app_name'
}
}
}
})
}

export { discordSdk }

function getOverrideOrRandomSessionValue(queryParam) {
const overrideValue = queryParams.get(queryParam)
if (overrideValue != null) {
return overrideValue
}

const currentStoredValue = sessionStorage.getItem(queryParam)
if (currentStoredValue != null) {
return currentStoredValue
}

// Set queryParam to a random 8-character string
const randomString = Math.random().toString(36).slice(2, 10)
sessionStorage.setItem(queryParam, randomString)
return randomString
}

const DiscordContext = createContext({
accessToken: null,
authenticated: false,
discordSdk: discordSdk,
error: null,
session: {
user: {
id: '',
username: '',
discriminator: '',
avatar: null,
public_flags: 0
},
access_token: '',
scopes: [],
expires: '',
application: {
rpc_origins: undefined,
id: '',
name: '',
icon: null,
description: ''
}
},
status: 'pending'
})

export function DiscordContextProvider(props) {
const { authenticate, children, loadingScreen = null, scope } = props
const setupResult = useDiscordSdkSetup({ authenticate, scope })

if (loadingScreen && !['error', 'ready'].includes(setupResult.status)) {
return <>{loadingScreen}</>
}

return <DiscordContext.Provider value={setupResult}>{children}</DiscordContext.Provider>
}

export function useDiscordSdk() {
return useContext(DiscordContext)
}

/**
* Authenticate with Discord and return the access token.
* See full list of scopes: https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes
*
* @param scope The scope of the authorization (default: ['identify', 'guilds'])
* @returns The result of the Discord SDK `authenticate()` command
*/
export async function authenticateSdk(options) {
const { scope = ['identify', 'guilds'] } = options ?? {}

await discordSdk.ready()
const { code } = await discordSdk.commands.authorize({
client_id: import.meta.env.VITE_DISCORD_CLIENT_ID,
response_type: 'code',
state: '',
prompt: 'none',
scope: scope
})

const response = await fetch('/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ code })
})

const { access_token } = await response.json()

// Authenticate with Discord client (using the access_token)
const auth = await discordSdk.commands.authenticate({ access_token })

if (auth == null) {
throw new Error('Authenticate command failed')
}
return { accessToken: access_token, auth }
}

export function useDiscordSdkSetup(options) {
const { authenticate } = options ?? {}
const [accessToken, setAccessToken] = useState<string | null>(null)
const [session, setSession] = useState<DiscordSession | null>(null)
const [error, setError] = useState<string | null>(null)
const [status, setStatus] = useState<'authenticating' | 'error' | 'loading' | 'pending' | 'ready'>('pending')

const setupDiscordSdk = useCallback(async () => {
try {
setStatus('loading')
await discordSdk.ready()

if (authenticate) {
setStatus('authenticating')
const { accessToken, auth } = await authenticateSdk()
setAccessToken(accessToken)
setSession(auth)
}

setStatus('ready')
} catch (e) {
console.error(e)
if (e instanceof Error) {
setError(e.message)
} else {
setError('An unknown error occurred')
}
setStatus('error')
}
}, [authenticate])

useStableEffect(() => {
setupDiscordSdk()
})

return { accessToken, authenticated: !!accessToken, discordSdk, error, session, status }
}

/**
* React in development mode re-mounts the root component initially.
* This hook ensures that the callback is only called once, preventing double authentication.
*/
function useStableEffect(callback) {
const isRunning = useRef(false)

useEffect(() => {
if (!isRunning.current) {
isRunning.current = true
callback()
}
}, [])
}
Loading

0 comments on commit c1d475e

Please sign in to comment.