├── pnpm-workspace.yaml ├── sdk-playground ├── packages │ ├── client │ │ ├── src │ │ │ ├── declare.d.ts │ │ │ ├── components │ │ │ │ ├── LoadingScreen.tsx │ │ │ │ ├── AuthProvider.tsx │ │ │ │ ├── Search.tsx │ │ │ │ ├── ReactJsonView.tsx │ │ │ │ ├── Scrollable.tsx │ │ │ │ └── DesignSystemProvider.tsx │ │ │ ├── Constants.ts │ │ │ ├── setupTests.ts │ │ │ ├── main.tsx │ │ │ ├── vite-env.d.ts │ │ │ ├── index.css │ │ │ ├── stores │ │ │ │ └── authStore.ts │ │ │ ├── reportWebVitals.ts │ │ │ ├── index.tsx │ │ │ ├── pages │ │ │ │ ├── CloseActivity.tsx │ │ │ │ ├── GetSkus.tsx │ │ │ │ ├── EncourageHardwareAcceleration.tsx │ │ │ │ ├── UserSettingsGetLocale.tsx │ │ │ │ ├── GetChannel.tsx │ │ │ │ ├── GetEntitlements.tsx │ │ │ │ ├── GetPlatformBehaviors.tsx │ │ │ │ ├── GetChannelPermissions.tsx │ │ │ │ ├── GetInstanceConnectedParticipants.tsx │ │ │ │ ├── GetActivityInstance.tsx │ │ │ │ ├── InitiateImageUpload.tsx │ │ │ │ ├── SetActivity.tsx │ │ │ │ ├── VoiceState.tsx │ │ │ │ ├── CurrentUser.tsx │ │ │ │ ├── Home.tsx │ │ │ │ ├── PlatformBehaviors.tsx │ │ │ │ ├── CurrentGuild.tsx │ │ │ │ ├── OrientationUpdates.tsx │ │ │ │ ├── WindowSizeTracker.tsx │ │ │ │ ├── LayoutMode.tsx │ │ │ │ ├── OpenExternalLink.tsx │ │ │ │ ├── ThermalStates.tsx │ │ │ │ ├── CurrentGuildMember.tsx │ │ │ │ ├── ActivityChannel.tsx │ │ │ │ ├── SafeAreas.tsx │ │ │ │ ├── ShareLink.tsx │ │ │ │ ├── Guilds.tsx │ │ │ │ ├── OpenInviteDialog.tsx │ │ │ │ ├── ProxyAuthExample.tsx │ │ │ │ ├── VisibilityListener.tsx │ │ │ │ ├── ActivityParticipants.tsx │ │ │ │ ├── OpenShareMomentDialog.tsx │ │ │ │ ├── AvatarAndName.tsx │ │ │ │ ├── OrientationLockState.tsx │ │ │ │ ├── QuickLink.tsx │ │ │ │ ├── Quests.tsx │ │ │ │ └── GamepadTester.tsx │ │ │ ├── styled │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ ├── getUserDisplayName.tsx │ │ │ │ └── getUserAvatarUrl.tsx │ │ │ ├── DiscordAPI.ts │ │ │ ├── AppStyles.tsx │ │ │ ├── favicon.svg │ │ │ ├── discordSdk.tsx │ │ │ ├── logo.svg │ │ │ ├── actions │ │ │ │ └── authActions.ts │ │ │ ├── App.css │ │ │ ├── serviceWorker.ts │ │ │ └── types.tsx │ │ ├── assets │ │ │ ├── baby-brick.jpeg │ │ │ └── brick-pug-life.gif │ │ ├── .gitignore │ │ ├── index.html │ │ ├── tsconfig.json │ │ ├── vite.config.ts │ │ ├── functions │ │ │ ├── templates │ │ │ │ └── index.ts │ │ │ └── _middleware.ts │ │ └── package.json │ ├── shared │ │ ├── .gitignore │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── crypto.ts │ │ ├── package.json │ │ └── tsconfig.json │ └── server │ │ ├── .gitignore │ │ ├── src │ │ ├── lib │ │ │ ├── bitflags.ts │ │ │ └── request.ts │ │ ├── handlers │ │ │ ├── getActivityInstanceHandler.ts │ │ │ ├── proxyAuthExampleHandler.ts │ │ │ ├── tokenHandler.ts │ │ │ └── iapHandler.ts │ │ ├── index.ts │ │ ├── handleApiRequest.ts │ │ ├── handleErrors.ts │ │ └── types.ts │ │ ├── wrangler.toml │ │ ├── tsconfig.json │ │ ├── test │ │ └── index.test.ts │ │ ├── handle-wrangler-secrets.sh │ │ └── package.json ├── package.json ├── .gitignore ├── example.env └── README.md ├── nested-messages ├── .gitignore ├── tsconfig.json ├── .example.env ├── types.d.ts ├── client │ ├── utils │ │ ├── types.ts │ │ ├── initializeSdk.ts │ │ └── MessageInterface.ts │ ├── nested │ │ ├── index.html │ │ └── index.ts │ ├── index.html │ └── index.ts ├── scripts │ └── build.js ├── package.json ├── server │ └── index.js └── README.md ├── .gitignore ├── discord-activity-starter ├── .gitignore ├── packages │ ├── server │ │ ├── src │ │ │ ├── shared │ │ │ │ └── hello.ts │ │ │ ├── utils.ts │ │ │ └── app.ts │ │ ├── environment.d.ts │ │ ├── tsconfig.json │ │ └── package.json │ └── client │ │ ├── src │ │ ├── discordSdk.ts │ │ ├── vite-env.d.ts │ │ ├── style.css │ │ └── main.ts │ │ ├── package.json │ │ ├── .gitignore │ │ ├── vite.config.ts │ │ ├── index.html │ │ ├── tsconfig.json │ │ └── favicon.svg ├── package.json └── README.md ├── renovate.json ├── package.json ├── biome.json ├── .github └── workflows │ ├── nested-messages.yml │ ├── ci.yml │ ├── discord-activity-starter.yml │ ├── sdk-playground.yml │ └── sdk-playground-production.yml ├── CONTRIBUTING.md ├── LICENSE.md └── README.md /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - '**/*' 3 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/declare.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nested-messages/.gitignore: -------------------------------------------------------------------------------- 1 | client/**/*.js 2 | .env 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | output 3 | *.log 4 | .DS_Store 5 | tmp 6 | *.tsbuildinfo 7 | -------------------------------------------------------------------------------- /sdk-playground/packages/shared/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | *.log 4 | .DS_Store -------------------------------------------------------------------------------- /sdk-playground/packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crypto'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /discord-activity-starter/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.pem 3 | *.log 4 | .DS_Store 5 | build 6 | dist 7 | .env 8 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | /dist 8 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/src/lib/bitflags.ts: -------------------------------------------------------------------------------- 1 | export const hasFlag = (currentFlags: number, flag: number): boolean => 2 | (currentFlags & flag) > 0; -------------------------------------------------------------------------------- /discord-activity-starter/packages/server/src/shared/hello.ts: -------------------------------------------------------------------------------- 1 | export function hello() { 2 | console.log("hello from the server's shared folder"); 3 | } 4 | -------------------------------------------------------------------------------- /nested-messages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true 4 | }, 5 | "include": ["."], 6 | "exclude": ["node_modules"] 7 | } 8 | -------------------------------------------------------------------------------- /nested-messages/.example.env: -------------------------------------------------------------------------------- 1 | # Copy, then rename this file .env and then put CLIENT_ID, etc... in the .env file 2 | CLIENT_ID=1234567890 3 | CLIENT_SECRET=abcdefghijklm 4 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/assets/baby-brick.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord/embedded-app-sdk-examples/HEAD/sdk-playground/packages/client/assets/baby-brick.jpeg -------------------------------------------------------------------------------- /nested-messages/types.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | CLIENT_ID: string; 5 | } 6 | } 7 | } 8 | 9 | export type {}; 10 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/assets/brick-pug-life.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discord/embedded-app-sdk-examples/HEAD/sdk-playground/packages/client/assets/brick-pug-life.gif -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/components/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function LoadingScreen() { 4 | return
loading
; 5 | } 6 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/client/src/discordSdk.ts: -------------------------------------------------------------------------------- 1 | import { DiscordSDK } from '@discord/embedded-app-sdk'; 2 | 3 | export const discordSdk = new DiscordSDK(import.meta.env.VITE_CLIENT_ID); 4 | -------------------------------------------------------------------------------- /nested-messages/client/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface MessageData { 2 | nonce?: string; 3 | event?: string; 4 | command?: string; 5 | data?: Record; 6 | args?: Record; 7 | } 8 | -------------------------------------------------------------------------------- /nested-messages/client/nested/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Game 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/Constants.ts: -------------------------------------------------------------------------------- 1 | const discordApiBase = import.meta.env.VITE_DISCORD_API_BASE; 2 | 3 | export const Constants = { 4 | WS_PORT: 3001, 5 | urls: { 6 | discord: discordApiBase, 7 | youtube: '/youtube', 8 | }, 9 | } as const; 10 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_CLIENT_ID: string; 5 | // add env variables here 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv; 10 | } 11 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | const root = document.getElementById('root')!; 7 | render( 8 | 9 | 10 | , 11 | root, 12 | ); 13 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/server/environment.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | VITE_CLIENT_ID: string; 5 | CLIENT_SECRET: string; 6 | NODE_ENV: 'development' | 'production'; 7 | PORT?: string; 8 | PWD: string; 9 | } 10 | } 11 | } 12 | 13 | export type {}; 14 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "sdk-playground" 2 | workers_dev = true 3 | compatibility_date = "2022-05-03" 4 | 5 | [vars] 6 | ENVIRONMENT = "dev" 7 | 8 | [env.dev.vars] 9 | ENVIRONMENT = "dev" 10 | 11 | [env.staging.vars] 12 | ENVIRONMENT = "staging" 13 | 14 | [env.production.vars] 15 | ENVIRONMENT = "production" 16 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_CLIENT_ID: string; 5 | readonly VITE_DISCORD_API_BASE: string; 6 | readonly VITE_APPLICATION_ID: string; 7 | // add env variables here 8 | } 9 | 10 | interface ImportMeta { 11 | readonly env: ImportMetaEnv; 12 | } 13 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build" 9 | }, 10 | "dependencies": { 11 | "@discord/embedded-app-sdk": "^2.0.0" 12 | }, 13 | "devDependencies": { 14 | "vite": "^5.2.9" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sdk-playground/packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "dev": "tsc --watch" 10 | }, 11 | "devDependencies": { 12 | "@cloudflare/workers-types": "^4.20240405.0", 13 | "typescript": "~5.9.0" 14 | } 15 | } -------------------------------------------------------------------------------- /sdk-playground/packages/client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es6", 5 | "module": "commonjs", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true 10 | }, 11 | "include": ["src/**/*.ts", "./environment.d.ts"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":disableDependencyDashboard", 6 | ":preserveSemverRanges" 7 | ], 8 | "ignorePaths": [ 9 | "**/node_modules/**" 10 | ], 11 | "packageRules": [ 12 | { 13 | "matchPackagePatterns": ["colyseus"], 14 | "groupName": "colyseus" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/client/src/style.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: Avenir, Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | text-align: center; 6 | color: black; 7 | background: white; 8 | } 9 | 10 | html, 11 | body, 12 | #app { 13 | width: 100%; 14 | height: 100%; 15 | } 16 | 17 | * { 18 | margin: 0; 19 | padding: 0; 20 | box-sizing: border-box; 21 | } 22 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({ 5 | envDir: '../../', 6 | server: { 7 | port: 3000, 8 | proxy: { 9 | '/api': { 10 | target: 'http://localhost:3001', 11 | changeOrigin: true, 12 | secure: false, 13 | ws: true, 14 | }, 15 | }, 16 | hmr: { 17 | clientPort: 443, 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SDK Playground 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /sdk-playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sdk-playground", 3 | "description": "A project for exploring and testing embedded app features", 4 | "private": true, 5 | "version": "0.1.0", 6 | "author": "Discord", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "pnpm run --filter \"./packages/**\" --parallel dev", 10 | "tunnel": "cloudflared tunnel --url http://localhost:3000" 11 | }, 12 | "engines": { 13 | "node": ">=16.0.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Discord Embedded App Starter 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sdk-playground/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # env files 4 | .env* 5 | .dev.vars 6 | 7 | # dependencies 8 | /node_modules 9 | /.pnp 10 | .pnp.js 11 | 12 | # testing 13 | /coverage 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | .idea -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "embedded-app-sdk-examples", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Examples for the Discord Embedded App SDK", 6 | "keywords": [ 7 | "Embedded App SDK", 8 | "Examples" 9 | ], 10 | "author": "Discord", 11 | "license": "MIT", 12 | "scripts": { 13 | "fix": "biome check --write .", 14 | "lint": "biome check ." 15 | }, 16 | "devDependencies": { 17 | "@biomejs/biome": "^2.2.7" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/stores/authStore.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | 3 | import type { TAuthenticatedContext } from '../types'; 4 | 5 | export const authStore = create(() => ({ 6 | user: undefined as unknown as TAuthenticatedContext['user'], 7 | access_token: '', 8 | scopes: [], 9 | expires: '', 10 | application: { 11 | rpc_origins: undefined, 12 | id: '', 13 | name: '', 14 | icon: null, 15 | description: '', 16 | }, 17 | guildMember: null, 18 | })); 19 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import type { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as serviceWorker from './serviceWorker'; 2 | import App from './App'; 3 | import React from 'react'; 4 | import {createRoot} from 'react-dom/client'; 5 | 6 | createRoot(document.getElementById('root')!).render(); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: http://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "module": "esnext", 5 | "target": "es2020", 6 | "lib": ["es2020"], 7 | "strict": true, 8 | "alwaysStrict": true, 9 | "preserveConstEnums": true, 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "skipLibCheck": true, 13 | "types": ["@cloudflare/workers-types", "@types/jest", "@types/service-worker-mock"] 14 | }, 15 | "include": ["src"], 16 | "exclude": ["node_modules", "dist", "test"] 17 | } 18 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/CloseActivity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import discordSdk from '../discordSdk'; 4 | import { RPCCloseCodes } from '@discord/embedded-app-sdk'; 5 | 6 | export default function CloseActivity() { 7 | React.useEffect(() => { 8 | discordSdk.close(RPCCloseCodes.CLOSE_NORMAL, 'Activity closed'); 9 | }, []); 10 | 11 | return ( 12 |
13 |
14 |

Close the Activity

15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.2.7/schema.json", 3 | "files": { 4 | "includes": [ 5 | "**/*.js", 6 | "**/*.ts", 7 | "!**/node_modules", 8 | "!**/dist", 9 | "!**/build", 10 | "!**/coverage", 11 | "!**/lib", 12 | "!**/.wrangler" 13 | ] 14 | }, 15 | "assist": { "actions": { "source": { "organizeImports": "on" } } }, 16 | "linter": { 17 | "enabled": true, 18 | "rules": { 19 | "recommended": true 20 | } 21 | }, 22 | "javascript": { 23 | "formatter": { 24 | "quoteStyle": "single" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sdk-playground/packages/shared/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Env { 2 | ENVIRONMENT: 'dev' | 'staging' | 'production'; 3 | VITE_CLIENT_ID: string; 4 | CLIENT_SECRET: string; 5 | BOT_TOKEN: string; 6 | PUBLIC_KEY: string; 7 | VITE_DISCORD_API_BASE: string; 8 | CF_ACCESS_CLIENT_ID?: string; 9 | CF_ACCESS_CLIENT_SECRET?: string; 10 | PROXY_AUTH_MODE?: 'enforce' | 'log-only' | 'disabled'; 11 | } 12 | 13 | export interface ProxyToken { 14 | application_id: string; 15 | user_id: string; 16 | is_developer: boolean; 17 | created_at: number; 18 | expires_at: number; 19 | } 20 | -------------------------------------------------------------------------------- /nested-messages/scripts/build.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { build } = require('esbuild'); 3 | const { glob } = require('glob'); 4 | const entryPoints = glob.globSync('./client/**/*.ts'); 5 | 6 | // Inject .env variables 7 | const define = {}; 8 | for (const k in process.env) { 9 | if (!['CLIENT_SECRET'].includes(k)) { 10 | define[`process.env.${k}`] = JSON.stringify(process.env[k]); 11 | } 12 | } 13 | 14 | build({ 15 | bundle: true, 16 | entryPoints, 17 | outbase: './client', 18 | outdir: './client', 19 | platform: 'browser', 20 | external: [], 21 | define, 22 | }); 23 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "Bundler", 8 | "strict": true, 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/components/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {LoadingScreen} from './LoadingScreen'; 3 | import {authStore} from '../stores/authStore'; 4 | import {start} from '../actions/authActions'; 5 | 6 | export function AuthProvider({children}: {children: React.ReactNode}) { 7 | const auth = authStore(); 8 | 9 | React.useEffect(() => { 10 | start().catch(e => { 11 | console.error('Error starting auth', e); 12 | }) 13 | }, []); 14 | 15 | if (auth.user == null) { 16 | return ; 17 | } 18 | 19 | return <>{children}; 20 | } 21 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/styled/index.ts: -------------------------------------------------------------------------------- 1 | import { indigo, indigoDark, slate, slateDark } from '@radix-ui/colors'; 2 | import { createStitches } from '@stitches/react'; 3 | 4 | export const { 5 | styled, 6 | css, 7 | globalCss, 8 | keyframes, 9 | getCssText, 10 | theme, 11 | createTheme, 12 | config, 13 | } = createStitches({ 14 | media: { 15 | small: '(max-width: 640px)', 16 | xsmall: '(max-width: 200px)', 17 | }, 18 | theme: { 19 | colors: { 20 | ...slate, 21 | ...indigo, 22 | }, 23 | }, 24 | }); 25 | 26 | export const darkTheme = createTheme({ 27 | colors: { 28 | ...slateDark, 29 | ...indigoDark, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/components/Search.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as S from '../AppStyles'; 3 | 4 | const Search = ({onQueryChanged}: {onQueryChanged: (q: string) => void}) => { 5 | const [query, setQuery] = React.useState(() => { 6 | return localStorage.getItem('sdkPlaygroundSearchQuery') ?? ''; 7 | }); 8 | 9 | React.useEffect(() => { 10 | localStorage.setItem('sdkPlaygroundSearchQuery', query); 11 | onQueryChanged(query); 12 | }, [onQueryChanged, query]); 13 | 14 | return setQuery(e.target.value)} />; 15 | }; 16 | 17 | export default Search; -------------------------------------------------------------------------------- /sdk-playground/packages/server/src/handlers/getActivityInstanceHandler.ts: -------------------------------------------------------------------------------- 1 | import { requestHeaders } from '../lib/request'; 2 | import type { Env } from '../types'; 3 | 4 | export default async function getActivityInstanceHandler( 5 | path: string[], 6 | _request: Request, 7 | env: Env, 8 | ) { 9 | try { 10 | const instanceId = path[1]; 11 | return await fetch( 12 | `${env.VITE_DISCORD_API_BASE}/applications/${env.VITE_CLIENT_ID}/activity-instances/${instanceId}`, 13 | { 14 | headers: requestHeaders(env), 15 | }, 16 | ); 17 | } catch (ex) { 18 | console.error(ex); 19 | return new Response(`Internal Error: ${ex}`, { status: 500 }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/utils/getUserDisplayName.tsx: -------------------------------------------------------------------------------- 1 | import {IGuildsMembersRead} from '../types'; 2 | import {Types} from '@discord/embedded-app-sdk'; 3 | 4 | interface GetUserDisplayNameArgs { 5 | guildMember: IGuildsMembersRead | null; 6 | user: Partial; 7 | } 8 | 9 | export function getUserDisplayName({guildMember, user}: GetUserDisplayNameArgs) { 10 | if (guildMember?.nick != null && guildMember.nick !== '') return guildMember.nick; 11 | 12 | if (user.discriminator !== '0') return `${user.username}#${user.discriminator}`; 13 | 14 | if (user.global_name != null && user.global_name !== '') return user.global_name; 15 | 16 | return user.username; 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/nested-messages.yml: -------------------------------------------------------------------------------- 1 | name: nested-messages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'nested-messages/packages/**' 8 | pull_request: 9 | branches: [main] 10 | paths: 11 | - 'nested-messages/packages/**' 12 | 13 | jobs: 14 | tsc: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 9.0.6 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: 22 24 | cache: pnpm 25 | - run: pnpm install 26 | - run: npm run build 27 | working-directory: nested-messages/ 28 | 29 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "types": ["@cloudflare/workers-types"] 19 | }, 20 | "include": ["."], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /sdk-playground/packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "lib": ["ES2022"], 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "exactOptionalPropertyTypes": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": false, 19 | "jsx": "react-jsx", 20 | "types": ["@cloudflare/workers-types"] 21 | }, 22 | "include": [ 23 | "src/**/*" 24 | ] 25 | } -------------------------------------------------------------------------------- /sdk-playground/packages/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig, loadEnv } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig(({ mode }) => { 6 | const env = loadEnv(mode, '../../', ''); 7 | return { 8 | plugins: [react()], 9 | envDir: '../../', 10 | server: { 11 | port: Number.parseInt(env.WEBAPP_SERVE_PORT, 10), 12 | allowedHosts: true, 13 | proxy: { 14 | '/api': { 15 | target: 'http://localhost:8787', 16 | changeOrigin: true, 17 | secure: false, 18 | ws: true, 19 | // rewrite: (path) => path.replace(/^\/api/, ""), 20 | }, 21 | }, 22 | hmr: { 23 | clientPort: 443, 24 | }, 25 | }, 26 | }; 27 | }); 28 | -------------------------------------------------------------------------------- /sdk-playground/example.env: -------------------------------------------------------------------------------- 1 | # fill in the credentials for the desired environment (dev, staging, prod) 2 | # copy this file to appropriate .env file, and then remove the changes 3 | # for example, for prod env 4 | # `cp example.env .env.prod && git checkout example.env` 5 | 6 | VITE_CLIENT_ID=123456789 7 | VITE_APPLICATION_ID=123456789 8 | VITE_DISCORD_API_BASE=https://discord.com/api 9 | 10 | BOT_TOKEN=bottoken # This should be the bot token for your application from the developer portal. 11 | CLIENT_SECRET=secret # This should be the oauth2 token for your application from the developer portal. 12 | PROXY_AUTH_MODE='log-only' 13 | PUBLIC_KEY=publickey # This should be the publickey for your application from the developer portal. 14 | WEBAPP_SERVE_PORT=3000 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test-ci: 11 | name: test-ci 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 5 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: pnpm/action-setup@v4 17 | with: 18 | version: 9.0.6 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 22 22 | cache: pnpm 23 | - run: pnpm install 24 | - run: pnpm --filter "./sdk-playground/packages/shared" build 25 | - run: pnpm --filter "./sdk-playground/packages/server" build 26 | - run: pnpm --filter "./sdk-playground/packages/client" build 27 | - run: pnpm lint 28 | -------------------------------------------------------------------------------- /discord-activity-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-activity-starter", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "A minimal starter project using embedded-app-sdk", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "pnpm run --filter \"./packages/**\" --parallel dev", 9 | "tunnel": "cloudflared tunnel --url http://localhost:3000" 10 | }, 11 | "author": "Discord", 12 | "license": "MIT", 13 | "dependencies": { 14 | "express": "^4.19.2" 15 | }, 16 | "devDependencies": { 17 | "@types/express": "^5.0.0", 18 | "@types/node": "^22.0.0", 19 | "nodemon": "^3.1.0", 20 | "npm-run-all2": "^7.0.0", 21 | "rimraf": "^6.0.0", 22 | "ts-node": "^10.9.1", 23 | "typescript": "~5.9.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nested-messages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nested-messages", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "build": "node ./scripts/build.js", 8 | "start": "node server/index.js", 9 | "dev": "nodemon --watch client -e ts,html --exec \"$npm_execpath run build && $npm_execpath run start\"" 10 | }, 11 | "author": "Discord", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@discord/embedded-app-sdk": "^2.0.0", 15 | "dotenv": "^16.0.3", 16 | "esbuild": "^0.25.0", 17 | "eventemitter3": "^5.0.0", 18 | "express": "^4.19.2", 19 | "uuid": "^11.0.0" 20 | }, 21 | "devDependencies": { 22 | "glob": "^11.0.0", 23 | "nodemon": "^3.1.0", 24 | "typescript": "~5.9.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/src/index.ts: -------------------------------------------------------------------------------- 1 | import { handleApiRequest } from './handleApiRequest'; 2 | import { handleErrors } from './handleErrors'; 3 | import type { Env } from './types'; 4 | export default { 5 | async fetch(request: Request, env: Env) { 6 | return await handleErrors(request, async () => { 7 | // We have received an HTTP request! Parse the URL and route the request. 8 | 9 | const url = await new URL(request.url); 10 | const path = url.pathname.slice(1).split('/'); 11 | 12 | switch (path[0]) { 13 | case 'api': 14 | // This is a request for `/api/...`, call the API handler. 15 | return handleApiRequest(path.slice(1), request, env); 16 | 17 | default: 18 | return new Response('Not found', { status: 404 }); 19 | } 20 | }); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/GetSkus.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import discordSdk from '../discordSdk'; 4 | import ReactJsonView from '../components/ReactJsonView'; 5 | 6 | export default function GetSkus() { 7 | const [skus, setSkus] = React.useState> | null>(null); 8 | 9 | React.useEffect(() => { 10 | async function getSkus() { 11 | const skus = await discordSdk.commands.getSkus(); 12 | setSkus(skus); 13 | } 14 | getSkus(); 15 | }, []); 16 | 17 | return ( 18 |
19 |
20 |

Get Skus

21 |
22 |
23 | {skus == null ? null : } 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import mock from 'service-worker-mock'; 2 | import { test } from 'uvu'; 3 | 4 | test.before(() => { 5 | Object.assign(globalThis, mock()); 6 | }); 7 | 8 | // test("GET /", async () => { 9 | // let req = new Request("/", { method: "GET" }); 10 | // let result = await Worker.fetch(req); 11 | // assert.is(result.status, 200); 12 | 13 | // let text = await result.text(); 14 | // assert.is(text, "request method: GET"); 15 | // }); 16 | 17 | // test("POST /", async () => { 18 | // let req = new Request("/", { method: "POST" }); 19 | // let result = await Worker.fetch(req); 20 | // assert.is(result.status, 200); 21 | 22 | // let text = await result.text(); 23 | // assert.is(text, "request method: POST"); 24 | // }); 25 | 26 | test.run(); 27 | -------------------------------------------------------------------------------- /.github/workflows/discord-activity-starter.yml: -------------------------------------------------------------------------------- 1 | name: discord-activity-starter 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'discord-activity-starter/packages/**' 8 | pull_request: 9 | branches: [main] 10 | paths: 11 | - 'discord-activity-starter/packages/**' 12 | 13 | jobs: 14 | tsc: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 9.0.6 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: 22 24 | cache: pnpm 25 | - run: pnpm install 26 | - run: npm run build 27 | working-directory: discord-activity-starter/packages/server 28 | - run: npm run build 29 | working-directory: discord-activity-starter/packages/client 30 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/handle-wrangler-secrets.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | env=$1 4 | if [ -z "$env" ] 5 | then 6 | echo "env is required" 7 | exit 1 8 | fi 9 | mode=$2 10 | if [ -z "$mode" ] 11 | then 12 | echo "mode is required" 13 | exit 1 14 | fi 15 | 16 | echo "setting wrangler env vars for env=$env mode=$mode" 17 | if [ "$mode" = "local" ] 18 | then 19 | # for local mode (eg `wranger dev`) wrangler uses a local .dev.vars file 20 | # https://developers.cloudflare.com/workers/wrangler/configuration/#environmental-variables 21 | cp ../../.env.$env .dev.vars 22 | else 23 | # open env file for env 24 | for var in $(grep -v '^#' ../../.env."$env") 25 | do 26 | key=$(sed 's/=.*//g' <<< $var) 27 | val=$(sed 's/.*=//g' <<< $var) 28 | echo $val | ./node_modules/.bin/wrangler secret put $key --env $env 29 | done 30 | fi 31 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.1.0", 4 | "description": "The app server", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm-run-all build start:prod", 8 | "start:prod": "NODE_ENV=production node ./dist/app.js", 9 | "dev": "nodemon --watch src -e ts,ejs --exec $npm_execpath start", 10 | "build": "npm-run-all build:clean build:tsc", 11 | "build:clean": "rimraf dist/*", 12 | "build:tsc": "tsc", 13 | "debug:start": "npm-run-all build debug:start:prod", 14 | "debug:start:prod": "node --nolazy --inspect-brk=9229 ./dist/app.js" 15 | }, 16 | "author": "Discord", 17 | "license": "MIT", 18 | "dependencies": { 19 | "dotenv": "^16.0.1", 20 | "nodemon": "^3.1.0" 21 | }, 22 | "devDependencies": { 23 | "npm-run-all2": "^7.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/EncourageHardwareAcceleration.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import discordSdk from '../discordSdk'; 3 | 4 | export default function EncourageHardwareAcceleration() { 5 | const [hwAccEnabled, setHWAccEnabled] = React.useState(null); 6 | 7 | const doEncourageHardwareAcceleration = async () => { 8 | const {enabled} = await discordSdk.commands.encourageHardwareAcceleration(); 9 | setHWAccEnabled(enabled === true); 10 | }; 11 | const enabledString = hwAccEnabled === null ? 'unknown' : hwAccEnabled === true ? 'Yes' : 'No'; 12 | 13 | return ( 14 |
15 |

Is Hardware Acceleration Enabled?

16 |

{enabledString}

17 | 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/sdk-playground.yml: -------------------------------------------------------------------------------- 1 | name: sdk-playground 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - 'sdk-playground/packages/**' 8 | pull_request: 9 | branches: [main] 10 | paths: 11 | - 'sdk-playground/packages/**' 12 | 13 | jobs: 14 | tsc: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 9.0.6 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: 22 24 | cache: pnpm 25 | - run: pnpm install 26 | - run: npm run build 27 | working-directory: sdk-playground/packages/shared 28 | - run: npm run build 29 | working-directory: sdk-playground/packages/server 30 | - run: npm run build 31 | working-directory: sdk-playground/packages/client 32 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "deploy": "./handle-wrangler-secrets.sh production local && wrangler publish src/index.ts --env production", 6 | "dev": "./handle-wrangler-secrets.sh dev local && wrangler dev src/index.ts --env dev", 7 | "staging": "./handle-wrangler-secrets.sh staging local && wrangler dev src/index.ts --env staging", 8 | "test": "uvu -r tsm test", 9 | "build": "tsc" 10 | }, 11 | "dependencies": { 12 | "shared": "workspace:*" 13 | }, 14 | "devDependencies": { 15 | "@cloudflare/workers-types": "^4.20240405.0", 16 | "@types/jest": "^30.0.0", 17 | "@types/service-worker-mock": "^2.0.1", 18 | "service-worker-mock": "^2.0.5", 19 | "tsm": "^2.2.1", 20 | "typescript": "~5.9.0", 21 | "uvu": "^0.5.3", 22 | "wrangler": "^4.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /nested-messages/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | 22 |
Parent iframe
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/UserSettingsGetLocale.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import discordSdk from '../discordSdk'; 4 | import ReactJsonView from '../components/ReactJsonView'; 5 | 6 | export default function UserSettingsGetLocale() { 7 | const [locale, setLocale] = React.useState 9 | > | null>(null); 10 | 11 | React.useEffect(() => { 12 | async function getEntitlements() { 13 | const locale = await discordSdk.commands.userSettingsGetLocale(); 14 | setLocale(locale); 15 | } 16 | getEntitlements(); 17 | }, []); 18 | 19 | return ( 20 |
21 |
22 |

User Settings Get Locale

23 |
24 |
25 | {locale == null ? null : } 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/GetChannel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import discordSdk from '../discordSdk'; 4 | import ReactJsonView from '../components/ReactJsonView'; 5 | 6 | export default function GetChannel() { 7 | const [channel, setChannel] = React.useState> | null>(null); 8 | 9 | React.useEffect(() => { 10 | async function getChannel() { 11 | if (discordSdk.channelId == null) return; 12 | const channel = await discordSdk.commands.getChannel({channel_id: discordSdk.channelId}); 13 | setChannel(channel); 14 | } 15 | getChannel(); 16 | }, []); 17 | 18 | return ( 19 |
20 |
21 |

Get Channel

22 |
23 |
24 | {channel == null ? null : } 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/GetEntitlements.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import discordSdk from '../discordSdk'; 4 | import ReactJsonView from '../components/ReactJsonView'; 5 | 6 | export default function GetEntitlements() { 7 | const [entitlements, setEntitlements] = React.useState 9 | > | null>(null); 10 | 11 | React.useEffect(() => { 12 | async function getEntitlements() { 13 | const entitlements = await discordSdk.commands.getEntitlements(); 14 | setEntitlements(entitlements); 15 | } 16 | getEntitlements(); 17 | }, []); 18 | 19 | return ( 20 |
21 |
22 |

Get Entitlements

23 |
24 |
25 | {entitlements == null ? null : } 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /nested-messages/server/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const path = require('node:path'); 3 | const express = require('express'); 4 | 5 | const app = express(); 6 | 7 | app.use(express.static(path.join(__dirname, '../client'))); 8 | app.use(express.json()); 9 | 10 | // Fetch token from developer portal and return to the embedded app 11 | app.post('/api/token', async (req, res) => { 12 | const response = await fetch('https://discord.com/api/oauth2/token', { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/x-www-form-urlencoded', 16 | }, 17 | body: new URLSearchParams({ 18 | client_id: process.env.CLIENT_ID, 19 | client_secret: process.env.CLIENT_SECRET, 20 | grant_type: 'authorization_code', 21 | code: req.body.code, 22 | }), 23 | }); 24 | 25 | const { access_token } = await response.json(); 26 | 27 | res.send({ access_token }); 28 | return; 29 | }); 30 | 31 | app.listen(3000); 32 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/src/handleApiRequest.ts: -------------------------------------------------------------------------------- 1 | import getActivityInstanceHandler from './handlers/getActivityInstanceHandler'; 2 | import iapHandler from './handlers/iapHandler'; 3 | import proxyAuthExampleHandler from './handlers/proxyAuthExampleHandler'; 4 | import tokenHandler from './handlers/tokenHandler'; 5 | import type { Env } from './types'; 6 | 7 | export function handleApiRequest(path: string[], request: Request, env: Env) { 8 | // We've received at API request. Route the request based on the path. 9 | switch (path[0]) { 10 | case 'token': 11 | return tokenHandler(request, env); 12 | case 'iap': 13 | return iapHandler(path, request, env); 14 | case 'activity-instance': 15 | return getActivityInstanceHandler(path, request, env); 16 | case 'proxy-auth-example': 17 | return proxyAuthExampleHandler(path, request, env); 18 | default: 19 | return new Response('Not found', { status: 404 }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/src/handlers/proxyAuthExampleHandler.ts: -------------------------------------------------------------------------------- 1 | import { withProxyAuth } from 'shared'; 2 | import type { Env, ProxyToken } from '../types'; 3 | 4 | async function handleWithProxyAuth( 5 | proxyToken: ProxyToken, 6 | _path: string[], 7 | _request: Request, 8 | _env: Env, 9 | ) { 10 | return new Response( 11 | JSON.stringify({ 12 | message: 'Proxy authentication verified successfully', 13 | user_id: proxyToken.user_id, 14 | application_id: proxyToken.application_id, 15 | is_developer: proxyToken.is_developer, 16 | expires_at: proxyToken.expires_at, 17 | }), 18 | { 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | 'Access-Control-Allow-Origin': '*', 22 | }, 23 | }, 24 | ); 25 | } 26 | 27 | export default async function proxyAuthExampleHandler( 28 | path: string[], 29 | request: Request, 30 | env: Env, 31 | ) { 32 | return withProxyAuth(handleWithProxyAuth, request, env, path); 33 | } 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | > See the [README](https://github.com/discord/embedded-app-sdk-examples/blob/main/README.md) for licensing and legal information 4 | 5 | Thank you for your interest in contributing to our examples! Before you create that pull request, here are some examples of wanted and unwanted changes to the repository. 6 | 7 | ## Types of Changes 8 | 9 | ### Wanted Changes 10 | 11 | 1. Bug fixes that address broken behavior or crashes in the examples. 12 | 1. Links to new externally maintained examples in the Community Curated Examples section of the README. 13 | 1. Fixes to incorrect statements or inaccuracies within the documentation. 14 | 1. Fixing of spelling and grammatical errors in the documentation. 15 | 16 | ### Unwanted Changes 17 | 18 | 1. New example codebases added directly to the repository. 19 | 1. Whitespace or formatting changes. 20 | 1. Subjective wording changes. 21 | 1. Additions that document unreleased product functionality. 22 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/GetPlatformBehaviors.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import discordSdk from '../discordSdk'; 4 | import ReactJsonView from '../components/ReactJsonView'; 5 | 6 | export default function GetPlatformBehaviors() { 7 | const [platformBehaviors, setPlatformBehaviors] = React.useState 9 | > | null>(null); 10 | 11 | React.useEffect(() => { 12 | async function getPlatformBehaviors() { 13 | const platformBehaviors = await discordSdk.commands.getPlatformBehaviors(); 14 | setPlatformBehaviors(platformBehaviors); 15 | } 16 | getPlatformBehaviors(); 17 | }, []); 18 | 19 | return ( 20 |
21 |
22 |

Get Platform Behaviors

23 |
24 |
25 | {platformBehaviors == null ? null : } 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/GetChannelPermissions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import discordSdk from '../discordSdk'; 4 | import ReactJsonView from '../components/ReactJsonView'; 5 | 6 | export default function GetChannelPermissions() { 7 | const [channelPermissions, setChannelPermissions] = React.useState 9 | > | null>(null); 10 | 11 | React.useEffect(() => { 12 | async function getChannelPermissions() { 13 | const channelPermissions = await discordSdk.commands.getChannelPermissions(); 14 | setChannelPermissions(channelPermissions); 15 | } 16 | getChannelPermissions(); 17 | }, []); 18 | 19 | return ( 20 |
21 |
22 |

Get Channel Permisssions

23 |
24 |
25 | {channelPermissions == null ? null : } 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/GetInstanceConnectedParticipants.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import discordSdk from '../discordSdk'; 4 | import ReactJsonView from '../components/ReactJsonView'; 5 | 6 | export default function GetInstanceConnectedParticipants() { 7 | const [participants, setParticipants] = React.useState 9 | > | null>(null); 10 | 11 | React.useEffect(() => { 12 | async function getParticipants() { 13 | const participants = await discordSdk.commands.getInstanceConnectedParticipants(); 14 | setParticipants(participants); 15 | } 16 | getParticipants(); 17 | }, []); 18 | 19 | return ( 20 |
21 |
22 |

Get Instance Connected Participants

23 |
24 |
25 | {participants == null ? null : } 26 |
27 |
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 |
24 |

Initiate image upload

25 |
Awaiting initiateImageUpload? "{JSON.stringify(awaitingInitiateImageUpload)}"
26 | 27 | {imageUrl ? : null} 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/SetActivity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import discordSdk from '../discordSdk'; 4 | 5 | export default function SetActivity() { 6 | React.useEffect(() => { 7 | // Grab a random image somewhere between 200 and 299 8 | const sizeString = (new Date().getTime() % 100).toString().padStart(2, '0'); 9 | const fillerUrl = `https://placebear.com/2${sizeString}/2${sizeString}`; 10 | const now = new Date(); 11 | 12 | discordSdk.commands.setActivity({ 13 | activity: { 14 | details: `Testing setActivity at ${now.toISOString()}`, 15 | assets: { 16 | small_image: fillerUrl, 17 | large_image: fillerUrl, 18 | }, 19 | secrets: { 20 | join: crypto.randomUUID(), 21 | }, 22 | party: { 23 | id: discordSdk.instanceId, 24 | size: [1, 0] 25 | }, 26 | timestamps: { 27 | start: now.getTime(), 28 | } 29 | }, 30 | }); 31 | }, []); 32 | 33 | return ( 34 |
35 |
36 |

Set Activity

37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/src/handlers/tokenHandler.ts: -------------------------------------------------------------------------------- 1 | import { readRequestBody, requestHeaders } from '../lib/request'; 2 | import type { Env, IGetOAuthToken } from '../types'; 3 | 4 | export default async function tokenHandler(request: Request, env: Env) { 5 | try { 6 | const body = JSON.parse(await readRequestBody(request)); 7 | const tokenBody = new URLSearchParams({ 8 | client_id: env.VITE_CLIENT_ID, 9 | client_secret: env.CLIENT_SECRET, 10 | grant_type: 'authorization_code', 11 | code: body.code, 12 | }); 13 | const response = await fetch(`${env.VITE_DISCORD_API_BASE}/oauth2/token`, { 14 | method: 'POST', 15 | headers: requestHeaders(env, { 16 | 'Content-Type': 'application/x-www-form-urlencoded', 17 | }), 18 | body: tokenBody, 19 | }); 20 | 21 | if (response.status !== 200) { 22 | return response; 23 | } 24 | 25 | const { access_token } = await response.json(); 26 | 27 | return new Response(JSON.stringify({ access_token }), { 28 | headers: { 'Access-Control-Allow-Origin': '*' }, 29 | }); 30 | } catch (ex) { 31 | console.error(ex); 32 | return new Response(`Internal Error: ${ex}`, { status: 500 }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nested-messages/client/nested/index.ts: -------------------------------------------------------------------------------- 1 | import { MessageInterface } from '../utils/MessageInterface'; 2 | 3 | const messageInterface = new MessageInterface(); 4 | 5 | window.addEventListener('DOMContentLoaded', async () => { 6 | await messageInterface.ready(); 7 | 8 | messageInterface.sendMessage({ 9 | command: 'SET_ACTIVITY', 10 | data: { 11 | activity: { 12 | details: 'Set Activity from nested iframe', 13 | type: 0, 14 | state: 'Playing', 15 | }, 16 | }, 17 | }); 18 | 19 | const parentQueryParams = new URLSearchParams(window.parent.location.search); 20 | const channelId = parentQueryParams.get('channel_id'); 21 | 22 | messageInterface.subscribe( 23 | 'SPEAKING_START', 24 | ({ user_id }) => { 25 | console.log(`"${user_id}" Just started talking`); 26 | }, 27 | { channel_id: channelId }, 28 | ); 29 | 30 | messageInterface.subscribe( 31 | 'SPEAKING_STOP', 32 | ({ user_id }) => { 33 | console.log(`"${user_id}" Just stopped talking`); 34 | }, 35 | { channel_id: channelId }, 36 | ); 37 | 38 | const reloadButton = document.getElementById('reload'); 39 | reloadButton?.addEventListener('click', () => { 40 | console.log('reloading'); 41 | window.location.reload(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/components/ReactJsonView.tsx: -------------------------------------------------------------------------------- 1 | import {JSONTree} from 'react-json-tree'; 2 | 3 | import {theme} from '../styled'; 4 | 5 | const jsonTheme = { 6 | base00: theme.colors.slate1.computedValue, // background 7 | base01: theme.colors.slate12.computedValue, // idk 8 | base02: theme.colors.slate6.computedValue, // lines 9 | base03: theme.colors.slate12.computedValue, // idk 10 | base04: theme.colors.slate10.computedValue, // object value count 11 | base05: theme.colors.slate12.computedValue, // idk 12 | base06: theme.colors.slate12.computedValue, // idk 13 | base07: theme.colors.slate12.computedValue, // object key 14 | base08: theme.colors.slate12.computedValue, // idk 15 | base09: theme.colors.slate12.computedValue, // string kv 16 | base0A: theme.colors.slate12.computedValue, // idk 17 | base0B: theme.colors.slate12.computedValue, // idk 18 | base0C: theme.colors.slate12.computedValue, // array index 19 | base0D: theme.colors.slate10.computedValue, // arrow 20 | base0E: theme.colors.slate12.computedValue, // idk 21 | base0F: theme.colors.slate12.computedValue, // int 22 | }; 23 | 24 | export default function ReactJsonView({src}: {src: unknown}) { 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/VoiceState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import discordSdk from '../discordSdk'; 3 | import ReactJsonView from '../components/ReactJsonView'; 4 | import {useLocation} from 'react-router-dom'; 5 | import {EventPayloadData} from '@discord/embedded-app-sdk'; 6 | 7 | export default function VoiceState() { 8 | const [voiceState, setVoiceState] = React.useState(null); 9 | const location = useLocation(); 10 | 11 | React.useEffect(() => { 12 | const {channelId} = discordSdk; 13 | if (!channelId) return; 14 | 15 | const updateVoiceState = (voiceState: EventPayloadData<'VOICE_STATE_UPDATE'>) => { 16 | setVoiceState(voiceState); 17 | }; 18 | discordSdk.subscribe('VOICE_STATE_UPDATE', updateVoiceState, { 19 | channel_id: channelId, 20 | }); 21 | return () => { 22 | discordSdk.unsubscribe('VOICE_STATE_UPDATE', updateVoiceState, {channel_id: channelId}); 23 | }; 24 | }, [location.search]); 25 | 26 | return ( 27 |
28 |
29 |

Event Subscription

30 |

VOICE_STATE_UPDATE

31 |
32 |
33 | {voiceState ? : null} 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/CurrentUser.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import discordSdk from '../discordSdk'; 3 | import ReactJsonView from '../components/ReactJsonView'; 4 | import {useLocation} from 'react-router-dom'; 5 | import {EventPayloadData} from '@discord/embedded-app-sdk'; 6 | 7 | export default function CurrentUser() { 8 | const [currentUser, setCurrentUser] = React.useState | null>(null); 9 | const location = useLocation(); 10 | 11 | React.useEffect(() => { 12 | const {channelId} = discordSdk; 13 | if (!channelId) return; 14 | 15 | const handleCurrentUserUpdate = (currentUserEvent: EventPayloadData<'CURRENT_USER_UPDATE'>) => { 16 | setCurrentUser(currentUserEvent); 17 | }; 18 | discordSdk.subscribe('CURRENT_USER_UPDATE', handleCurrentUserUpdate); 19 | return () => { 20 | discordSdk.unsubscribe('CURRENT_USER_UPDATE', handleCurrentUserUpdate); 21 | }; 22 | }, [location.search]); 23 | 24 | return ( 25 |
26 |
27 |

Event Subscription

28 |

CURRENT_USER_UPDATE

29 |
30 |
31 | {currentUser ? : null} 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/DiscordAPI.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from './Constants'; 2 | 3 | export enum RequestType { 4 | GET = 'GET', 5 | PATCH = 'PATCH', 6 | PUT = 'PUT', 7 | POST = 'POST', 8 | DELETE = 'DELETE', 9 | } 10 | 11 | export interface DiscordAPIRequest { 12 | endpoint: string; 13 | method: RequestType; 14 | body?: unknown; 15 | headers?: Record; 16 | stringifyBody?: boolean; 17 | } 18 | 19 | function request( 20 | { 21 | method, 22 | endpoint, 23 | body, 24 | headers: baseHeaders, 25 | stringifyBody = false, 26 | }: DiscordAPIRequest, 27 | accessToken: string, 28 | ): Promise { 29 | const headers: HeadersInit = { 30 | Authorization: `Bearer ${accessToken}`, 31 | ...baseHeaders, 32 | }; 33 | return fetch(`${Constants.urls.discord}${endpoint}`, { 34 | method, 35 | headers, 36 | body: stringifyBody === true ? JSON.stringify(body) : (body as BodyInit), 37 | }) 38 | .then((response) => { 39 | if (!response.ok || response.status >= 400) { 40 | console.error(`error: ${response.body}`); 41 | throw new Error(`error${response.status}`); 42 | } 43 | return response.json(); 44 | }) 45 | .catch((e) => { 46 | console.error(`error: ${JSON.stringify(e)}`); 47 | throw e; 48 | }); 49 | } 50 | 51 | export const DiscordAPI = { 52 | request, 53 | }; 54 | -------------------------------------------------------------------------------- /sdk-playground/packages/server/src/types.ts: -------------------------------------------------------------------------------- 1 | // Re-export shared types from the shared lib 2 | export type { Env, ProxyToken } from 'shared'; 3 | 4 | // Server-specific types 5 | export interface IGetOAuthToken { 6 | access_token: string; 7 | } 8 | 9 | export enum SKUAccessTypes { 10 | FULL = 1, 11 | EARLY_ACCESS = 2, 12 | VIP_ACCESS = 3, 13 | } 14 | 15 | export const SKUFlags = { 16 | AVAILABLE: 1 << 2, 17 | }; 18 | 19 | export enum EntitlementTypes { 20 | PURCHASE = 1, 21 | PREMIUM_SUBSCRIPTION = 2, 22 | DEVELOPER_GIFT = 3, 23 | TEST_MODE_PURCHASE = 4, 24 | FREE_PURCHASE = 5, 25 | USER_GIFT = 6, 26 | PREMIUM_PURCHASE = 7, 27 | APPLICATION_SUBSCRIPTION = 8, 28 | } 29 | 30 | export interface IGetSKUs { 31 | id: string; 32 | type: number; 33 | dependent_sku_id: string | null; 34 | application_id: string; 35 | access_type: SKUAccessTypes; 36 | name: string; 37 | slug: string; 38 | flags: number; 39 | release_date: string | null; 40 | price: { 41 | amount: number; 42 | currency: string; 43 | }; 44 | } 45 | 46 | export interface IGetEntitlements { 47 | user_id: string; 48 | sku_id: string; 49 | application_id: string; 50 | id: string; 51 | type: number; 52 | consumed: boolean; 53 | payment: { 54 | id: string; 55 | currency: string; 56 | amount: number; 57 | tax: number; 58 | tax_inclusive: boolean; 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/server/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function sleep(ms: number) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | 5 | /** 6 | * This function extends fetch to allow retrying 7 | * If the request returns a 429 error code, it will wait and retry after "retry_after" seconds 8 | */ 9 | export async function fetchAndRetry( 10 | input: RequestInfo, 11 | init?: RequestInit | undefined, 12 | nRetries = 3, 13 | ): Promise { 14 | try { 15 | // Make the request 16 | const response = await fetch(input, init); 17 | 18 | // If there's a 429 error code, retry after retry_after seconds 19 | // https://discord.com/developers/docs/topics/rate-limits#rate-limits 20 | if (response.status === 429 && nRetries > 0) { 21 | const retryAfter = Number(response.headers.get('retry_after')); 22 | if (Number.isNaN(retryAfter)) { 23 | return response; 24 | } 25 | await sleep(retryAfter * 1000); 26 | return await fetchAndRetry(input, init, nRetries - 1); 27 | } 28 | return response; 29 | } catch (ex) { 30 | if (nRetries <= 0) { 31 | throw ex; 32 | } 33 | 34 | // If the request failed, wait one second before trying again 35 | // This could probably be fancier with exponential backoff 36 | await sleep(1000); 37 | return await fetchAndRetry(input, init, nRetries - 1); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import discordSdk from '../discordSdk'; 2 | 3 | export default function Home() { 4 | const instanceId = discordSdk.instanceId; 5 | const clientId = discordSdk.clientId; 6 | const channelId = discordSdk.channelId; 7 | const guildId = discordSdk.guildId; 8 | const platform = discordSdk.platform; 9 | const mobileAppVersion = discordSdk.mobileAppVersion; 10 | const sdkVersion = discordSdk.sdkVersion; 11 | return ( 12 |
13 |

Welcome to the SDK playground! 🎉

14 |

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 |

18 |

19 |

Basic Activity Info

20 |

Custom ID: {discordSdk.customId}

21 |

Instance ID: {instanceId}

22 |

Referrer ID: {discordSdk.referrerId}

23 |

Client ID: {clientId}

24 |

Channel ID: {channelId}

25 | { guildId != null ? (

Guild ID: {guildId}

) : null } 26 |

Platform: {platform}

27 | { mobileAppVersion != null ? (

Mobile App Version: {mobileAppVersion}

) : null } 28 |

Using SDK Version: {sdkVersion }

29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/PlatformBehaviors.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {range} from 'lodash'; 3 | 4 | import discordSdk from '../discordSdk'; 5 | import ReactJsonView from '../components/ReactJsonView'; 6 | 7 | const fillerArray = range(0, 40); 8 | 9 | export default function PlatformBehaviors() { 10 | const [platformBehaviors, setPlatformBehaviors] = React.useState(null); 11 | 12 | const getPlatformBehaviors = React.useCallback(async () => { 13 | const behaviors = await discordSdk.commands.getPlatformBehaviors(); 14 | setPlatformBehaviors(behaviors); 15 | }, []); 16 | 17 | React.useEffect(() => { 18 | getPlatformBehaviors(); 19 | }, [getPlatformBehaviors]); 20 | 21 | return ( 22 |
23 |
24 |

Platform Behaviors

25 |
26 |
27 | 28 |
29 |
30 | {platformBehaviors == null ? null : } 31 |
32 |
33 | {fillerArray.map((i) => ( 34 |
Space {i}
35 | ))} 36 | 37 | {fillerArray.map((i) => ( 38 |
Space {i}
39 | ))} 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/CurrentGuild.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import discordSdk from '../discordSdk'; 3 | import ReactJsonView from '../components/ReactJsonView'; 4 | import {DiscordAPI, RequestType} from '../DiscordAPI'; 5 | import {authStore} from '../stores/authStore'; 6 | 7 | /** Full guild "shape" here: https://discord.com/developers/docs/resources/guild#guild-object-guild-structure */ 8 | interface Guild { 9 | id: string; 10 | } 11 | 12 | export default function CurrentGuild() { 13 | const [guild, setGuild] = React.useState(null); 14 | const auth = authStore(); 15 | 16 | React.useEffect(() => { 17 | async function update() { 18 | if (!auth) { 19 | return; 20 | } 21 | const guildId = discordSdk.guildId; 22 | if (!guildId) { 23 | return; 24 | } 25 | const guilds = await DiscordAPI.request( 26 | {method: RequestType.GET, endpoint: `/users/@me/guilds`}, 27 | auth.access_token, 28 | ); 29 | const newGuild = guilds.find(({id}) => id === guildId) ?? null; 30 | setGuild(newGuild); 31 | } 32 | update(); 33 | }, [auth]); 34 | 35 | return ( 36 |
37 |
38 |

Current Guild

39 |
40 |
41 | {guild ? : null} 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/server/src/app.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import dotenv from 'dotenv'; 3 | import express, { 4 | type Application, 5 | type Request, 6 | type Response, 7 | } from 'express'; 8 | import { fetchAndRetry } from './utils'; 9 | 10 | dotenv.config({ path: '../../.env' }); 11 | 12 | const app: Application = express(); 13 | const port: number = Number(process.env.PORT) || 3001; 14 | 15 | app.use(express.json()); 16 | 17 | if (process.env.NODE_ENV === 'production') { 18 | const clientBuildPath = path.join(__dirname, '../../client/dist'); 19 | app.use(express.static(clientBuildPath)); 20 | } 21 | 22 | // Fetch token from developer portal and return to the embedded app 23 | app.post('/api/token', async (req: Request, res: Response) => { 24 | const response = await fetchAndRetry('https://discord.com/api/oauth2/token', { 25 | method: 'POST', 26 | headers: { 27 | 'Content-Type': 'application/x-www-form-urlencoded', 28 | }, 29 | body: new URLSearchParams({ 30 | client_id: process.env.VITE_CLIENT_ID, 31 | client_secret: process.env.CLIENT_SECRET, 32 | grant_type: 'authorization_code', 33 | code: req.body.code, 34 | }), 35 | }); 36 | 37 | const { access_token } = (await response.json()) as { 38 | access_token: string; 39 | }; 40 | 41 | res.send({ access_token }); 42 | }); 43 | 44 | app.listen(port, () => { 45 | console.log(`App is listening on port ${port} !`); 46 | }); 47 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/OrientationUpdates.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import discordSdk from '../discordSdk'; 3 | import {Common, Events, EventPayloadData} from '@discord/embedded-app-sdk'; 4 | 5 | export default function OrientationUpdates() { 6 | const [orientationString, setOrientationString] = React.useState(''); 7 | 8 | const updateOrientation = React.useCallback<(u: EventPayloadData<'ORIENTATION_UPDATE'>) => void>((update) => { 9 | const screenOrientation = update.screen_orientation; 10 | let orientationStr; 11 | switch (screenOrientation) { 12 | case Common.OrientationTypeObject.PORTRAIT: 13 | orientationStr = 'PORTRAIT'; 14 | break; 15 | case Common.OrientationTypeObject.LANDSCAPE: 16 | orientationStr = 'LANDSCAPE'; 17 | break; 18 | default: 19 | orientationStr = 'UNHANDLED'; 20 | break; 21 | } 22 | 23 | setOrientationString(orientationStr); 24 | }, []); 25 | 26 | React.useEffect(() => { 27 | discordSdk.subscribe(Events.ORIENTATION_UPDATE, updateOrientation); 28 | return () => { 29 | discordSdk.unsubscribe(Events.ORIENTATION_UPDATE, updateOrientation); 30 | }; 31 | }, [updateOrientation]); 32 | 33 | return ( 34 |
35 |

Orientation Updates

36 |

app orientation: {orientationString}

37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /nested-messages/client/utils/initializeSdk.ts: -------------------------------------------------------------------------------- 1 | import { DiscordSDK } from '@discord/embedded-app-sdk'; 2 | 3 | /** 4 | * This function should be called from the top-level of your embedded app 5 | * It initializes the SDK, then authorizes, and authenticates the embedded app 6 | */ 7 | export async function initializeSdk(): Promise { 8 | if (typeof process.env.CLIENT_ID !== 'string') { 9 | throw new Error("Must specify 'CLIENT_ID"); 10 | } 11 | const discordSdk = new DiscordSDK(process.env.CLIENT_ID); 12 | await discordSdk.ready(); 13 | 14 | // Pop open the OAuth permission modal and request for access to scopes listed in scope array below 15 | const { code } = await discordSdk.commands.authorize({ 16 | client_id: process.env.CLIENT_ID, 17 | response_type: 'code', 18 | state: '', 19 | prompt: 'none', 20 | scope: [ 21 | 'identify', 22 | 'applications.commands', 23 | 'rpc.activities.write', 24 | 'rpc.voice.read', 25 | ], 26 | }); 27 | 28 | // Retrieve an access_token from your embedded app's server 29 | const response = await fetch('/api/token', { 30 | method: 'POST', 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | }, 34 | body: JSON.stringify({ 35 | code, 36 | }), 37 | }); 38 | 39 | const { access_token } = await response.json(); 40 | 41 | // Authenticate with Discord client (using the access_token) 42 | await discordSdk.commands.authenticate({ 43 | access_token, 44 | }); 45 | 46 | return discordSdk; 47 | } 48 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/functions/templates/index.ts: -------------------------------------------------------------------------------- 1 | // HTML templates for error pages 2 | export const AUTH_REQUIRED_HTML = ` 3 | 4 | 5 | Discord Authentication Required 6 | 17 | 18 | 19 |

🔒 Discord Authentication Required

20 |

This application requires Discord proxy authentication.

21 |

Please access this app through Discord's embedded app system.

22 |

Add ?proxy_auth=log-only to bypass auth for testing.

23 | 24 | `; 25 | 26 | export const SERVER_ERROR_HTML = ` 27 | 28 | 29 | Server Error 30 | 40 | 41 | 42 |

⚠️ Server Error

43 |

An error occurred while validating authentication.

44 |

Add ?proxy_auth=disabled to skip auth for debugging.

45 | 46 | `; 47 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/WindowSizeTracker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useInterval, useUpdate} from 'react-use'; 3 | 4 | export default function WindowSizeTracker() { 5 | const update = useUpdate(); 6 | // Update UI every second 7 | useInterval(update, 1000); 8 | 9 | const windowSizesRef = React.useRef>([ 10 | {width: window.innerWidth, height: window.innerHeight}, 11 | ]); 12 | React.useEffect(() => { 13 | function handleResize(ev: Event) { 14 | const target = ev.target as Window; 15 | windowSizesRef.current.unshift({width: target.innerWidth, height: target.innerHeight}); 16 | } 17 | window.addEventListener('resize', handleResize, true); 18 | return () => { 19 | window.removeEventListener('resize', handleResize, true); 20 | }; 21 | }, []); 22 | 23 | return ( 24 |
25 |

Window Resize Tracker

26 |

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 |

30 | 33 |
34 | {windowSizesRef.current.map((windowSize) => ( 35 |
{JSON.stringify(windowSize)}
36 | ))} 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "test": "$npm_execpath run tsc", 8 | "dev": "vite --mode dev", 9 | "staging": "vite --mode staging", 10 | "build": "tsc && vite build", 11 | "build-staging": "tsc && vite build --mode staging", 12 | "preview": "vite preview", 13 | "tsc": "tsc" 14 | }, 15 | "dependencies": { 16 | "@discord/embedded-app-sdk": "^2.4.0", 17 | "@radix-ui/colors": "^3.0.0", 18 | "@radix-ui/react-scroll-area": "^1.0.1", 19 | "@stitches/react": "^1.2.1", 20 | "lodash": "^4.17.21", 21 | "react": "^18.2.0", 22 | "react-device-detect": "^2.2.2", 23 | "react-dom": "^18.2.0", 24 | "react-json-tree": "^0.19.0", 25 | "react-router": "^6.16.0", 26 | "react-router-dom": "^6.16.0", 27 | "react-use": "^17.2.4", 28 | "shared": "workspace:*", 29 | "web-vitals": "^3.0.0", 30 | "zustand": "^4.1.4" 31 | }, 32 | "devDependencies": { 33 | "@cloudflare/workers-types": "^4.0.0", 34 | "@testing-library/jest-dom": "^6.4.2", 35 | "@testing-library/react": "^16.0.0", 36 | "@testing-library/user-event": "^14.0.0", 37 | "@types/jest": "^30.0.0", 38 | "@types/lodash": "^4.14.200", 39 | "@types/node": "^22.0.0", 40 | "@types/react": "^18.2.34", 41 | "@types/react-dom": "^18.2.7", 42 | "@vitejs/plugin-react": "^4.0.0", 43 | "typescript": "~5.9.0", 44 | "vite": "^5.2.9" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/LayoutMode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import discordSdk from '../discordSdk'; 3 | import {Common, EventPayloadData} from '@discord/embedded-app-sdk'; 4 | 5 | export default function LayoutMode() { 6 | const [layoutModeString, setLayoutModeString] = React.useState(''); 7 | 8 | const handleLayoutModeUpdate = React.useCallback((update: EventPayloadData<'ACTIVITY_LAYOUT_MODE_UPDATE'>) => { 9 | const layoutMode = update.layout_mode; 10 | let layoutModeStr = ''; 11 | switch (layoutMode) { 12 | case Common.LayoutModeTypeObject.FOCUSED: { 13 | layoutModeStr = 'FOCUSED'; 14 | break; 15 | } 16 | case Common.LayoutModeTypeObject.PIP: { 17 | layoutModeStr = 'PIP'; 18 | break; 19 | } 20 | case Common.LayoutModeTypeObject.GRID: { 21 | layoutModeStr = 'GRID'; 22 | break; 23 | } 24 | case Common.LayoutModeTypeObject.UNHANDLED: { 25 | layoutModeStr = 'UNHANDLED'; 26 | break; 27 | } 28 | } 29 | 30 | setLayoutModeString(layoutModeStr); 31 | }, []); 32 | 33 | React.useEffect(() => { 34 | discordSdk.subscribe('ACTIVITY_LAYOUT_MODE_UPDATE', handleLayoutModeUpdate); 35 | return () => { 36 | discordSdk.unsubscribe('ACTIVITY_LAYOUT_MODE_UPDATE', handleLayoutModeUpdate); 37 | }; 38 | }, [handleLayoutModeUpdate]); 39 | 40 | return ( 41 |
42 |

Embedded App Layout Mode

43 |

{layoutModeString}

44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/OpenExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import discordSdk from "../discordSdk"; 3 | 4 | enum LinkStatus { 5 | NOT_CLICKED = "NOT_CLICKED", 6 | CLICKED = "CLICKED", 7 | NOT_OPENED = "NOT_OPENED", 8 | OPENED = "OPENED", 9 | } 10 | 11 | // Note: we're still using the anchor tag, to ensure standard accessibility UX 12 | export default function OpenExternalLink() { 13 | const [linkStatus, setLinkStatus] = React.useState(LinkStatus.NOT_CLICKED); 14 | 15 | async function handleLinkClicked( 16 | e: React.MouseEvent 17 | ) { 18 | e.preventDefault(); 19 | setLinkStatus(LinkStatus.CLICKED); 20 | const { opened } = await discordSdk.commands.openExternalLink({ 21 | url: "https://google.com", 22 | }); 23 | if (opened) { 24 | setLinkStatus(LinkStatus.OPENED); 25 | } else { 26 | setLinkStatus(LinkStatus.NOT_OPENED); 27 | } 28 | } 29 | 30 | return ( 31 |
39 |

Open external link

40 |

41 | Click here to go to google:{" "} 42 | 43 | Google! 44 | 45 |

46 |
Link Status: {linkStatus}
47 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/AppStyles.tsx: -------------------------------------------------------------------------------- 1 | import {styled} from './styled'; 2 | import {Link} from 'react-router-dom'; 3 | import * as ScrollArea from '@radix-ui/react-scroll-area'; 4 | 5 | export const Navigation = styled(ScrollArea.Root, { 6 | height: '100%', 7 | width: '300px', 8 | overflow: 'auto', 9 | border: '1px solid', 10 | borderColor: '$slate12', 11 | }); 12 | 13 | export const Ul = styled('ul', { 14 | display: 'flex', 15 | flexDirection: 'column', 16 | paddingBottom: '36px', 17 | }); 18 | 19 | export const Li = styled(Link, { 20 | padding: '24px', 21 | textDecoration: 'none', 22 | color: '$slate12', 23 | '&:visited': { 24 | color: '$slate12', 25 | }, 26 | '&:hover': { 27 | backgroundColor: '$slate4', 28 | }, 29 | variants: { 30 | selected: { 31 | true: { 32 | backgroundColor: '$indigo6', 33 | color: '$indigo12', 34 | '&:visited': { 35 | color: '$indigo12', 36 | }, 37 | '&:hover': { 38 | backgroundColor: '$indigo7', 39 | }, 40 | }, 41 | }, 42 | }, 43 | }); 44 | 45 | export const Input = styled('input', { 46 | padding: '8px', 47 | marginBottom: '8px', 48 | width: '100%', 49 | border: '1px solid', 50 | borderColor: '$slate12', 51 | borderRadius: '4px', 52 | }); 53 | 54 | export const SiteWrapper = styled('div', { 55 | display: 'flex', 56 | height: '100%', 57 | color: '$slate12', 58 | flexDirection: 'row', 59 | '@small': { 60 | flexDirection: 'column', 61 | }, 62 | '@xsmall': { 63 | flexDirection: 'column', 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /discord-activity-starter/packages/client/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/components/Scrollable.tsx: -------------------------------------------------------------------------------- 1 | import {styled} from '../styled'; 2 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; 3 | 4 | const SCROLLBAR_SIZE = 10; 5 | 6 | export const Root = styled(ScrollAreaPrimitive.Root, { 7 | overflow: 'hidden', 8 | }); 9 | 10 | export const Viewport = styled(ScrollAreaPrimitive.Viewport, { 11 | width: '100%', 12 | height: '100%', 13 | borderRadius: 'inherit', 14 | }); 15 | 16 | export const Scrollbar = styled(ScrollAreaPrimitive.Scrollbar, { 17 | display: 'flex', 18 | // ensures no selection 19 | userSelect: 'none', 20 | // disable browser handling of all panning and zooming gestures on touch devices 21 | touchAction: 'none', 22 | padding: 2, 23 | background: '$slate6', 24 | transition: 'background 160ms ease-out', 25 | '&:hover': {background: '$slate8'}, 26 | '&[data-orientation="vertical"]': {width: SCROLLBAR_SIZE}, 27 | '&[data-orientation="horizontal"]': { 28 | flexDirection: 'column', 29 | height: SCROLLBAR_SIZE, 30 | }, 31 | }); 32 | 33 | export const Thumb = styled(ScrollAreaPrimitive.Thumb, { 34 | flex: 1, 35 | background: '$slate10', 36 | borderRadius: SCROLLBAR_SIZE, 37 | // increase target size for touch devices https://www.w3.org/WAI/WCAG21/Understanding/target-size.html 38 | position: 'relative', 39 | '&::before': { 40 | content: '""', 41 | position: 'absolute', 42 | top: '50%', 43 | left: '50%', 44 | transform: 'translate(-50%, -50%)', 45 | width: '100%', 46 | height: '100%', 47 | minWidth: 44, 48 | minHeight: 44, 49 | }, 50 | }); 51 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/ThermalStates.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import discordSdk from '../discordSdk'; 3 | import {Common, Events, EventPayloadData} from '@discord/embedded-app-sdk'; 4 | 5 | export default function ThermalStates() { 6 | const [thermalStateString, setThermalStateString] = React.useState(''); 7 | 8 | const updateThermalState = React.useCallback( 9 | (update: EventPayloadData<'THERMAL_STATE_UPDATE'>) => { 10 | const thermalState = update.thermal_state; 11 | let state; 12 | switch (thermalState) { 13 | case Common.ThermalStateTypeObject.NOMINAL: 14 | state = 'NOMINAL'; 15 | break; 16 | case Common.ThermalStateTypeObject.FAIR: 17 | state = 'FAIR'; 18 | break; 19 | case Common.ThermalStateTypeObject.SERIOUS: 20 | state = 'SERIOUS'; 21 | break; 22 | case Common.ThermalStateTypeObject.CRITICAL: 23 | state = 'CRITICAL'; 24 | break; 25 | default: 26 | state = 'UNHANDLED'; 27 | break; 28 | } 29 | 30 | setThermalStateString(state); 31 | }, 32 | [setThermalStateString], 33 | ); 34 | 35 | React.useEffect(() => { 36 | discordSdk.subscribe(Events.THERMAL_STATE_UPDATE, updateThermalState); 37 | return () => { 38 | discordSdk.unsubscribe(Events.THERMAL_STATE_UPDATE, updateThermalState); 39 | }; 40 | }, [updateThermalState]); 41 | 42 | return ( 43 |
44 |

Thermal States

45 |

latest thermal state: {thermalStateString}

46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/CurrentGuildMember.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import discordSdk from '../discordSdk'; 3 | import ReactJsonView from '../components/ReactJsonView'; 4 | import {useLocation} from 'react-router-dom'; 5 | import {EventPayloadData} from '@discord/embedded-app-sdk'; 6 | 7 | export default function CurrentGuildMember() { 8 | const [currentGuildMember, setCurrentGuildMember] = 9 | React.useState | null>(null); 10 | const location = useLocation(); 11 | 12 | React.useEffect(() => { 13 | const {channelId} = discordSdk; 14 | if (!channelId) return; 15 | 16 | const handleCurrentGuildMemberUpdate = ( 17 | currentGuildMemberEvent: EventPayloadData<'CURRENT_GUILD_MEMBER_UPDATE'>, 18 | ) => { 19 | setCurrentGuildMember(currentGuildMemberEvent); 20 | }; 21 | 22 | const guildId = discordSdk.guildId; 23 | if (guildId) { 24 | discordSdk.subscribe('CURRENT_GUILD_MEMBER_UPDATE', handleCurrentGuildMemberUpdate, { 25 | guild_id: guildId, 26 | }); 27 | } 28 | 29 | return () => { 30 | if (guildId) { 31 | discordSdk.unsubscribe('CURRENT_GUILD_MEMBER_UPDATE', handleCurrentGuildMemberUpdate, { 32 | guild_id: guildId, 33 | }); 34 | } 35 | }; 36 | }, [location.search]); 37 | 38 | return ( 39 |
40 |
41 |

Event Subscription

42 |

CURRENT_GUILD_MEMBER_UPDATE

43 |
44 |
45 | {currentGuildMember ? : null} 46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/ActivityChannel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import discordSdk from '../discordSdk'; 3 | import ReactJsonView from '../components/ReactJsonView'; 4 | import {authStore} from '../stores/authStore'; 5 | import {ISDKError, type Types} from '@discord/embedded-app-sdk'; 6 | 7 | function instanceOfSdkError(object: any): object is ISDKError { 8 | return 'code' in object && 'message' in object; 9 | } 10 | 11 | export default function CurrentGuild() { 12 | const [channel, setChannel] = React.useState(null); 13 | const [error, setError] = React.useState(null); 14 | const auth = authStore(); 15 | 16 | React.useEffect(() => { 17 | async function update() { 18 | if (!auth) { 19 | return; 20 | } 21 | 22 | const channelId = discordSdk.channelId; 23 | if (channelId == null) { 24 | return; 25 | } 26 | 27 | try { 28 | const newChannel = await discordSdk.commands.getChannel({channel_id: channelId}); 29 | 30 | setChannel(newChannel); 31 | } catch (error: any) { 32 | if (instanceOfSdkError(error)) { 33 | setError(error); 34 | } 35 | } 36 | } 37 | update(); 38 | }, [auth]); 39 | 40 | return ( 41 |
42 |
43 |

Activity Channel

44 |
45 |
46 | {channel != null ? : null} 47 | {error != null ? ( 48 |
49 |

Error

50 | 51 |
52 | ) : null} 53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/SafeAreas.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function SafeAreas() { 4 | const [insets, setInsets] = React.useState<{ 5 | top: string; 6 | bottom: string; 7 | left: string; 8 | right: string; 9 | }>({ 10 | top: '0px', 11 | bottom: '0px', 12 | left: '0px', 13 | right: '0px', 14 | }); 15 | 16 | const measure = () => { 17 | // The safe area css values are not correct if read right away. 18 | // Sometimes the values are stale, or the numerical values are incorrect. 19 | // Performing this read on the next tick gives accurate results 20 | setTimeout(() => { 21 | const left = getComputedStyle(document.documentElement).getPropertyValue('--sail'); 22 | const right = getComputedStyle(document.documentElement).getPropertyValue('--sair'); 23 | const top = getComputedStyle(document.documentElement).getPropertyValue('--sait'); 24 | const bottom = getComputedStyle(document.documentElement).getPropertyValue('--saib'); 25 | 26 | setInsets({ 27 | top, 28 | bottom, 29 | left, 30 | right, 31 | }); 32 | }, 0); 33 | }; 34 | 35 | React.useEffect(() => { 36 | addEventListener('resize', measure); 37 | measure(); 38 | return () => { 39 | removeEventListener('resize', measure); 40 | }; 41 | }, []); 42 | 43 | return ( 44 |
45 |

Safe Areas

46 |
47 |
48 | top: {insets.top} 49 |
50 |
51 | bottom: {insets.bottom} 52 |
53 |
54 | left: {insets.left} 55 |
56 |
57 | right: {insets.right} 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/sdk-playground-production.yml: -------------------------------------------------------------------------------- 1 | name: sdk playground production deploy 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | jobs: 7 | build: 8 | environment: production 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v5 12 | - uses: pnpm/action-setup@v4 13 | with: 14 | version: latest 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 22 18 | cache: pnpm 19 | - run: pnpm install 20 | - name: create env file 21 | run: | 22 | cd sdk-playground 23 | echo "${{ secrets.ENV_FILE }}" > .env.production 24 | - name: build sdk-playground shared 25 | run: | 26 | cd sdk-playground/packages/shared 27 | pnpm build 28 | - name: build sdk-playground client 29 | run: | 30 | cd sdk-playground/packages/client 31 | pnpm build 32 | - name: deploy client 33 | uses: cloudflare/wrangler-action@v3.14.1 34 | with: 35 | packageManager: pnpm 36 | apiToken: ${{ secrets.CF_API_TOKEN }} 37 | accountId: ${{ secrets.CF_ACCOUNT_ID }} 38 | workingDirectory: sdk-playground/packages/client 39 | command: pages deploy --project-name=sdk-playground dist 40 | wranglerVersion: '3.52.0' 41 | - name: deploy server 42 | uses: cloudflare/wrangler-action@v3.14.1 43 | with: 44 | packageManager: pnpm 45 | apiToken: ${{ secrets.CF_API_TOKEN }} 46 | accountId: ${{ secrets.CF_ACCOUNT_ID }} 47 | preCommands: ./handle-wrangler-secrets.sh production remote 48 | workingDirectory: sdk-playground/packages/server 49 | command: publish src/index.ts --env production 50 | wranglerVersion: '3.52.0' 51 | -------------------------------------------------------------------------------- /sdk-playground/packages/client/src/pages/ShareLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import discordSdk from '../discordSdk'; 3 | 4 | export default function ShareLink() { 5 | const [message, setMessage] = React.useState('Come Play SDK Playground!'); 6 | const [customId, setCustomId] = React.useState(undefined); 7 | 8 | const [hasPressedSend, setHasPressedSend] = React.useState(false); 9 | const [didSend, setDidSend] = React.useState(false); 10 | 11 | const handleMessageChange= (event: React.ChangeEvent) => { 12 | setMessage(event.target.value); 13 | }; 14 | const handleCustomIdChange = (event: React.ChangeEvent) => { 15 | setCustomId(event.target.value); 16 | }; 17 | 18 | const doShareLink = async () => { 19 | const { success} = await discordSdk.commands.shareLink({ 20 | message, 21 | custom_id: customId, 22 | }); 23 | setHasPressedSend(true); 24 | setDidSend(success); 25 | }; 26 | 27 | return ( 28 |
29 |

Message:

30 |