28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/sdk-playground/packages/client/src/utils/getUserAvatarUrl.tsx:
--------------------------------------------------------------------------------
1 | import discordSdk from '../discordSdk';
2 | import {IGuildsMembersRead} from '../types';
3 | import {Types} from '@discord/embedded-app-sdk';
4 |
5 | interface GetUserAvatarArgs {
6 | guildMember: IGuildsMembersRead | null;
7 | user: Partial & Pick;
8 | cdn?: string;
9 | size?: number;
10 | }
11 |
12 | export function getUserAvatarUrl({
13 | guildMember,
14 | user,
15 | cdn = `https://cdn.discordapp.com`,
16 | size = 256,
17 | }: GetUserAvatarArgs): string {
18 | if (guildMember?.avatar != null && discordSdk.guildId != null) {
19 | return `${cdn}/guilds/${discordSdk.guildId}/users/${user.id}/avatars/${guildMember.avatar}.png?size=${size}`;
20 | }
21 | if (user.avatar != null) {
22 | return `${cdn}/avatars/${user.id}/${user.avatar}.png?size=${size}`;
23 | }
24 |
25 | const defaultAvatarIndex = (BigInt(user.id) >> 22n) % 6n;
26 | return `${cdn}/embed/avatars/${defaultAvatarIndex}.png?size=${size}`;
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Discord Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/sdk-playground/packages/client/src/pages/GetActivityInstance.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import discordSdk from '../discordSdk';
3 | import ReactJsonView from '../components/ReactJsonView';
4 | import {authStore} from '../stores/authStore';
5 |
6 | export default function GetActivityInstance() {
7 | const [instance, setInstance] = React.useState(null);
8 | const auth = authStore();
9 |
10 | React.useEffect(() => {
11 | async function update() {
12 | if (!auth) {
13 | return;
14 | }
15 | const instanceResponse = await fetch(`/api/activity-instance/${discordSdk.instanceId}`);
16 | setInstance(await instanceResponse.json());
17 | }
18 | update();
19 | }, [auth]);
20 |
21 | return (
22 |
23 |
24 |
Current Validated Instance
25 |
26 |
27 | {instance ? : null}
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/sdk-playground/packages/server/src/handleErrors.ts:
--------------------------------------------------------------------------------
1 | // `handleErrors()` is a little utility function that can wrap an HTTP request handler in a
2 | // try/catch and return errors to the client. You probably wouldn't want to use this in production
3 | // code but it is convenient when debugging and iterating.
4 | export async function handleErrors(
5 | request: Request,
6 | func: () => Promise,
7 | ) {
8 | try {
9 | return await func();
10 | } catch (e) {
11 | const err = e as Error;
12 | if (request.headers.get('Upgrade') === 'websocket') {
13 | // Annoyingly, if we return an HTTP error in response to a WebSocket request, Chrome devtools
14 | // won't show us the response body! So... let's send a WebSocket response with an error
15 | // frame instead.
16 | const pair = new WebSocketPair();
17 | pair[1].accept();
18 | pair[1].send(JSON.stringify({ error: err.stack }));
19 | pair[1].close(1011, 'Uncaught exception during session setup');
20 | return new Response(null, { status: 101, webSocket: pair[0] });
21 | }
22 | return new Response(err.stack, { status: 500 });
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sdk-playground/packages/client/src/pages/InitiateImageUpload.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import discordSdk from '../discordSdk';
3 |
4 | export default function InitiateImageUpload() {
5 | const [imageUrl, setImageUrl] = React.useState();
6 | const [awaitingInitiateImageUpload, setAwaitingInitiateImageUpload] = React.useState(false);
7 |
8 | const doOpenAttachmentUpload = async () => {
9 | try {
10 | setAwaitingInitiateImageUpload(true);
11 | const response = await discordSdk.commands.initiateImageUpload();
12 | if (response) {
13 | setImageUrl(response.image_url);
14 | }
15 | } catch (err: any) {
16 | console.log(err);
17 | } finally {
18 | setAwaitingInitiateImageUpload(false);
19 | }
20 | };
21 |
22 | return (
23 |
15 | This is a place for testing out all of the capabilities of the Embedded App SDK as well as validating
16 | platform-specific behavior related to Activities in Discord.
17 |
27 | This example keeps track of every window "resize" event. It can be used to debug and verify dimension changes
28 | when the iframe transitions between PIP / tiles for web and mobile
29 |
43 | This page demonstrates Discord's proxy authentication system for embedded apps.
44 | The request is authenticated using Discord's proxy headers and verified on the server.
45 |
46 |
47 |
48 |
63 |
64 | {!auth && (
65 |
66 | Please authenticate first to use this feature.
67 |
36 | This example is used for tracking and debugging when the document's visibility state (
37 |
38 | document.visibilityState
39 |
40 | ) changes. This is important to track on mobile embedded apps, as embedded iframes such as youtube, will pause
41 | video (or trigger other unwanted side-effects) if the visibility state switches to "hidden" when the embedded
42 | app is passed between webviews.
43 |
API response from {`/api/users/@me/guilds/${discordSdk.guildId}/member`}
116 |
117 | >
118 | )}
119 |
120 | );
121 | }
122 |
--------------------------------------------------------------------------------
/nested-messages/README.md:
--------------------------------------------------------------------------------
1 | # Embedded app with Nested Messages
2 |
3 | The `embedded-app-sdk` is intended for use by a single-page application. We recognize developers may be using frameworks or approaches that do not necessarily "fit into the bucket" of single-page applications, and wanted to provide some suggestions, specifically, we recommend nesting those frameworks inside of your embedded app's top-level single-page application and passing messages as you see fit. The developer recognizes that Discord may not be able to provide support, guidance, or code samples for communication required between your embedded app's top-level single-page applications and any frameworks you use inside of it.
4 |
5 | This example shows how an embedded app with a nested framework, such as a iframe hosting a multi-page-app, an iframe hosting a Unity App, or any other unique framework can be set up to work inside of a Discord embedded app iframe. We will create a parent website to mount the nested framework, hold state, and pass along messages between the Discord Client and the nested framework. This example is not meant to serve as a source-of-truth for how you should implement passing messages, instead it's a minimal example, which you could take inspiration from, or expand upon, based on your embedded app's needs.
6 |
7 | ## How to run
8 |
9 | This embedded app depends on the embedded-app-sdk being built. To build the package, from the root of this repository run the following commands in your terminal.
10 |
11 | ```
12 | pnpm install
13 | pnpm build
14 | ```
15 |
16 | ### Set up your .env file
17 |
18 | Copy/rename the [.example.env](/examples/nested-messages/.example.env) file to `.env`.
19 | Fill in `CLIENT_ID` and `CLIENT_SECRET` with the OAuth2 Client ID and Client Secret, as described [here](https://discord.com/developers/docs/activities/building-an-activity#find-your-oauth2-credentials).
20 |
21 | To serve this embedded app locally, from terminal navigate to `/embedded-app-sdk/examples/nested-messages` and run the following:
22 |
23 | ```
24 | pnpm install
25 | pnpm dev
26 | ```
27 |
28 | ## Many ways to solve a puzzle
29 |
30 | In this example, the core issue we're trying to solve is how to pass messages (commands, responses, and event subscriptions) between the Discord client and a nested framework inside of your embedded app. This example solves the puzzle by creating a `MessageInterface` class which looks very similar to `embedded-app-sdk`. This is for the following reasons:
31 |
32 | 1. It's also using javascript inside the nested iframe
33 | 2. It needs to solve many similar problems, such as passing events and listening for events that have been subscribed to.
34 |
35 | Depending on your use-case, you may find an alternate solution to `MessageInterface` to be a better fit for your embedded app.
36 |
37 | ## Nested Messages architecture
38 |
39 | ### Routing
40 |
41 | All client code is located in [/client](/examples/nested-messages/client/) and is served via a NodeJS Express server. Each route is provided by an `index.html` file. Each `index.html` file has a corresponding `index.ts` file and a (gitignored) compiled `index.js` file which is consumed by the html file. For example, let's consider the nested "embedded app", which is served at `/nested`. When a user visits this route, they are sent the file [client/nested/index.html](/examples/nested-messages/client/nested/index.html) which imports the compiled javascript from [client/nested/index.ts](/examples/nested-messages/client/nested/index.ts).
42 |
43 | ### Build tooling
44 |
45 | All typescript files inside of the `client` directory can be compiled into javascript files by running `npm run build`. When developing, you can run `npm run dev` which will rebuild whenever a file-change is detected inside of the `client` directory.
46 |
47 | ### Routes
48 |
49 | In this example, we have an embedded app which is nested inside of the parent "embedded app host". The embedded app host's responsibility is to initialize the SDK, listen for commands sent from the nested embedded app, and pass along responses sent by the Discord client.
50 |
51 | We have added a button to the nested embedded app which allows it to call `window.location.reload()` without invalidating the embedded app session.
52 |
53 | ### Nested message management
54 |
55 | See [client/index.ts](/examples/nested-messages/client/index.ts) to learn more about the "how" of how this example supports nested embedded app messages.
56 |
--------------------------------------------------------------------------------
/sdk-playground/packages/client/src/pages/OrientationLockState.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import discordSdk from '../discordSdk';
3 | import {Common} from '@discord/embedded-app-sdk';
4 |
5 | enum DropdownOptions {
6 | UNDEFINED = 'undefined',
7 | NULL = 'null',
8 | LANDSCAPE = 'landscape',
9 | PORTRAIT = 'portrait',
10 | UNLOCKED = 'unlocked',
11 | }
12 |
13 | const OPTIONS = Object.values(DropdownOptions);
14 | type OrientationLockStateType =
15 | | typeof Common.OrientationLockStateTypeObject.LANDSCAPE
16 | | typeof Common.OrientationLockStateTypeObject.PORTRAIT
17 | | typeof Common.OrientationLockStateTypeObject.UNLOCKED;
18 |
19 | type NonNullDropdownOptions = DropdownOptions.UNLOCKED | DropdownOptions.LANDSCAPE | DropdownOptions.PORTRAIT;
20 |
21 | function getNonNullLockStateFromDropdownOption(dropdownOption: NonNullDropdownOptions): OrientationLockStateType {
22 | switch (dropdownOption) {
23 | case DropdownOptions.LANDSCAPE:
24 | return Common.OrientationLockStateTypeObject.LANDSCAPE;
25 | case DropdownOptions.PORTRAIT:
26 | return Common.OrientationLockStateTypeObject.PORTRAIT;
27 | case DropdownOptions.UNLOCKED:
28 | return Common.OrientationLockStateTypeObject.UNLOCKED;
29 | }
30 | }
31 |
32 | function getLockStateFromDropdownOption(dropdownOption: DropdownOptions): OrientationLockStateType | null | undefined {
33 | switch (dropdownOption) {
34 | case DropdownOptions.UNDEFINED:
35 | return undefined;
36 | case DropdownOptions.NULL:
37 | return null;
38 | case DropdownOptions.LANDSCAPE:
39 | return Common.OrientationLockStateTypeObject.LANDSCAPE;
40 | case DropdownOptions.PORTRAIT:
41 | return Common.OrientationLockStateTypeObject.PORTRAIT;
42 | case DropdownOptions.UNLOCKED:
43 | return Common.OrientationLockStateTypeObject.UNLOCKED;
44 | }
45 | }
46 |
47 | export default function OrientationLockState() {
48 | const [defaultLockStateDropdownOption, setDefaultLockStateDropdownOption] = React.useState(
49 | DropdownOptions.UNLOCKED,
50 | );
51 | const [pipLockStateDropdownOption, setPipLockStateDropdownOption] = React.useState(
52 | DropdownOptions.UNDEFINED,
53 | );
54 | const [gridLockStateDropdownOption, setGridLockStateDropdownOption] = React.useState(
55 | DropdownOptions.UNDEFINED,
56 | );
57 |
58 | const onDefaultLockStateOptionSelected = React.useCallback((event: any) => {
59 | setDefaultLockStateDropdownOption(event.target.value);
60 | }, []);
61 |
62 | const onPipLockStateOptionSelected = React.useCallback((event: any) => {
63 | setPipLockStateDropdownOption(event.target.value);
64 | }, []);
65 | const onGridLockStateOptionSelected = React.useCallback((event: any) => {
66 | setGridLockStateDropdownOption(event.target.value);
67 | }, []);
68 |
69 | const onButtonClick = React.useCallback(() => {
70 | discordSdk.commands.setOrientationLockState({
71 | lock_state: getNonNullLockStateFromDropdownOption(defaultLockStateDropdownOption),
72 | picture_in_picture_lock_state: getLockStateFromDropdownOption(pipLockStateDropdownOption),
73 | grid_lock_state: getLockStateFromDropdownOption(gridLockStateDropdownOption),
74 | });
75 | }, [defaultLockStateDropdownOption, pipLockStateDropdownOption, gridLockStateDropdownOption]);
76 |
77 | return (
78 |
135 | Clicking the button will generate a quick link and open the share link dialog
136 |
137 |
138 |
139 | { hasPressedSend ? (didSend ? (
Succesfully shared!
) : (
Did not share
)) : null }
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/sdk-playground/README.md:
--------------------------------------------------------------------------------
1 | # SDK Playground
2 |
3 | ## Tech Stack
4 |
5 | This repo is an example built on top of the following frameworks
6 |
7 | 1. [ReactJS](https://reactjs.org/) - A frontend javascript UI framework
8 | 2. [Cloudflare Workers](https://developers.cloudflare.com/workers/) - A serverless execution environment
9 |
10 | ## Client architecture
11 |
12 | The client (aka front-end) is using [ViteJS](https://vitejs.dev/)'s React Typescript starter project. Vite has great starter projects in [many common javascript frameworks](https://vitejs.dev/guide/#trying-vite-online). All of these projects use the same config setup, which means that if you prefer VanillaJS, Svelte, etc... you can swap frameworks and still get the following:
13 |
14 | - Fast typescript bundling with hot-module-reloading
15 | - Identical configuration API
16 | - Identical environment variable API
17 |
18 | ## Server architecture
19 |
20 | The server (aka back-end) is using Cloudflare workers with typescript. Any file in the server project can be imported by the client, in case you need to share business logic.
21 |
22 | ## Setting up your Discord Application
23 |
24 | Before we write any code, lets follow the instructions [here](https://discord.com/developers/docs/activities/building-an-activity#step-1-creating-a-new-app) to make sure your Discord application is set up correctly.
25 |
26 | 
27 |
28 | ## Running your app locally
29 |
30 | As described [here](https://discord.com/developers/docs/activities/building-an-activity#step-4-running-your-app-locally-in-discord), we encourage using a tunnel solution such as [cloudflared](https://github.com/cloudflare/cloudflared#installing-cloudflared) for local development.
31 | To run your app locally, run the following from this directory (/examples/sdk-playground)
32 |
33 | ```
34 | pnpm install # only need to run this the first time
35 | pnpm dev
36 | pnpm tunnel # from another terminal
37 | ```
38 |
39 | Be sure to complete all the steps listed [here](https://discord.com/developers/docs/activities/building-an-activity) to ensure your development setup is working as expected.
40 |
41 | ### Adding a new environment variable
42 |
43 | In order to add new environment variables, you will need to do the following:
44 |
45 | 1. Add the environment key and value to `.env`
46 | 2. Add the key to [/packages/client/src/vite-env.d.ts](/packages/client/src/vite-env.d.ts)
47 | 3. Add the key to [/packages/server/src/types.ts](/packages/server/src/types.ts)
48 |
49 | This will ensure that you have type safety in your client and server code when consuming environment variables
50 |
51 | Per the [ViteJS docs](https://vitejs.dev/guide/env-and-mode.html#env-files)
52 |
53 | > To prevent accidentally leaking env variables to the client, only variables prefixed with VITE\_ are exposed to your Vite-processed code.
54 |
55 | ```env
56 | # Example .env file
57 | VITE_CLIENT_ID=123456789012345678
58 | CLIENT_SECRET=abcdefghijklmnopqrstuvwxyzabcdef # This should be the application oauth2 token from the developer portal.
59 | BOT_TOKEN=bot_token
60 | ```
61 |
62 | See instructions on getting the oauth2 token and the bot token [here](https://discord.com/developers/docs/activities/building-an-activity#find-your-oauth2-credentials).
63 |
64 | # Manual Deployment
65 |
66 | Steps to manually deploy the embedded app 0. Have access to the Discord Dev cloudflare account
67 |
68 | 1. Log into cloudflare with your credentials associated with Discord Dev
69 |
70 | ```sh
71 | wrangler login
72 | ```
73 |
74 | 2. Create or verify .env.production file
75 | If you haven't made it yet, copy the example.env file, rename it to `.env.production`, and add the `VITE_CLIENT_ID` and `CLIENT_SECRET` variables
76 |
77 | 3. Build and deploy the client
78 |
79 | ```
80 | cd packages/client
81 | npm run build
82 | CLOUDFLARE_ACCOUNT_ID=867c81bb01731ca0dfff534a58ce67d7 npx wrangler pages publish dist
83 | ```
84 |
85 | 4. Build and deploy the server
86 |
87 | ```
88 | cd packages/server
89 | npm run deploy
90 | ```
91 |
92 | # Testing SDK changes locally
93 |
94 | In order to test changes to the [embedded-app-sdk](https://github.com/discord/embedded-app-sdk) locally, follow these steps:
95 |
96 | - In a separate directory, clone the `embedded-app-sdk` github repo
97 | - From that directory, run `npm run dev` to continuously rebuild the SDK when changes are made
98 | - From inside of the sdk-playground's client directory, link the sdk via pnpm
99 |
100 | ```sh
101 | cd embedded-app-sdk-examples/sdk-playground-packages/client
102 | pnpm link ~/path/to/embedded-app-sdk # this is an example path
103 | cd embedded-app-sdk-examples/sdk-playground
104 | pnpm dev
105 | ```
106 |
107 | You should now be up and running with your own local build of the embedded-app-sdk. Be sure to not commit the linked pnpm-lock.yaml file.
108 |
109 | Note - You may need to close and relaunch the activity for changes to the sdk to take effect
110 |
--------------------------------------------------------------------------------
/discord-activity-starter/packages/client/src/main.ts:
--------------------------------------------------------------------------------
1 | import type { CommandResponse } from '@discord/embedded-app-sdk';
2 | import './style.css';
3 | import { discordSdk } from './discordSdk';
4 |
5 | type Auth = CommandResponse<'authenticate'>;
6 | let auth: Auth;
7 |
8 | // Once setupDiscordSdk is complete, we can assert that "auth" is initialized
9 | setupDiscordSdk().then(() => {
10 | appendVoiceChannelName();
11 | appendGuildAvatar();
12 | });
13 |
14 | async function setupDiscordSdk() {
15 | await discordSdk.ready();
16 |
17 | // Authorize with Discord Client
18 | const { code } = await discordSdk.commands.authorize({
19 | client_id: import.meta.env.VITE_CLIENT_ID,
20 | response_type: 'code',
21 | state: '',
22 | prompt: 'none',
23 | // More info on scopes here: https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes
24 | scope: [
25 | // Activities will launch through app commands and interactions of user-installable apps.
26 | // https://discord.com/developers/docs/tutorials/developing-a-user-installable-app#configuring-default-install-settings-adding-default-install-settings
27 | 'applications.commands',
28 |
29 | // "applications.builds.upload",
30 | // "applications.builds.read",
31 | // "applications.store.update",
32 | // "applications.entitlements",
33 | // "bot",
34 | 'identify',
35 | // "connections",
36 | // "email",
37 | // "gdm.join",
38 | 'guilds',
39 | // "guilds.join",
40 | 'guilds.members.read',
41 | // "messages.read",
42 | // "relationships.read",
43 | // 'rpc.activities.write',
44 | // "rpc.notifications.read",
45 | // "rpc.voice.write",
46 | 'rpc.voice.read',
47 | // "webhook.incoming",
48 | ],
49 | });
50 |
51 | // Retrieve an access_token from your activity's server
52 | // see https://discord.com/developers/docs/activities/development-guides/networking#construct-a-full-url
53 | const response = await fetch('/api/token', {
54 | method: 'POST',
55 | headers: {
56 | 'Content-Type': 'application/json',
57 | },
58 | body: JSON.stringify({
59 | code,
60 | }),
61 | });
62 | const { access_token } = await response.json();
63 |
64 | // Authenticate with Discord client (using the access_token)
65 | auth = await discordSdk.commands.authenticate({
66 | access_token,
67 | });
68 |
69 | if (auth == null) {
70 | throw new Error('Authenticate command failed');
71 | }
72 | }
73 |
74 | /**
75 | * This function fetches the current voice channel over RPC. It then creates a
76 | * text element that displays the voice channel's name
77 | */
78 | async function appendVoiceChannelName() {
79 | const app = document.querySelector('#app');
80 | if (!app) {
81 | throw new Error('Could not find #app element');
82 | }
83 |
84 | let activityChannelName = 'Unknown';
85 |
86 | // Requesting the channel in GDMs (when the guild ID is null) requires
87 | // the dm_channels.read scope which requires Discord approval.
88 | if (discordSdk.channelId != null && discordSdk.guildId != null) {
89 | // Over RPC collect info about the channel
90 | const channel = await discordSdk.commands.getChannel({
91 | channel_id: discordSdk.channelId,
92 | });
93 | if (channel.name != null) {
94 | activityChannelName = channel.name;
95 | }
96 | }
97 |
98 | // Update the UI with the name of the current voice channel
99 | const textTagString = `Activity Channel: "${activityChannelName}"`;
100 | const textTag = document.createElement('p');
101 | textTag.textContent = textTagString;
102 | app.appendChild(textTag);
103 | }
104 |
105 | /**
106 | * This function utilizes RPC and HTTP apis, in order show the current guild's avatar
107 | * Here are the steps:
108 | * 1. From RPC fetch the currently selected voice channel, which contains the voice channel's guild id
109 | * 2. From the HTTP API fetch a list of all of the user's guilds
110 | * 3. Find the current guild's info, including its "icon"
111 | * 4. Append to the UI an img tag with the related information
112 | */
113 | async function appendGuildAvatar() {
114 | const app = document.querySelector('#app');
115 | if (!app) {
116 | throw new Error('Could not find #app element');
117 | }
118 |
119 | // 1. From the HTTP API fetch a list of all of the user's guilds
120 | const guilds: Array<{ id: string; icon: string }> = await fetch(
121 | 'https://discord.com/api/users/@me/guilds',
122 | {
123 | headers: {
124 | // NOTE: we're using the access_token provided by the "authenticate" command
125 | Authorization: `Bearer ${auth.access_token}`,
126 | 'Content-Type': 'application/json',
127 | },
128 | },
129 | ).then((reply) => reply.json());
130 |
131 | // 2. Find the current guild's info, including it's "icon"
132 | const currentGuild = guilds.find((g) => g.id === discordSdk.guildId);
133 |
134 | // 3. Append to the UI an img tag with the related information
135 | if (currentGuild != null) {
136 | const guildImg = document.createElement('img');
137 | guildImg.setAttribute(
138 | 'src',
139 | // More info on image formatting here: https://discord.com/developers/docs/reference#image-formatting
140 | `https://cdn.discordapp.com/icons/${currentGuild.id}/${currentGuild.icon}.webp?size=128`,
141 | );
142 | guildImg.setAttribute('width', '128px');
143 | guildImg.setAttribute('height', '128px');
144 | guildImg.setAttribute('style', 'border-radius: 50%;');
145 | app.appendChild(guildImg);
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/sdk-playground/packages/client/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read http://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
20 | ),
21 | );
22 |
23 | interface ServiceWorkerConfig {
24 | onSuccess: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate: (registration: ServiceWorkerRegistration) => void;
26 | }
27 |
28 | export function register(config: ServiceWorkerConfig) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | if (!process.env.PUBLIC_URL) {
31 | throw new Error('process.env.PUBLIC_URL must be set.');
32 | }
33 | // The URL constructor is available in all browsers that support SW.
34 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit http://bit.ly/CRA-PWA',
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config: ServiceWorkerConfig) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then((registration) => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.',
83 | );
84 |
85 | // Execute callback
86 | if (config?.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config?.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch((error) => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config: ServiceWorkerConfig) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then((response) => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type');
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then((registration) => {
121 | registration.unregister().then(() => {
122 | window.location.reload();
123 | });
124 | });
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config);
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.',
133 | );
134 | });
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then((registration) => {
140 | registration.unregister();
141 | });
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/sdk-playground/packages/client/src/pages/Quests.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import discordSdk from '../discordSdk';
3 | import {RPCErrorCodes} from '@discord/embedded-app-sdk';
4 |
5 | interface QuestEnrollmentStatus {
6 | quest_id: string;
7 | is_enrolled: boolean;
8 | enrolled_at?: string | null;
9 | }
10 |
11 | interface QuestEnrollmentUpdateEvent {
12 | quest_id: string;
13 | is_enrolled: boolean;
14 | enrolled_at: string;
15 | }
16 |
17 | interface QuestTimerResponse {
18 | success: boolean;
19 | }
20 |
21 | export default function Quests() {
22 | const [questId, setQuestId] = React.useState('');
23 | const [message, setMessage] = React.useState('Enter a Quest ID to get started');
24 | const [enrollmentStatus, setEnrollmentStatus] = React.useState(null);
25 | const [isSubscribed, setIsSubscribed] = React.useState(false);
26 | const [awaitingStatus, setAwaitingStatus] = React.useState(false);
27 | const [awaitingTimer, setAwaitingTimer] = React.useState(false);
28 |
29 | const getEnrollmentStatus = async () => {
30 | if (!questId.trim()) {
31 | setMessage('Please enter a Quest ID');
32 | return;
33 | }
34 |
35 | try {
36 | setAwaitingStatus(true);
37 | setMessage('Getting enrollment status...');
38 | const response = await discordSdk.commands.getQuestEnrollmentStatus({ quest_id: questId });
39 | setEnrollmentStatus(response);
40 | setMessage(`Status retrieved: ${response.is_enrolled ? 'Enrolled' : 'Not Enrolled'}`);
41 | } catch (err: any) {
42 | const errorMessage = err.message ?? 'Unknown';
43 | setMessage(`Failed to get enrollment status. Reason: ${errorMessage}`);
44 | setEnrollmentStatus(null);
45 | } finally {
46 | setAwaitingStatus(false);
47 | }
48 | };
49 |
50 | const startTimer = async () => {
51 | if (!questId.trim()) {
52 | setMessage('Please enter a Quest ID');
53 | return;
54 | }
55 |
56 | try {
57 | setAwaitingTimer(true);
58 | setMessage('Starting quest timer...');
59 | const response = await discordSdk.commands.questStartTimer({ quest_id: questId });
60 | if (response.success) {
61 | setMessage('Quest timer started successfully!');
62 | } else {
63 | setMessage('Quest timer failed to start');
64 | }
65 | } catch (err: any) {
66 | const errorMessage = err.message ?? 'Unknown';
67 | setMessage(`Failed to start quest timer. Reason: ${errorMessage}`);
68 | } finally {
69 | setAwaitingTimer(false);
70 | }
71 | };
72 |
73 | const handleQuestEnrollmentUpdate = (event: QuestEnrollmentUpdateEvent) => {
74 | if (event.quest_id === questId) {
75 | setMessage(`Quest enrollment updated: ${event.is_enrolled ? 'Enrolled' : 'Not Enrolled'} at ${event.enrolled_at}`);
76 | setEnrollmentStatus({
77 | quest_id: event.quest_id,
78 | is_enrolled: event.is_enrolled,
79 | enrolled_at: event.enrolled_at,
80 | });
81 | }
82 | };
83 |
84 | const subscribeToUpdates = async () => {
85 | if (!questId.trim()) {
86 | setMessage('Please enter a Quest ID');
87 | return;
88 | }
89 |
90 | try {
91 | if (isSubscribed) {
92 | await discordSdk.unsubscribe('QUEST_ENROLLMENT_STATUS_UPDATE', handleQuestEnrollmentUpdate);
93 | setIsSubscribed(false);
94 | setMessage('Unsubscribed from quest enrollment updates');
95 | } else {
96 | await discordSdk.subscribe('QUEST_ENROLLMENT_STATUS_UPDATE', handleQuestEnrollmentUpdate);
97 | setIsSubscribed(true);
98 | setMessage(`Subscribed to quest enrollment updates for ${questId}`);
99 | }
100 | } catch (err: any) {
101 | const errorMessage = err.message ?? 'Unknown';
102 | setMessage(`Failed to ${isSubscribed ? 'unsubscribe from' : 'subscribe to'} quest updates. Reason: ${errorMessage}`);
103 | }
104 | };
105 |
106 | return (
107 |