Repository: skip-mev/skip-go
Pull request #1055: [FRE-1668] Update web-component to allow passing props in the same format as the react component (camelCase)
Author: login: toddkao
Created: 2025-04-11T17:09:42Z | Merged: 2025-04-15T17:15:01Z



Diff:
diff --git a/.changeset/strange-taxis-talk.md b/.changeset/strange-taxis-talk.md
new file mode 100644
index 000000000..aca629e68
--- /dev/null
+++ b/.changeset/strange-taxis-talk.md
@@ -0,0 +1,5 @@
+---
+"@skip-go/widget": patch
+---
+
+Update web-component to allow passing props via javascript properties
diff --git a/docs/widget/web-component.mdx b/docs/widget/web-component.mdx
index b83f1c800..fcb3608db 100644
--- a/docs/widget/web-component.mdx
+++ b/docs/widget/web-component.mdx
@@ -33,20 +33,24 @@ This can be added to npm scripts in `package.json`, a `.env file`, or used when
 
 ## Usage
 
-Props are the same as [`WidgetProps`](./configuration), but passed as attributes in kebab-case. Use strings or stringified objects for complex props.
+Props are the exact same as [`WidgetProps`](./configuration) but you are required to pass them to the element via javascript/typescript.
 
 ```tsx
 <div style="width:100%; max-width:500px; padding:0 10px;">
-  <skip-widget
-    theme='{
-      "brandColor": "#FF4FFF",
-    }'
-    default-route='{
-      "srcChainId": "osmosis-1",
-      "srcAssetDenom": "ibc/1480b8fd20ad5fcae81ea87584d269547dd4d436843c1d20f15e00eb64743ef4"
-    }'
-  ></skip-widget>
+  <skip-widget></skip-widget>
 </div>
+<script>
+  const skipWidget = document.querySelector("skip-widget");
+  if (skipWidget) {
+    skipWidget.theme = {
+      brandColor: "#FF4FFF",
+    };
+    skipWidget.defaultRoute = {
+      srcChainId: "osmosis-1",
+      srcAssetDenom: "ibc/1480b8fd20ad5fcae81ea87584d269547dd4d436843c1d20f15e00eb64743ef4",
+    }
+  }
+</script>
 ```
 
 ## Performance Considerations
diff --git a/examples/nuxtjs/app.vue b/examples/nuxtjs/app.vue
index 151fad1ab..87fd987fb 100644
--- a/examples/nuxtjs/app.vue
+++ b/examples/nuxtjs/app.vue
@@ -1,19 +1,17 @@
 <template>
   <div>
     <div style="width:100%; max-width:500px; padding: 0 10px;">
-      <skip-widget
-        theme='{
-          "backgroundColor": "#191A1C",
-          "textColor": "#E6EAE9",
-          "borderColor": "#363B3F",
-          "brandColor": "#FF4FFF",
-          "highlightColor": "#1F2022"
-        }'
-        default-route='{
-          "srcChainID": "osmosis-1",
-          "srcAssetDenom": "ibc/1480b8fd20ad5fcae81ea87584d269547dd4d436843c1d20f15e00eb64743ef4"
-        }'>
-      </skip-widget>
+      <skip-widget></skip-widget>
     </div>
   </div>
 </template>
+
+<script setup>
+  const skipWidget = document.querySelector("skip-widget");
+
+  if (skipWidget) {
+    skipWidget.onRouteUpdated = (route) => {
+      console.log("route updated", route);
+    };
+  }
+</script>
\ No newline at end of file
diff --git a/examples/raw-html.html b/examples/raw-html.html
index 8f90d301c..07b466dab 100644
--- a/examples/raw-html.html
+++ b/examples/raw-html.html
@@ -7,15 +7,17 @@
       type="module"
     ></script>
     <div style="width: 100%; max-width: 500px; padding: 0 10px">
-      <skip-widget
-        theme='{
-      "backgroundColor": "#191A1C",
-      "textColor": "#E6EAE9",
-      "borderColor": "#363B3F",
-      "brandColor": "#FF4FFF",
-      "highlightColor": "#1F2022"
-    }'
-      ></skip-widget>
+      <skip-widget></skip-widget>
     </div>
   </body>
 </html>
+
+<script>
+  const skipWidget = document.querySelector("skip-widget");
+
+  if (skipWidget) {
+    skipWidget.onRouteUpdated = (route) => {
+      console.log("route updated", route);
+    };
+  }
+</script>
\ No newline at end of file
diff --git a/packages/widget/src/devMode/loadWidget.tsx b/packages/widget/src/devMode/loadWidget.tsx
index b2de11693..51097e3f9 100644
--- a/packages/widget/src/devMode/loadWidget.tsx
+++ b/packages/widget/src/devMode/loadWidget.tsx
@@ -1,16 +1,18 @@
-import { StrictMode, useState } from "react";
+import { StrictMode, useEffect, useMemo, useState } from "react";
 import { createRoot } from "react-dom/client";
-import { Widget } from "@/widget/Widget";
+import "../web-component";
 import { Column, Row } from "@/components/Layout";
 import "./global.css";
-import { defaultTheme, lightTheme } from "@/widget/theme";
 import { resetWidget } from "@/state/swapPage";
+import { defaultTheme, lightTheme } from "@/widget/theme";
+import { Widget, WidgetProps } from "@/widget/Widget";
 
 const DevMode = () => {
   const [theme, setTheme] = useState<"dark" | "light">("dark");
   const [apiUrl, setApiUrl] = useState<"prod" | "dev">("prod");
   const [testnet, setTestnet] = useState<boolean>(false);
   const [disableShadowDom, setDisableShadowDom] = useState(true);
+  const [renderWebComponent, setRenderWebComponent] = useState(false);
 
   const toggleTheme = () => {
     if (theme === "dark") {
@@ -20,6 +22,65 @@ const DevMode = () => {
     }
   };
 
+  const widgetProps: WidgetProps = useMemo(() => {
+    return {
+      theme: {
+        ...(theme === "dark" ? defaultTheme : lightTheme),
+        brandTextColor: "black",
+        brandColor: "#FF66FF",
+      },
+      settings: {
+        useUnlimitedApproval: true,
+      },
+      disableShadowDom,
+      onlyTestnet: testnet,
+      routeConfig: {
+        experimentalFeatures: ["eureka"],
+      },
+      apiUrl:
+        apiUrl === "prod" ? "https://go.skip.build/api/skip" : "https://dev.go.skip.build/api/skip",
+      ibcEurekaHighlightedAssets: {
+        USDC: ["cosmoshub-4"],
+        USDT: undefined,
+      },
+      assetSymbolsSortedToTop: [
+        "LBTC",
+        "ATOM",
+        "USDC",
+        "USDT",
+        "ETH",
+        "TIA",
+        "OSMO",
+        "NTRN",
+        "INJ",
+      ],
+      filterOut: {
+        destination: {
+          "pacific-1": ["ibc/6C00E4AA0CC7618370F81F7378638AE6C48EFF8C9203CE1C2357012B440EBDB7"],
+          "1329": ["0xB75D0B03c06A926e488e2659DF1A861F860bD3d1"],
+          "1": ["0xbf45a5029d081333407cc52a84be5ed40e181c46"],
+        },
+      },
+      filterOutUnlessUserHasBalance: {
+        source: {
+          "1": ["0xbf45a5029d081333407cc52a84be5ed40e181c46"],
+        },
+      },
+    };
+  }, [apiUrl, disableShadowDom, testnet, theme]);
+
+  useEffect(() => {
+    const skipWidget = document.querySelector("skip-widget");
+    if (skipWidget) {
+      Object.entries(widgetProps).forEach(([key, value]) => {
+        // @ts-expect-error this is like the equivalent of
+        // spreading the props to the web-component
+        // but we dont expect users to do it like this
+        skipWidget[key as keyof WidgetProps] = value;
+      });
+    }
+  }, [widgetProps]);
+
   return (
     <Column align="flex-end">
       <Column gap={5} style={{ width: 200 }}>
@@ -35,6 +96,9 @@ const DevMode = () => {
         <button onClick={() => setApiUrl((v) => (v === "prod" ? "dev" : "prod"))}>
           {apiUrl === "prod" ? "prod" : "dev"}
         </button>
+        <button onClick={() => setRenderWebComponent((v) => !v)}>
+          web-component: {renderWebComponent.toString()}
+        </button>
       </Column>
       <Row
         style={{
@@ -55,55 +119,7 @@ const DevMode = () => {
             padding: "0 10px",
           }}
         >
-          <Widget
-            theme={{
-              ...(theme === "dark" ? defaultTheme : lightTheme),
-              brandTextColor: "black",
-              brandColor: "#FF66FF",
-            }}
-            settings={{
-              useUnlimitedApproval: true,
-            }}
-            disableShadowDom={disableShadowDom}
-            onlyTestnet={testnet}
-            routeConfig={{
-              experimentalFeatures: ["eureka"],
-            }}
-            apiUrl={
-              apiUrl === "prod"
-                ? "https://go.skip.build/api/skip"
-                : "https://dev.go.skip.build/api/skip"
-            }
-            ibcEurekaHighlightedAssets={{
-              USDC: ["cosmoshub-4"],
-              USDT: undefined,
-            }}
-            assetSymbolsSortedToTop={[
-              "LBTC",
-              "ATOM",
-              "USDC",
-              "USDT",
-              "ETH",
-              "TIA",
-              "OSMO",
-              "NTRN",
-              "INJ",
-            ]}
-            filterOut={{
-              destination: {
-                "pacific-1": [
-                  "ibc/6C00E4AA0CC7618370F81F7378638AE6C48EFF8C9203CE1C2357012B440EBDB7",
-                ],
-                "1329": ["0xB75D0B03c06A926e488e2659DF1A861F860bD3d1"],
-                "1": ["0xbf45a5029d081333407cc52a84be5ed40e181c46"],
-              },
-            }}
-            filterOutUnlessUserHasBalance={{
-              source: {
-                "1": ["0xbf45a5029d081333407cc52a84be5ed40e181c46"],
-              },
-            }}
-          />
+          {renderWebComponent ? <skip-widget /> : <Widget {...widgetProps} />}
         </div>
       </Row>
     </Column>
diff --git a/packages/widget/src/web-component.tsx b/packages/widget/src/web-component.tsx
index 9f165ff70..7bfd1e5c2 100644
--- a/packages/widget/src/web-component.tsx
+++ b/packages/widget/src/web-component.tsx
@@ -1,49 +1,58 @@
 /* eslint-disable @typescript-eslint/consistent-type-definitions */
-/* eslint-disable @typescript-eslint/no-namespace */
+
 import toWebComponent from "@r2wc/react-to-web-component";
-import { Widget } from "./widget/Widget";
+import { NewSkipClientOptions, Widget, WidgetProps } from "./widget/Widget";
 
-type WebComponentProps = {
-  container: {
-    attributes: Record<string, string>[];
-  };
-};
+const WEB_COMPONENT_NAME = "skip-widget";
 
-function isJsonString(str: string) {
-  try {
-    JSON.parse(str);
-  } catch (_err) {
-    return false;
-  }
-  return true;
-}
+type WebComponentProps = WidgetProps & NewSkipClientOptions;
 
-const camelize = (inputString: string) => {
-  inputString = inputString.toLowerCase();
-  return inputString.replace(/-./g, (x) => x[1].toUpperCase());
+type PropDescriptors = {
+  [K in keyof WebComponentProps]: "any";
 };
 
-const WidgetWithProvider = (props: WebComponentProps) => {
-  const parsedProps = Array.from(props.container.attributes).map(({ name, value }) => {
-    return { key: name, value };
-  });
-
-  const realProps = parsedProps.reduce(
-    (accumulator, initialValue) => {
-      const { key, value } = initialValue;
-
-      accumulator[camelize(key)] = isJsonString(value) ? JSON.parse(value) : value;
-      return accumulator;
-    },
-    {} as Record<string, string>,
-  );
-
-  return <Widget {...realProps} />;
+const widgetPropTypes: Required<PropDescriptors> = {
+  rootId: "any",
+  theme: "any",
+  brandColor: "any",
+  onlyTestnet: "any",
+  defaultRoute: "any",
+  settings: "any",
+  routeConfig: "any",
+  filter: "any",
+  filterOut: "any",
+  filterOutUnlessUserHasBalance: "any",
+  walletConnect: "any",
+  enableSentrySessionReplays: "any",
+  enableAmplitudeAnalytics: "any",
+  connectedAddresses: "any",
+  simulate: "any",
+  disableShadowDom: "any",
+  ibcEurekaHighlightedAssets: "any",
+  assetSymbolsSortedToTop: "any",
+  hideAssetsUnlessWalletTypeConnected: "any",
+  apiUrl: "any",
+  apiKey: "any",
+  endpointOptions: "any",
+  aminoTypes: "any",
+  registryTypes: "any",
+  chainIdsToAffiliates: "any",
+  cacheDurationMs: "any",
+  getCosmosSigner: "any",
+  getEVMSigner: "any",
+  getSVMSigner: "any",
+  onWalletConnected: "any",
+  onWalletDisconnected: "any",
+  onTransactionBroadcasted: "any",
+  onTransactionComplete: "any",
+  onTransactionFailed: "any",
+  onRouteUpdated: "any",
 };
 
-const WEB_COMPONENT_NAME = "skip-widget";
-
-const WebComponent = toWebComponent(WidgetWithProvider);
+const WebComponent = toWebComponent(Widget, {
+  // @ts-expect-error any is not one of the valid types but it works
+  props: widgetPropTypes,
+});
 
 function initializeSkipWidget() {
   if (!customElements.get(WEB_COMPONENT_NAME)) {
@@ -51,23 +60,15 @@ function initializeSkipWidget() {
   }
 
   // Upgrade any existing skip-widget elements
-  document.querySelectorAll(WEB_COMPONENT_NAME).forEach((el) => {
-    customElements.upgrade(el);
-  });
+  document.querySelectorAll(WEB_COMPONENT_NAME).forEach((el) => customElements.upgrade(el as Node));
 }
 
 initializeSkipWidget();
 
 export default WebComponent;
 
-type Stringify<T> = {
-  [K in keyof T]?: string;
-};
-
 declare global {
-  namespace JSX {
-    interface IntrinsicElements {
-      [WEB_COMPONENT_NAME]: Stringify<WebComponentProps>;
-    }
+  interface HTMLElementTagNameMap {
+    [WEB_COMPONENT_NAME]: WidgetProps;
   }
 }
diff --git a/packages/widget/src/widget/Widget.tsx b/packages/widget/src/widget/Widget.tsx
index 968a2eb1d..e28b5e697 100644
--- a/packages/widget/src/widget/Widget.tsx
+++ b/packages/widget/src/widget/Widget.tsx
@@ -97,7 +97,7 @@ type NewSwapVenueRequest = {
   chainId: string;
 };
 
-type NewSkipClientOptions = Omit<SkipClientOptions, "apiURL" | "chainIDsToAffiliates"> & {
+export type NewSkipClientOptions = Omit<SkipClientOptions, "apiURL" | "chainIDsToAffiliates"> & {
   apiUrl?: string;
   chainIdsToAffiliates?: Record<string, ChainAffiliates>;
 };


Reviews:
user: login: toddkao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T20:08:33Z
user: login: toddkao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T20:15:07Z
user: login: toddkao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T20:15:52Z
user: login: toddkao
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T16:21:55Z
user: login: codingki
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-15T17:06:18Z

Review comments:
diff_hunk: @@ -60,14 +69,10 @@ initializeSkipWidget();
 
 export default WebComponent;
 
-type Stringify<T> = {
-  [K in keyof T]?: string;
-};
-
 declare global {
-  namespace JSX {
-    interface IntrinsicElements {
-      [WEB_COMPONENT_NAME]: Stringify<WebComponentProps>;
-    }
+  interface SkipWidgetElement extends HTMLElement, WidgetProps {}
+
+  interface HTMLElementTagNameMap {
+    [WEB_COMPONENT_NAME]: SkipWidgetElement;
path: packages/widget/src/web-component.tsx
user: login: toddkao
body: ![image](https://github.com/user-attachments/assets/ad1836b2-a0c8-4832-bc89-9a13f3e0f3f0)

extending `WidgetProps` means that you get type data for the element!
created_at: 2025-04-14T20:08:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,14 +69,10 @@ initializeSkipWidget();
 
 export default WebComponent;
 
-type Stringify<T> = {
-  [K in keyof T]?: string;
-};
-
 declare global {
-  namespace JSX {
-    interface IntrinsicElements {
-      [WEB_COMPONENT_NAME]: Stringify<WebComponentProps>;
-    }
+  interface SkipWidgetElement extends HTMLElement, WidgetProps {}
+
+  interface HTMLElementTagNameMap {
+    [WEB_COMPONENT_NAME]: SkipWidgetElement;
path: packages/widget/src/web-component.tsx
user: login: toddkao
body: actually, I can remove extending by HTMLElement and only use `WidgetProps` directly
created_at: 2025-04-14T20:15:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,14 +69,8 @@ initializeSkipWidget();
 
 export default WebComponent;
 
-type Stringify<T> = {
-  [K in keyof T]?: string;
-};
-
 declare global {
-  namespace JSX {
-    interface IntrinsicElements {
-      [WEB_COMPONENT_NAME]: Stringify<WebComponentProps>;
-    }
+  interface HTMLElementTagNameMap {
+    [WEB_COMPONENT_NAME]: WidgetProps;
path: packages/widget/src/web-component.tsx
user: login: toddkao
body: super simple, now the element type is equivalent to `WidgetProps`, so the ide will only show widgetProps in the autocomplete:

![image](https://github.com/user-attachments/assets/29759997-c246-4929-bd36-8573c4826bfb)

created_at: 2025-04-14T20:15:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,20 +33,24 @@ This can be added to npm scripts in `package.json`, a `.env file`, or used when
 
 ## Usage
 
-Props are the same as [`WidgetProps`](./configuration), but passed as attributes in kebab-case. Use strings or stringified objects for complex props.
+Props are the exact same as [`WidgetProps`](./configuration) but you are required to pass them to the element via javascript/typescript.
 
 ```tsx
 <div style="width:100%; max-width:500px; padding:0 10px;">
-  <skip-widget
-    theme='{
-      "brandColor": "#FF4FFF",
-    }'
-    default-route='{
-      "srcChainId": "osmosis-1",
-      "srcAssetDenom": "ibc/1480b8fd20ad5fcae81ea87584d269547dd4d436843c1d20f15e00eb64743ef4"
-    }'
-  ></skip-widget>
+  <skip-widget></skip-widget>
 </div>
+<script>
+  const skipWidget = document.querySelector("skip-widget");
+  if (skipWidget) {
+    skipWidget.theme = {
+      brandColor: "#FF4FFF",
+    };
+    skipWidget.defaultRoute = {
+      srcChainId: "osmosis-1",
+      srcAssetDenom: "ibc/1480b8fd20ad5fcae81ea87584d269547dd4d436843c1d20f15e00eb64743ef4",
+    }
+  }
+</script>
path: docs/widget/web-component.mdx
user: login: toddkao
body: > can add this to the documentation, I'm not sure there is a doc for web component.

@codingki docs are here and this is where it shows up https://docs.skip.build/go/widget/web-component

created_at: 2025-04-15T16:21:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-11T17:09:46Z
body: ###  🦋  Changeset detected

Latest commit: acf842089e8a99abff566ea54d76a823baf6ecd8

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 2 packages</summary>

  | Name            | Type  |
| --------------- | ----- |
| @skip-go/widget | Patch |
| nextjs          | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/skip-mev/skip-go/new/wip-fixing-web-component?filename=.changeset/late-numbers-mix.md&value=---%0A%22%40skip-go%2Fwidget%22%3A%20patch%0A%22nuxt-app%22%3A%20patch%0A---%0A%0A%5BFRE-1668%5D%20Update%20web-component%20to%20allow%20passing%20props%20in%20the%20same%20format%20as%20the%20react%20component%20(camelCase)%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-11T17:09:47Z
body: [vc]: #ew75cOrlPvJ3Yuy988I5whQuF8Evf/08SbMptLqEqkk=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ3aWRnZXQtbmV4dGpzIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3NraXAtcHJvdG9jb2wvd2lkZ2V0LW5leHRqcy9FNmtiRFRlWm83M05ONUNTOUZZcWljZmVDUmtiIiwicHJldmlld1VybCI6IndpZGdldC1uZXh0anMtZ2l0LXdpcC1maXhpbmctd2ViLWNvbXBvbmVudC1za2lwLXByb3RvY29sLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoid2lkZ2V0LW5leHRqcy1naXQtd2lwLWZpeGluZy13ZWItY29tcG9uZW50LXNraXAtcHJvdG9jb2wudmVyY2VsLmFwcCJ9LCJyb290RGlyZWN0b3J5IjoiZXhhbXBsZXMvbmV4dGpzIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **widget-nextjs** | ✅ Ready ([Inspect](https://vercel.com/skip-protocol/widget-nextjs/E6kbDTeZo73NN5CS9FYqicfeCRkb)) | [Visit Preview](https://widget-nextjs-git-wip-fixing-web-component-skip-protocol.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/widget-nextjs-git-wip-fixing-web-component-skip-protocol.vercel.app?via=pr-comment-feedback-link) | Apr 14, 2025 8:26pm |


author_association: NONE
user: login: codingki
created_at: 2025-04-15T16:19:15Z
body: can add this to the documentation, I'm not sure there is a doc for web component. 
author_association: MEMBER