-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(create-robo): updated react templates
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
Showing
15 changed files
with
540 additions
and
223 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'create-robo': patch | ||
--- | ||
|
||
chore: updated react templates |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
packages/create-robo/templates/app-js-react/src/app/Activity.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
32
packages/create-robo/templates/app-js-react/src/app/App.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
90
packages/create-robo/templates/app-js-react/src/app/discord-sdk.js
This file was deleted.
Oops, something went wrong.
File renamed without changes.
199 changes: 199 additions & 0 deletions
199
packages/create-robo/templates/app-js-react/src/hooks/useDiscordSdk.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
}, []) | ||
} |
Oops, something went wrong.