ton-connect / sdk

SDK for TON Connect 2.0 — a comprehensive communication protocol between wallets and apps in TON ecosystem
Apache License 2.0
165 stars 40 forks source link

[<UI>]: TonConnectUI from @tonconnect/ui with Vite without affecting its Hot Module Replacement (HMR) feature #89

Open designervoid opened 9 months ago

designervoid commented 9 months ago

In the context of Vite development, how might you ensure TonConnectUI from @tonconnect/ui doesn't interfere with HMR?

For instance, let's consider an application with @tonconnect/ui + Solid.js + Vite. We have an updated App.tsx after the command pnpm create vite solidjs-vite-tonconnect2.0 --template solid-ts:

import { createSignal, onMount } from 'solid-js'
import { TonConnectUI } from '@tonconnect/ui'
import './App.css'

function App() {
const [connectedWallet, setConnectedWallet] = createSignal<boolean>(localStorage.getItem('dAppToken') ? true : false);
  const tonConnectUI = new TonConnectUI({
    manifestUrl: 'https://about.systemdesigndao.xyz/ton-connect.manifest.json',
    buttonRootId: null,
  });

  onMount(() => {
    const run = async () => {
       tonConnectUI.uiOptions = {
          ...tonConnectUI.uiOptions,
          buttonRootId: 'ton-connect2-container',
      }

      // enable ui loader
      tonConnectUI.setConnectRequestParameters({ state: 'loading' });

      // fetch you tonProofPayload from the backend
      const d = await postData('https://demo.tonconnect.dev/ton-proof/generatePayload');
      const { payload } = await d.json();

      if (!payload) {
          // remove loader, connect request will be without any additional parameters
          tonConnectUI.setConnectRequestParameters(null);
      } else {
          // add tonProof to the connect request
          tonConnectUI.setConnectRequestParameters({
              state: "ready",
              value: { tonProof: payload }
          });
      }

      const unsubscribe = tonConnectUI.onStatusChange(
        async wallet => {
          if (!wallet) {
            return;
          }

          const tonProof = wallet.connectItems?.tonProof;

          if (tonProof) {
            if ('proof' in tonProof) {
              const obj = {
                  proof: {
                    ...tonProof.proof,
                    state_init: wallet.account.walletStateInit,
                  },
                  network: wallet.account.chain,
                  address: wallet.account.address
              };

              try {
                tonConnectUI.setConnectRequestParameters({
                  state: "loading",
                });

                const d = await postData('https://demo.tonconnect.dev/ton-proof/checkProof', obj);
                const { token } = await d.json();
                const r = await fetch(`https://demo.tonconnect.dev/dapp/getAccountInfo?network=${obj.network}`, {
                        headers: {
                          Authorization: Bearer ${token},
                          'Content-Type': 'application/json',
                        }
                });
                const data = await r.json();

                console.log('success: ', data);

                tonConnectUI.setConnectRequestParameters({
                  state: "ready",
                  value: { tonProof: payload }
                });

                setConnectedWallet(true);
                localStorage.setItem('dAppToken', `Bearer ${token}`);
              } catch (err) {
                console.error(err);

                setConnectedWallet(false);
                localStorage.removeItem('dAppToken');
              }
            }
          }
        }
      );

      return unsubscribe;
    }

    run();
  });

  return (
    <>
      <div id="ton-connect2-container"></div>
      {connectedWallet() && <div style={{ 'display': 'flex', 'align-items': 'center', "flex-direction": "column" }}>
        <pre>Connected: {JSON.stringify(connectedWallet())}</pre>
      </div>}
    </>
  )
}

export default App

Then, we insert something into jsx and save - HMR doesn't trigger.

Let's consider the next scenario where we remove everything related to tonconnect2.0:

import './App.css'

function App() {  
  return (
    <>
      <span>hmr works 1</span> 
    </>
  )
}

export default App

Restart Vite with Ctrl + C and then pnpm run dev. This is needed because HMR isn't working. Normally, you'd only see the span update without the tonconnect2.0 logic. After restarting, you should see the span displaying 'hmr works 1'

Insert:

      <br />
      <span>hmr works 2</span>

Save.

And we see that the new span was inserted into jsx and rendered in the browser.

However, if we keep the logic related to ton-connect2.0, then #tc-root won't re-render and we'll see errors in the browser console:

web-api.ts:87 Uncaught (in promise) DOMException: Failed to execute 'define' on 'CustomElementRegistry': the name "tc-root" has already been used with this registry
    at defineStylesRoot (http://localhost:4001/node_modules/.vite/deps/@tonconnect_ui.js?v=bd6b2e50:7465:18)
    at App (http://localhost:4001/node_modules/.vite/deps/@tonconnect_ui.js?v=bd6b2e50:15244:3)
    at http://localhost:4001/node_modules/.vite/deps/@tonconnect_ui.js?v=bd6b2e50:6411:24
    at untrack (http://localhost:4001/node_modules/.vite/deps/@tonconnect_ui.js?v=bd6b2e50:5951:12)
    at createComponent (http://localhost:4001/node_modules/.vite/deps/@tonconnect_ui.js?v=bd6b2e50:6411:10)
    at http://localhost:4001/node_modules/.vite/deps/@tonconnect_ui.js?v=bd6b2e50:15298:51
    at http://localhost:4001/node_modules/.vite/deps/@tonconnect_ui.js?v=bd6b2e50:6764:53
    at updateFn (http://localhost:4001/node_modules/.vite/deps/@tonconnect_ui.js?v=bd6b2e50:5770:38)
    at runUpdates (http://localhost:4001/node_modules/.vite/deps/@tonconnect_ui.js?v=bd6b2e50:6182:17)
    at createRoot (http://localhost:4001/node_modules/.vite/deps/@tonconnect_ui.js?v=bd6b2e50:5774:12)
designervoid commented 9 months ago

src/ton-connect-ui-build/index.js:

image

src/ton-connect-ui-build/index.js:

image

src/App.tsx:

image

As a temporary solution, I'm using a bad practice patch, utilizing the built index.js + index.d.ts from node_modules.

With this patch, the UI component doesn't break during HMR.

designervoid commented 8 months ago

I added context to the SolidJS app, and developer experience improved. However, if I modify the files associated with @tonconnect/ui, the render is triggered again and the web component disappears in dev mode.

miohtama commented 1 month ago

This same issue happens with Svelte/Vite both production and development builds. I believe any frontend system that is dynamically creating pages, which may include recreating #tc-root element, might suffer from this, and some kind of workaround is needed.