Skip to content

Commit f53402b

Browse files
committed
feat: Add SiteLink component for cross-site wallet-aware linking
1 parent 882f59b commit f53402b

File tree

5 files changed

+241
-105
lines changed

5 files changed

+241
-105
lines changed

.changeset/real-cars-sell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Adds SiteLink

packages/thirdweb/src/exports/react.ts

Lines changed: 53 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
export { darkTheme, lightTheme } from "../react/core/design-system/index.js";
22
export type {
3-
Theme,
4-
ThemeOverrides,
3+
Theme,
4+
ThemeOverrides,
55
} from "../react/core/design-system/index.js";
66

77
export { ConnectButton } from "../react/web/ui/ConnectWallet/ConnectButton.js";
88
export { ConnectEmbed } from "../react/web/ui/ConnectWallet/Modal/ConnectEmbed.js";
99
export type { ConnectEmbedProps } from "../react/core/hooks/connection/ConnectEmbedProps.js";
1010

1111
export type {
12-
ConnectButtonProps,
13-
ConnectButton_connectButtonOptions,
14-
ConnectButton_connectModalOptions,
15-
ConnectButton_detailsButtonOptions,
16-
ConnectButton_detailsModalOptions,
12+
ConnectButtonProps,
13+
ConnectButton_connectButtonOptions,
14+
ConnectButton_connectModalOptions,
15+
ConnectButton_detailsButtonOptions,
16+
ConnectButton_detailsModalOptions,
1717
} from "../react/core/hooks/connection/ConnectButtonProps.js";
1818
export type { NetworkSelectorProps } from "../react/web/ui/ConnectWallet/NetworkSelector.js";
1919
export type { WelcomeScreen } from "../react/web/ui/ConnectWallet/screens/types.js";
@@ -26,12 +26,12 @@ export { ThirdwebProvider } from "../react/web/providers/thirdweb-provider.js";
2626

2727
// tokens
2828
export type {
29-
SupportedTokens,
30-
TokenInfo,
29+
SupportedTokens,
30+
TokenInfo,
3131
} from "../react/core/utils/defaultTokens.js";
3232
export {
33-
defaultTokens,
34-
getDefaultToken,
33+
defaultTokens,
34+
getDefaultToken,
3535
} from "../react/core/utils/defaultTokens.js";
3636

3737
// Media Renderer
@@ -71,8 +71,8 @@ export { useContractEvents } from "../react/core/hooks/contract/useContractEvent
7171

7272
// transaction
7373
export type {
74-
SendTransactionConfig,
75-
SendTransactionPayModalConfig,
74+
SendTransactionConfig,
75+
SendTransactionPayModalConfig,
7676
} from "../react/core/hooks/transaction/useSendTransaction.js";
7777
export { useSimulateTransaction } from "../react/core/hooks/transaction/useSimulateTransaction.js";
7878
export { useSendTransaction } from "../react/web/hooks/transaction/useSendTransaction.js";
@@ -83,8 +83,8 @@ export { useEstimateGasCost } from "../react/core/hooks/transaction/useEstimateG
8383

8484
// rpc related
8585
export {
86-
useBlockNumber,
87-
type UseBlockNumberOptions,
86+
useBlockNumber,
87+
type UseBlockNumberOptions,
8888
} from "../react/core/hooks/rpc/useBlockNumber.js";
8989

9090
// utils
@@ -93,30 +93,30 @@ export { useInvalidateContractQuery } from "../react/core/hooks/others/useInvali
9393

9494
// pay
9595
export {
96-
useBuyWithCryptoQuote,
97-
type BuyWithCryptoQuoteQueryOptions,
96+
useBuyWithCryptoQuote,
97+
type BuyWithCryptoQuoteQueryOptions,
9898
} from "../react/core/hooks/pay/useBuyWithCryptoQuote.js";
9999
export { useBuyWithCryptoStatus } from "../react/core/hooks/pay/useBuyWithCryptoStatus.js";
100100
export {
101-
useBuyWithCryptoHistory,
102-
type BuyWithCryptoHistoryQueryOptions,
101+
useBuyWithCryptoHistory,
102+
type BuyWithCryptoHistoryQueryOptions,
103103
} from "../react/core/hooks/pay/useBuyWithCryptoHistory.js";
104104
export {
105-
useBuyWithFiatQuote,
106-
type BuyWithFiatQuoteQueryOptions,
105+
useBuyWithFiatQuote,
106+
type BuyWithFiatQuoteQueryOptions,
107107
} from "../react/core/hooks/pay/useBuyWithFiatQuote.js";
108108
export { useBuyWithFiatStatus } from "../react/core/hooks/pay/useBuyWithFiatStatus.js";
109109
export {
110-
useBuyWithFiatHistory,
111-
type BuyWithFiatHistoryQueryOptions,
110+
useBuyWithFiatHistory,
111+
type BuyWithFiatHistoryQueryOptions,
112112
} from "../react/core/hooks/pay/useBuyWithFiatHistory.js";
113113
export {
114-
useBuyHistory,
115-
type BuyHistoryQueryOptions,
114+
useBuyHistory,
115+
type BuyHistoryQueryOptions,
116116
} from "../react/core/hooks/pay/useBuyHistory.js";
117117
export {
118-
usePostOnRampQuote,
119-
type PostOnRampQuoteQueryOptions,
118+
usePostOnRampQuote,
119+
type PostOnRampQuoteQueryOptions,
120120
} from "../react/core/hooks/pay/usePostOnrampQuote.js";
121121

122122
export { AutoConnect } from "../react/web/ui/AutoConnect/AutoConnect.js";
@@ -126,34 +126,34 @@ export type { AutoConnectProps } from "../react/core/hooks/connection/types.js";
126126
export type { SiweAuthOptions } from "../react/core/hooks/auth/useSiweAuth.js";
127127

128128
export {
129-
PayEmbed,
130-
type PayEmbedProps,
131-
type PayEmbedConnectOptions,
129+
PayEmbed,
130+
type PayEmbedProps,
131+
type PayEmbedConnectOptions,
132132
} from "../react/web/ui/PayEmbed.js";
133133
export type {
134-
PayUIOptions,
135-
PaymentInfo,
136-
DirectPaymentOptions,
137-
FundWalletOptions,
138-
TranasctionOptions,
134+
PayUIOptions,
135+
PaymentInfo,
136+
DirectPaymentOptions,
137+
FundWalletOptions,
138+
TranasctionOptions,
139139
} from "../react/core/hooks/connection/ConnectButtonProps.js";
140140

141141
export {
142-
useConnectModal,
143-
type UseConnectModalOptions,
142+
useConnectModal,
143+
type UseConnectModalOptions,
144144
} from "../react/web/ui/ConnectWallet/useConnectModal.js";
145145

146146
// wallet info
147147
export { useWalletInfo, useWalletImage } from "../react/core/utils/wallet.js";
148148

149149
export {
150-
useWalletDetailsModal,
151-
type UseWalletDetailsModalOptions,
150+
useWalletDetailsModal,
151+
type UseWalletDetailsModalOptions,
152152
} from "../react/web/ui/ConnectWallet/Details.js";
153153

154154
export {
155-
useNetworkSwitcherModal,
156-
type UseNetworkSwitcherModalOptions,
155+
useNetworkSwitcherModal,
156+
type UseNetworkSwitcherModalOptions,
157157
} from "../react/web/ui/ConnectWallet/NetworkSelector.js";
158158

159159
// ens
@@ -165,17 +165,17 @@ export { useEnsName, useEnsAvatar } from "../react/core/utils/wallet.js";
165165
export { ClaimButton } from "../react/web/ui/prebuilt/thirdweb/ClaimButton/index.js";
166166
export type { ClaimButtonProps } from "../react/web/ui/prebuilt/thirdweb/ClaimButton/types.js";
167167
export {
168-
BuyDirectListingButton,
169-
type BuyDirectListingButtonProps,
168+
BuyDirectListingButton,
169+
type BuyDirectListingButtonProps,
170170
} from "../react/web/ui/prebuilt/thirdweb/BuyDirectListingButton/index.js";
171171
export {
172-
CreateDirectListingButton,
173-
type CreateDirectListingButtonProps,
172+
CreateDirectListingButton,
173+
type CreateDirectListingButtonProps,
174174
} from "../react/web/ui/prebuilt/thirdweb/CreateDirectListingButton/index.js";
175175

176176
export {
177-
NFT,
178-
type NFTMediaProps,
177+
NFT,
178+
type NFTMediaProps,
179179
} from "../react/web/ui/prebuilt/NFT/NFT.js";
180180

181181
export { useConnectionManager } from "../react/core/providers/connection-manager.js";
@@ -190,11 +190,12 @@ export { useSiweAuth } from "../react/core/hooks/auth/useSiweAuth.js";
190190
// Social
191191
export { useSocialProfiles } from "../react/core/social/useSocialProfiles.js";
192192
export type {
193-
SocialProfile,
194-
EnsProfile,
195-
FarcasterProfile,
196-
LensProfile,
193+
SocialProfile,
194+
EnsProfile,
195+
FarcasterProfile,
196+
LensProfile,
197197
} from "../social/types.js";
198198

199-
// Site Embed
199+
// Site Embed and Linking
200200
export { SiteEmbed } from "../react/web/ui/SiteEmbed.js";
201+
export { SiteLink } from "../react/web/ui/SiteLink.js";

packages/thirdweb/src/react/web/ui/SiteEmbed.tsx

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,74 +17,74 @@ import { useActiveWallet } from "../../core/hooks/wallets/useActiveWallet.js";
1717
*
1818
* @param {Object} props - The props to pass to the iframe
1919
* @param {String} props.src - The URL of the site to embed
20-
* @param {ThirdwebClient} props.client - The client to use for the embedded site
21-
* @param {Ecosystem} [props.ecosystem] - The ecosystem to use for the embedded site
20+
* @param {ThirdwebClient} props.client - The current site's thirdweb client
21+
* @param {Ecosystem} [props.ecosystem] - The ecosystem to use for the wallet connection in the embedded site
2222
*
2323
* @example
2424
* ```tsx
2525
* import { SiteEmbed } from "thirdweb/react";
2626
*
27-
* <SiteEmbed src="https://thirdweb.com" clientId="thirdweb-client-id" />
27+
* <SiteEmbed src="https://thirdweb.com" client={thirdwebClient} ecosystem={{ id: "ecosystem.thirdweb" }} />
2828
* ```
2929
*/
3030
export function SiteEmbed({
31-
src,
32-
client,
33-
ecosystem,
34-
...props
31+
src,
32+
client,
33+
ecosystem,
34+
...props
3535
}: {
36-
src: string;
37-
client: ThirdwebClient;
38-
ecosystem?: Ecosystem;
36+
src: string;
37+
client: ThirdwebClient;
38+
ecosystem?: Ecosystem;
3939
} & React.DetailedHTMLProps<
40-
React.IframeHTMLAttributes<HTMLIFrameElement>,
41-
HTMLIFrameElement
40+
React.IframeHTMLAttributes<HTMLIFrameElement>,
41+
HTMLIFrameElement
4242
>) {
43-
if (!client.clientId) {
44-
throw new Error("The SiteEmbed client must have a clientId");
45-
}
43+
if (!client.clientId) {
44+
throw new Error("The SiteEmbed client must have a clientId");
45+
}
4646

47-
const activeWallet = useActiveWallet();
48-
const walletId = activeWallet?.id;
47+
const activeWallet = useActiveWallet();
48+
const walletId = activeWallet?.id;
4949

50-
const {
51-
data: { authProvider, authCookie } = {},
52-
} = useQuery({
53-
queryKey: ["site-embed", walletId, src, client.clientId, ecosystem],
54-
enabled:
55-
activeWallet && (isEcosystemWallet(activeWallet) || walletId === "inApp"),
56-
queryFn: async () => {
57-
const storage = new ClientScopedStorage({
58-
storage: webLocalStorage,
59-
clientId: client.clientId,
60-
ecosystem,
61-
});
50+
const {
51+
data: { authProvider, authCookie } = {},
52+
} = useQuery({
53+
queryKey: ["site-embed", walletId, src, client.clientId, ecosystem],
54+
enabled:
55+
activeWallet && (isEcosystemWallet(activeWallet) || walletId === "inApp"),
56+
queryFn: async () => {
57+
const storage = new ClientScopedStorage({
58+
storage: webLocalStorage,
59+
clientId: client.clientId,
60+
ecosystem,
61+
});
6262

63-
const authProvider = await getLastAuthProvider(webLocalStorage);
64-
const authCookie = await storage.getAuthCookie();
63+
const authProvider = await getLastAuthProvider(webLocalStorage);
64+
const authCookie = await storage.getAuthCookie();
6565

66-
return { authProvider, authCookie };
67-
},
68-
});
66+
return { authProvider, authCookie };
67+
},
68+
});
6969

70-
const url = new URL(src);
71-
if (walletId) {
72-
url.searchParams.set("walletId", walletId);
73-
}
74-
if (authProvider) {
75-
url.searchParams.set("authProvider", authProvider);
76-
}
77-
if (authCookie) {
78-
url.searchParams.set("authCookie", authCookie);
79-
}
70+
const url = new URL(src);
71+
if (walletId) {
72+
url.searchParams.set("walletId", walletId);
73+
}
74+
if (authProvider) {
75+
url.searchParams.set("authProvider", authProvider);
76+
}
77+
if (authCookie) {
78+
url.searchParams.set("authCookie", authCookie);
79+
}
8080

81-
return (
82-
<iframe
83-
src={encodeURI(url.toString())}
84-
width="100%"
85-
height="100%"
86-
allowFullScreen
87-
{...props}
88-
/>
89-
);
81+
return (
82+
<iframe
83+
src={encodeURI(url.toString())}
84+
width="100%"
85+
height="100%"
86+
allowFullScreen
87+
{...props}
88+
/>
89+
);
9090
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it } from "vitest";
2+
import { render, waitFor } from "../../../../test/src/react-render.js";
3+
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
4+
import { SiteLink } from "./SiteLink.js";
5+
6+
describe("SiteLink", () => {
7+
it("renders anchor with correct href", () => {
8+
const testUrl = "https://example.com/";
9+
const { container } = render(
10+
<SiteLink href={testUrl} client={TEST_CLIENT}>
11+
Test Link
12+
</SiteLink>,
13+
);
14+
15+
const anchor = container.querySelector("a");
16+
expect(anchor).toBeTruthy();
17+
expect(anchor?.href).toBe(testUrl);
18+
expect(anchor?.textContent).toBe("Test Link");
19+
});
20+
21+
it("throws error if clientId is not provided", () => {
22+
const testUrl = "https://example.com/";
23+
expect(() =>
24+
// biome-ignore lint/suspicious/noExplicitAny: testing invalid input
25+
render(<SiteLink href={testUrl} client={{} as any}>Test Link</SiteLink>),
26+
).toThrow("The SiteLink client must have a clientId");
27+
});
28+
29+
it("adds wallet params to url when wallet is connected", async () => {
30+
const testUrl = "https://example.com/";
31+
const { container } = render(
32+
<SiteLink href={testUrl} client={TEST_CLIENT}>
33+
Test Link
34+
</SiteLink>,
35+
{
36+
setConnectedWallet: true,
37+
},
38+
);
39+
40+
const anchor = container.querySelector("a");
41+
expect(anchor).toBeTruthy();
42+
await waitFor(() => expect(anchor?.href).toContain("walletId="));
43+
});
44+
});

0 commit comments

Comments
 (0)