Skip to content

[FRE-1668] Update web-component to allow passing props in the same format as the react component (camelCase) #1055

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 15, 2025
5 changes: 5 additions & 0 deletions .changeset/strange-taxis-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@skip-go/widget": patch
---

Update web-component to allow passing props via javascript properties
24 changes: 14 additions & 10 deletions docs/widget/web-component.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Comment on lines 33 to +53
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

```

## Performance Considerations
Expand Down
24 changes: 11 additions & 13 deletions examples/nuxtjs/app.vue
Original file line number Diff line number Diff line change
@@ -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>
20 changes: 11 additions & 9 deletions examples/raw-html.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
120 changes: 68 additions & 52 deletions packages/widget/src/devMode/loadWidget.tsx
Original file line number Diff line number Diff line change
@@ -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") {
Expand All @@ -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 }}>
Expand All @@ -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={{
Expand All @@ -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>
Expand Down
97 changes: 49 additions & 48 deletions packages/widget/src/web-component.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,74 @@
/* 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)) {
customElements.define(WEB_COMPONENT_NAME, WebComponent);
}

// 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;
Comment on lines +71 to +72
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super simple, now the element type is equivalent to WidgetProps, so the ide will only show widgetProps in the autocomplete:

image

}
}
2 changes: 1 addition & 1 deletion packages/widget/src/widget/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
};
Expand Down