Closed alula closed 5 months ago
I ran into the same issue trying to reload for debugging. My minimum reproduction as a single html file:
<html>
<head>
<script type="module">
const { DiscordSDK } = await import("https://unpkg.com/@discord/embedded-app-sdk@1.0.0/output/index.mjs");
const discordSdk = new DiscordSDK(CLIENT_ID);
document.body.innerHTML += "<p>Waiting for DiscordSDK to be ready...</p>";
await discordSdk.ready();
document.body.innerHTML += "<p>DiscordSDK is ready! Reloading in 2 seconds.</p>";
await new Promise(res => setTimeout(res, 2000));
window.location.reload();
</script>
</head>
<body></body>
</html>
I think I've found some clues!
The SDK uses document.referrer
to figure out the origin of parent window to limit it to the only recipient of RPC messages (in theory):
Before reload the value of document.referrer
is https://discord.com/
.
After reload the value of document.referrer
is equal to URL of your iframe, which is passed to postMessage()
, which means the application sends messages to itself instead of Discord client.
Technically you can mess with sourceOrigin
property of Discord SDK (note: this impacts security, I discourage doing this on production!!):
const discordSdk = new DiscordSDK(import.meta.env.VITE_CLIENT_ID, { disableConsoleLogOverride: true });
if (import.meta.env.DEV) {
const discordSdkAny = discordSdk as any;
if (!discordSdkAny.sourceOrigin.includes("discord.com")) {
discordSdkAny.sourceOrigin = "*";
discordSdkAny.handshake();
}
}
However, Discord client doesn't seem to like getting a handshake from reloaded frame:
Even hacking it to set isReady
to true, makes commands like discordSdk.commands.authorize()
fail with "Already authenticated" error.
Working around this seems to require the formerly mentioned hack and caching of authentication/user data, eg. in SessionStorage.
The problem seems to lie in both the library and clients.
This is hacky and horrible but might be useful for someone: https://gist.github.com/alula/2e9f6874bd7f12d4ca9201347979ac92
this really needs to be fixed, and it really negatively impacts developer experience.
my current "workaround" is to use DiscordSDKMock
and test the activity through a browser instead of Discord, instead of trying to do some hacky solution. (but this doesn't fix the hot reloading issue.)
DiscordSDKMock
reference: https://github.com/discord/embedded-app-sdk/blob/main/examples/react-colyseus/packages/client/src/discordSdk.tsx
I believe they really only intended this to be used with single pages apps which do not do full page refreshes. With my SPA I use hot reloading in development and it sometimes required a full page refresh so I listen to onbeforeunload
to run discordSdk.close()
.
I listen to
onbeforeunload
to rundiscordSdk.close()
.
That closes the activity for me. You got it to work with a location.reload()
?
No, what I am saying is just force the activity to close when a reload is detected. https://github.com/AshMW2724/react-discord-activity/blob/main/src/hooks/useAuthenticatedContext.tsx#L49
The client allows only one handshake to be performed and when the window is reloaded the fake RPC connection is kept in same state, client is unaware of anything that happened, but your app is losing all the state.
The only workaround for now to support reloads/redirects is to somehow keep the state of discord authentication (eg. through sessionStorage) and use the workaround for the origin bug in app SDK.
Even hacking it to set
isReady
to true, makes commands likediscordSdk.commands.authorize()
fail with "Already authenticated" error.Working around this seems to require the formerly mentioned hack and caching of authentication/user data, eg. in SessionStorage.
The problem seems to lie in both the library and clients.
This is hacky and horrible but might be useful for someone: https://gist.github.com/alula/2e9f6874bd7f12d4ca9201347979ac92
does this work on mobile as well? I'm using LocalStorage as well now, but I wrote my own code for it. I could be wrong, but I think every time I initialize DiscordSDK, call the ready function, then initalize the class again, the activity automatically closes on mobile (android).
Unfortunately, the SDK (and the matching RPC server which it is talking to) is designed to support only one "ready handshake" per iframe mounted. Once the handshake between a Discord client and the iframe is complete, it will not issue additional "READY" payloads. As noted here activities are intended for "single page apps" (SPAs).
This means your activity needs to only initialize the SDK once per activity instance.
When it comes to a smooth developer flow, one optimization to explore is isolating code which calls new DiscordSDK()
and discordSdk.ready
from the rest of your flow to reduce the chance of waiting for reinitialization of the SDK.
Unfortunately, the SDK (and the matching RPC server which it is talking to) is designed to support only one "ready handshake" per iframe mounted. Once the handshake between a Discord client and the iframe is complete, it will not issue additional "READY" payloads.
I appreciate this detail and was able to reference the Colyseus example alongside it to find a clean solution! ✨
For those interested, you can avoid having Vite's HMR re-create the SDK instance in a React app by managing it in a context provider close to the root node. This is what the react-colyseus
and sdk-playground
examples do to avoid this issue.
My team has a minimal example of this in our framework's template example for more... context: https://github.com/Wave-Play/robo.js/tree/main/templates/starter-app-ts-react#authenticating
I do really prefer to have the option to do a full reload in some scenarios, like in the event of an update or an unrecoverable error.
Any chance the promise could resolve to an error instead of just hanging?
I'm also struggling greatly without a functioning reload. I'm currently tasked with getting Worlds FRVR into Discord activities, and we reload under a number of circumstances. In development, the obvious case of applying any changes that can't be hot-reloaded, so not having it there is rather annoying (but not a show stopper, even if physically painful, as I have to reach for the mouse to restart the activity). In production, we do reloads when we apply updates (absolutely critical, as, if it's a "breaking"/protocol change, old clients will simply not work, and even if a non-breaking change, old clients are going to be loading new data which will lead to undefined behavior). We also do reloads in production to deal with other rare situations that can't be reasonably gracefully handled (e.g. user changing antialiasing settings, which requires re-creating the canvas/WebGL context and all dynamic GPU data). Without reloads functioning we're just going to have do the discordSDK.close()
approach, which is a pretty crappy experience for the end-user (and frustrating for development).
The recommendation seems to be to make another nested iframe, which seems yucky to me. I was able to work around it in my simple prototype, but it seems my only viable option for updates is to either the nested iframes or killing the activity as you suggest.
While prototyping an app, I've found out that
discordSdk.ready()
never completes after full page reload. The issue has also been mentioned in #activities-dev-help channel on Discord Developers server.How to reproduce:
pnpm dev
main.ts
to make Vite reload, dowindow.location.reload()
or whatever else from your application.I've tested and reproduced the issue on following Discord clients: