thirdweb 5.112.4 → 5.114.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/exports/react.js +4 -1
- package/dist/cjs/exports/react.js.map +1 -1
- package/dist/cjs/exports/react.native.js +4 -1
- package/dist/cjs/exports/react.native.js.map +1 -1
- package/dist/cjs/react/core/hooks/x402/useFetchWithPaymentCore.js +110 -0
- package/dist/cjs/react/core/hooks/x402/useFetchWithPaymentCore.js.map +1 -0
- package/dist/cjs/react/native/hooks/x402/useFetchWithPayment.js +89 -0
- package/dist/cjs/react/native/hooks/x402/useFetchWithPayment.js.map +1 -0
- package/dist/cjs/react/web/hooks/x402/useFetchWithPayment.js +168 -0
- package/dist/cjs/react/web/hooks/x402/useFetchWithPayment.js.map +1 -0
- package/dist/cjs/react/web/ui/components/basic.js +1 -1
- package/dist/cjs/react/web/ui/x402/PaymentErrorModal.js +107 -0
- package/dist/cjs/react/web/ui/x402/PaymentErrorModal.js.map +1 -0
- package/dist/cjs/react/web/ui/x402/SignInRequiredModal.js +28 -0
- package/dist/cjs/react/web/ui/x402/SignInRequiredModal.js.map +1 -0
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/wallets/coinbase/coinbase-web.js +9 -0
- package/dist/cjs/wallets/coinbase/coinbase-web.js.map +1 -1
- package/dist/cjs/x402/common.js +2 -10
- package/dist/cjs/x402/common.js.map +1 -1
- package/dist/cjs/x402/facilitator.js +2 -1
- package/dist/cjs/x402/facilitator.js.map +1 -1
- package/dist/cjs/x402/fetchWithPayment.js +13 -14
- package/dist/cjs/x402/fetchWithPayment.js.map +1 -1
- package/dist/cjs/x402/schemas.js +4 -1
- package/dist/cjs/x402/schemas.js.map +1 -1
- package/dist/cjs/x402/settle-payment.js +13 -2
- package/dist/cjs/x402/settle-payment.js.map +1 -1
- package/dist/cjs/x402/types.js +6 -1
- package/dist/cjs/x402/types.js.map +1 -1
- package/dist/cjs/x402/verify-payment.js +12 -2
- package/dist/cjs/x402/verify-payment.js.map +1 -1
- package/dist/esm/exports/react.js +2 -0
- package/dist/esm/exports/react.js.map +1 -1
- package/dist/esm/exports/react.native.js +2 -0
- package/dist/esm/exports/react.native.js.map +1 -1
- package/dist/esm/react/core/hooks/x402/useFetchWithPaymentCore.js +107 -0
- package/dist/esm/react/core/hooks/x402/useFetchWithPaymentCore.js.map +1 -0
- package/dist/esm/react/native/hooks/x402/useFetchWithPayment.js +86 -0
- package/dist/esm/react/native/hooks/x402/useFetchWithPayment.js.map +1 -0
- package/dist/esm/react/web/hooks/x402/useFetchWithPayment.js +165 -0
- package/dist/esm/react/web/hooks/x402/useFetchWithPayment.js.map +1 -0
- package/dist/esm/react/web/ui/components/basic.js +1 -1
- package/dist/esm/react/web/ui/x402/PaymentErrorModal.js +104 -0
- package/dist/esm/react/web/ui/x402/PaymentErrorModal.js.map +1 -0
- package/dist/esm/react/web/ui/x402/SignInRequiredModal.js +25 -0
- package/dist/esm/react/web/ui/x402/SignInRequiredModal.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/esm/wallets/coinbase/coinbase-web.js +9 -0
- package/dist/esm/wallets/coinbase/coinbase-web.js.map +1 -1
- package/dist/esm/x402/common.js +2 -10
- package/dist/esm/x402/common.js.map +1 -1
- package/dist/esm/x402/facilitator.js +2 -1
- package/dist/esm/x402/facilitator.js.map +1 -1
- package/dist/esm/x402/fetchWithPayment.js +13 -14
- package/dist/esm/x402/fetchWithPayment.js.map +1 -1
- package/dist/esm/x402/schemas.js +4 -1
- package/dist/esm/x402/schemas.js.map +1 -1
- package/dist/esm/x402/settle-payment.js +13 -2
- package/dist/esm/x402/settle-payment.js.map +1 -1
- package/dist/esm/x402/types.js +5 -0
- package/dist/esm/x402/types.js.map +1 -1
- package/dist/esm/x402/verify-payment.js +12 -2
- package/dist/esm/x402/verify-payment.js.map +1 -1
- package/dist/scripts/bridge-widget.js +50 -50
- package/dist/types/exports/react.d.ts +1 -0
- package/dist/types/exports/react.d.ts.map +1 -1
- package/dist/types/exports/react.native.d.ts +1 -0
- package/dist/types/exports/react.native.d.ts.map +1 -1
- package/dist/types/react/core/hooks/x402/useFetchWithPaymentCore.d.ts +131 -0
- package/dist/types/react/core/hooks/x402/useFetchWithPaymentCore.d.ts.map +1 -0
- package/dist/types/react/native/hooks/x402/useFetchWithPayment.d.ts +189 -0
- package/dist/types/react/native/hooks/x402/useFetchWithPayment.d.ts.map +1 -0
- package/dist/types/react/web/hooks/x402/useFetchWithPayment.d.ts +248 -0
- package/dist/types/react/web/hooks/x402/useFetchWithPayment.d.ts.map +1 -0
- package/dist/types/react/web/ui/x402/PaymentErrorModal.d.ts +20 -0
- package/dist/types/react/web/ui/x402/PaymentErrorModal.d.ts.map +1 -0
- package/dist/types/react/web/ui/x402/SignInRequiredModal.d.ts +12 -0
- package/dist/types/react/web/ui/x402/SignInRequiredModal.d.ts.map +1 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types/x402/common.d.ts.map +1 -1
- package/dist/types/x402/facilitator.d.ts +1 -1
- package/dist/types/x402/facilitator.d.ts.map +1 -1
- package/dist/types/x402/fetchWithPayment.d.ts +7 -3
- package/dist/types/x402/fetchWithPayment.d.ts.map +1 -1
- package/dist/types/x402/schemas.d.ts +17 -17
- package/dist/types/x402/schemas.d.ts.map +1 -1
- package/dist/types/x402/settle-payment.d.ts +13 -2
- package/dist/types/x402/settle-payment.d.ts.map +1 -1
- package/dist/types/x402/types.d.ts +13 -1
- package/dist/types/x402/types.d.ts.map +1 -1
- package/dist/types/x402/verify-payment.d.ts +12 -2
- package/dist/types/x402/verify-payment.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/exports/react.native.ts +5 -0
- package/src/exports/react.ts +5 -0
- package/src/react/core/hooks/x402/useFetchWithPaymentCore.ts +160 -0
- package/src/react/native/hooks/x402/useFetchWithPayment.ts +96 -0
- package/src/react/web/hooks/x402/useFetchWithPayment.tsx +238 -0
- package/src/react/web/ui/components/basic.tsx +1 -1
- package/src/react/web/ui/x402/PaymentErrorModal.tsx +261 -0
- package/src/react/web/ui/x402/SignInRequiredModal.tsx +75 -0
- package/src/version.ts +1 -1
- package/src/wallets/coinbase/coinbase-web.ts +10 -0
- package/src/x402/common.ts +2 -20
- package/src/x402/facilitator.ts +2 -1
- package/src/x402/fetchWithPayment.ts +23 -22
- package/src/x402/schemas.ts +4 -1
- package/src/x402/settle-payment.ts +13 -2
- package/src/x402/types.ts +16 -1
- package/src/x402/verify-payment.ts +12 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ThirdwebClient } from "../../../../client/client.js";
|
|
4
|
+
import {
|
|
5
|
+
type UseFetchWithPaymentOptions,
|
|
6
|
+
useFetchWithPaymentCore,
|
|
7
|
+
} from "../../../core/hooks/x402/useFetchWithPaymentCore.js";
|
|
8
|
+
|
|
9
|
+
export type { UseFetchWithPaymentOptions };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A React hook that wraps the native fetch API to automatically handle 402 Payment Required responses
|
|
13
|
+
* using the x402 payment protocol with the currently connected wallet.
|
|
14
|
+
*
|
|
15
|
+
* This hook enables you to make API calls that require payment without manually handling the payment flow.
|
|
16
|
+
* Responses are automatically parsed as JSON by default (can be customized with `parseAs` option).
|
|
17
|
+
*
|
|
18
|
+
* When a 402 response is received, it will automatically:
|
|
19
|
+
* 1. Parse the payment requirements
|
|
20
|
+
* 2. Verify the payment amount is within the allowed maximum
|
|
21
|
+
* 3. Create a payment header using the connected wallet
|
|
22
|
+
* 4. Retry the request with the payment header
|
|
23
|
+
*
|
|
24
|
+
* Note: This is the React Native version which does not include modal UI.
|
|
25
|
+
* Payment errors will be thrown and should be handled by your application.
|
|
26
|
+
*
|
|
27
|
+
* @param client - The thirdweb client used to access RPC infrastructure
|
|
28
|
+
* @param options - Optional configuration for payment handling
|
|
29
|
+
* @param options.maxValue - The maximum allowed payment amount in base units
|
|
30
|
+
* @param options.paymentRequirementsSelector - Custom function to select payment requirements from available options
|
|
31
|
+
* @param options.parseAs - How to parse the response: "json" (default), "text", or "raw"
|
|
32
|
+
* @returns An object containing:
|
|
33
|
+
* - `fetchWithPayment`: Function to make fetch requests with automatic payment handling (returns parsed data)
|
|
34
|
+
* - `isPending`: Boolean indicating if a request is in progress
|
|
35
|
+
* - `error`: Any error that occurred during the request
|
|
36
|
+
* - `data`: The parsed response data (JSON by default, or based on `parseAs` option)
|
|
37
|
+
* - Other mutation properties from React Query
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* import { useFetchWithPayment } from "thirdweb/react";
|
|
42
|
+
* import { createThirdwebClient } from "thirdweb";
|
|
43
|
+
*
|
|
44
|
+
* const client = createThirdwebClient({ clientId: "your-client-id" });
|
|
45
|
+
*
|
|
46
|
+
* function MyComponent() {
|
|
47
|
+
* const { fetchWithPayment, isPending, error } = useFetchWithPayment(client);
|
|
48
|
+
*
|
|
49
|
+
* const handleApiCall = async () => {
|
|
50
|
+
* try {
|
|
51
|
+
* // Response is automatically parsed as JSON
|
|
52
|
+
* const data = await fetchWithPayment('https://api.example.com/paid-endpoint');
|
|
53
|
+
* console.log(data);
|
|
54
|
+
* } catch (err) {
|
|
55
|
+
* // Handle payment errors manually in React Native
|
|
56
|
+
* console.error("Payment failed:", err);
|
|
57
|
+
* }
|
|
58
|
+
* };
|
|
59
|
+
*
|
|
60
|
+
* return (
|
|
61
|
+
* <button onClick={handleApiCall} disabled={isPending}>
|
|
62
|
+
* {isPending ? 'Loading...' : 'Make Paid API Call'}
|
|
63
|
+
* </button>
|
|
64
|
+
* );
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* ### Customize response parsing
|
|
69
|
+
* ```tsx
|
|
70
|
+
* const { fetchWithPayment } = useFetchWithPayment(client, {
|
|
71
|
+
* parseAs: "text", // Get response as text instead of JSON
|
|
72
|
+
* });
|
|
73
|
+
*
|
|
74
|
+
* const textData = await fetchWithPayment('https://api.example.com/endpoint');
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* ### Customize payment options
|
|
78
|
+
* ```tsx
|
|
79
|
+
* const { fetchWithPayment } = useFetchWithPayment(client, {
|
|
80
|
+
* maxValue: 5000000n, // 5 USDC in base units
|
|
81
|
+
* paymentRequirementsSelector: (requirements) => {
|
|
82
|
+
* // Custom logic to select preferred payment method
|
|
83
|
+
* return requirements[0];
|
|
84
|
+
* }
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @x402
|
|
89
|
+
*/
|
|
90
|
+
export function useFetchWithPayment(
|
|
91
|
+
client: ThirdwebClient,
|
|
92
|
+
options?: UseFetchWithPaymentOptions,
|
|
93
|
+
) {
|
|
94
|
+
// Native version doesn't show modal, errors bubble up naturally
|
|
95
|
+
return useFetchWithPaymentCore(client, options);
|
|
96
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useContext } from "react";
|
|
4
|
+
import type { ThirdwebClient } from "../../../../client/client.js";
|
|
5
|
+
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
|
|
6
|
+
import type { Theme } from "../../../core/design-system/index.js";
|
|
7
|
+
import {
|
|
8
|
+
type UseFetchWithPaymentOptions,
|
|
9
|
+
useFetchWithPaymentCore,
|
|
10
|
+
} from "../../../core/hooks/x402/useFetchWithPaymentCore.js";
|
|
11
|
+
import { SetRootElementContext } from "../../../core/providers/RootElementContext.js";
|
|
12
|
+
import type { BuyWidgetProps } from "../../ui/Bridge/BuyWidget.js";
|
|
13
|
+
import {
|
|
14
|
+
type UseConnectModalOptions,
|
|
15
|
+
useConnectModal,
|
|
16
|
+
} from "../../ui/ConnectWallet/useConnectModal.js";
|
|
17
|
+
import { PaymentErrorModal } from "../../ui/x402/PaymentErrorModal.js";
|
|
18
|
+
import { SignInRequiredModal } from "../../ui/x402/SignInRequiredModal.js";
|
|
19
|
+
|
|
20
|
+
export type { UseFetchWithPaymentOptions };
|
|
21
|
+
|
|
22
|
+
type UseFetchWithPaymentConfig = UseFetchWithPaymentOptions & {
|
|
23
|
+
/**
|
|
24
|
+
* Whether to show the UI for connection, funding or payment retries.
|
|
25
|
+
* If false, no UI will be shown and errors will have to be handled manually.
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
uiEnabled?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Theme for the payment error modal
|
|
31
|
+
* @default "dark"
|
|
32
|
+
*/
|
|
33
|
+
theme?: Theme | "light" | "dark";
|
|
34
|
+
/**
|
|
35
|
+
* Options to customize the BuyWidget that appears when the user needs to fund their wallet.
|
|
36
|
+
* These options will be merged with default values.
|
|
37
|
+
*/
|
|
38
|
+
fundWalletOptions?: Partial<
|
|
39
|
+
Omit<
|
|
40
|
+
BuyWidgetProps,
|
|
41
|
+
"client" | "chain" | "tokenAddress" | "onSuccess" | "onCancel" | "theme"
|
|
42
|
+
>
|
|
43
|
+
>;
|
|
44
|
+
/**
|
|
45
|
+
* Options to customize the ConnectModal that appears when the user needs to sign in.
|
|
46
|
+
* These options will be merged with the client, theme, and chain from the hook.
|
|
47
|
+
*/
|
|
48
|
+
connectOptions?: Omit<UseConnectModalOptions, "client" | "theme">;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A React hook that wraps the native fetch API to automatically handle 402 Payment Required responses
|
|
53
|
+
* using the x402 payment protocol with the currently connected wallet.
|
|
54
|
+
*
|
|
55
|
+
* This hook enables you to make API calls that require payment without manually handling the payment flow.
|
|
56
|
+
* Responses are automatically parsed as JSON by default (can be customized with `parseAs` option).
|
|
57
|
+
*
|
|
58
|
+
* When a 402 response is received, it will automatically:
|
|
59
|
+
* 1. Parse the payment requirements
|
|
60
|
+
* 2. Verify the payment amount is within the allowed maximum
|
|
61
|
+
* 3. Create a payment header using the connected wallet
|
|
62
|
+
* 4. Retry the request with the payment header
|
|
63
|
+
*
|
|
64
|
+
* If payment fails (e.g. insufficient funds), a modal will be shown to help the user resolve the issue.
|
|
65
|
+
* If no wallet is connected, a sign-in modal will be shown to connect a wallet.
|
|
66
|
+
*
|
|
67
|
+
* @param client - The thirdweb client used to access RPC infrastructure
|
|
68
|
+
* @param options - Optional configuration for payment handling
|
|
69
|
+
* @param options.maxValue - The maximum allowed payment amount in base units
|
|
70
|
+
* @param options.paymentRequirementsSelector - Custom function to select payment requirements from available options
|
|
71
|
+
* @param options.parseAs - How to parse the response: "json" (default), "text", or "raw"
|
|
72
|
+
* @param options.uiEnabled - Whether to show the UI for connection, funding or payment retries (defaults to true). Set to false to handle errors yourself
|
|
73
|
+
* @param options.theme - Theme for the payment error modal (defaults to "dark")
|
|
74
|
+
* @param options.fundWalletOptions - Customize the BuyWidget shown when user needs to fund their wallet
|
|
75
|
+
* @param options.connectOptions - Customize the ConnectModal shown when user needs to sign in
|
|
76
|
+
* @returns An object containing:
|
|
77
|
+
* - `fetchWithPayment`: Function to make fetch requests with automatic payment handling (returns parsed data)
|
|
78
|
+
* - `isPending`: Boolean indicating if a request is in progress
|
|
79
|
+
* - `error`: Any error that occurred during the request
|
|
80
|
+
* - `data`: The parsed response data (JSON by default, or based on `parseAs` option)
|
|
81
|
+
* - Other mutation properties from React Query
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```tsx
|
|
85
|
+
* import { useFetchWithPayment } from "thirdweb/react";
|
|
86
|
+
* import { createThirdwebClient } from "thirdweb";
|
|
87
|
+
*
|
|
88
|
+
* const client = createThirdwebClient({ clientId: "your-client-id" });
|
|
89
|
+
*
|
|
90
|
+
* function MyComponent() {
|
|
91
|
+
* const { fetchWithPayment, isPending } = useFetchWithPayment(client);
|
|
92
|
+
*
|
|
93
|
+
* const handleApiCall = async () => {
|
|
94
|
+
* // Response is automatically parsed as JSON
|
|
95
|
+
* const data = await fetchWithPayment('https://api.example.com/paid-endpoint');
|
|
96
|
+
* console.log(data);
|
|
97
|
+
* };
|
|
98
|
+
*
|
|
99
|
+
* return (
|
|
100
|
+
* <button onClick={handleApiCall} disabled={isPending}>
|
|
101
|
+
* {isPending ? 'Loading...' : 'Make Paid API Call'}
|
|
102
|
+
* </button>
|
|
103
|
+
* );
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* ### Customize response parsing
|
|
108
|
+
* ```tsx
|
|
109
|
+
* const { fetchWithPayment } = useFetchWithPayment(client, {
|
|
110
|
+
* parseAs: "text", // Get response as text instead of JSON
|
|
111
|
+
* });
|
|
112
|
+
*
|
|
113
|
+
* const textData = await fetchWithPayment('https://api.example.com/endpoint');
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* ### Customize payment options
|
|
117
|
+
* ```tsx
|
|
118
|
+
* const { fetchWithPayment } = useFetchWithPayment(client, {
|
|
119
|
+
* maxValue: 5000000n, // 5 USDC in base units
|
|
120
|
+
* theme: "light",
|
|
121
|
+
* paymentRequirementsSelector: (requirements) => {
|
|
122
|
+
* // Custom logic to select preferred payment method
|
|
123
|
+
* return requirements[0];
|
|
124
|
+
* }
|
|
125
|
+
* });
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* ### Customize the fund wallet widget
|
|
129
|
+
* ```tsx
|
|
130
|
+
* const { fetchWithPayment } = useFetchWithPayment(client, {
|
|
131
|
+
* fundWalletOptions: {
|
|
132
|
+
* title: "Add Funds",
|
|
133
|
+
* description: "You need more tokens to complete this payment",
|
|
134
|
+
* buttonLabel: "Get Tokens",
|
|
135
|
+
* }
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* ### Customize the connect modal
|
|
140
|
+
* ```tsx
|
|
141
|
+
* const { fetchWithPayment } = useFetchWithPayment(client, {
|
|
142
|
+
* connectOptions: {
|
|
143
|
+
* wallets: [inAppWallet(), createWallet("io.metamask")],
|
|
144
|
+
* title: "Sign in to continue",
|
|
145
|
+
* }
|
|
146
|
+
* });
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* ### Disable the UI and handle errors yourself
|
|
150
|
+
* ```tsx
|
|
151
|
+
* const { fetchWithPayment, error } = useFetchWithPayment(client, {
|
|
152
|
+
* uiEnabled: false,
|
|
153
|
+
* });
|
|
154
|
+
*
|
|
155
|
+
* // Handle the error manually
|
|
156
|
+
* if (error) {
|
|
157
|
+
* console.error("Payment failed:", error);
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* @x402
|
|
162
|
+
*/
|
|
163
|
+
export function useFetchWithPayment(
|
|
164
|
+
client: ThirdwebClient,
|
|
165
|
+
options?: UseFetchWithPaymentConfig,
|
|
166
|
+
) {
|
|
167
|
+
const setRootEl = useContext(SetRootElementContext);
|
|
168
|
+
const { connect } = useConnectModal();
|
|
169
|
+
const theme = options?.theme || "dark";
|
|
170
|
+
const showModal = options?.uiEnabled !== false; // Default to true
|
|
171
|
+
|
|
172
|
+
const showErrorModal = showModal
|
|
173
|
+
? (data: {
|
|
174
|
+
errorData: Parameters<typeof PaymentErrorModal>[0]["errorData"];
|
|
175
|
+
onRetry: () => void;
|
|
176
|
+
onCancel: () => void;
|
|
177
|
+
}) => {
|
|
178
|
+
setRootEl(
|
|
179
|
+
<PaymentErrorModal
|
|
180
|
+
client={client}
|
|
181
|
+
errorData={data.errorData}
|
|
182
|
+
onCancel={() => {
|
|
183
|
+
setRootEl(null);
|
|
184
|
+
data.onCancel();
|
|
185
|
+
}}
|
|
186
|
+
onRetry={() => {
|
|
187
|
+
setRootEl(null);
|
|
188
|
+
data.onRetry();
|
|
189
|
+
}}
|
|
190
|
+
theme={theme}
|
|
191
|
+
fundWalletOptions={options?.fundWalletOptions}
|
|
192
|
+
paymentRequirementsSelector={options?.paymentRequirementsSelector}
|
|
193
|
+
/>,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
: undefined;
|
|
197
|
+
|
|
198
|
+
const showConnectModal = showModal
|
|
199
|
+
? (data: { onConnect: (wallet: Wallet) => void; onCancel: () => void }) => {
|
|
200
|
+
// First, show the SignInRequiredModal
|
|
201
|
+
setRootEl(
|
|
202
|
+
<SignInRequiredModal
|
|
203
|
+
theme={theme}
|
|
204
|
+
onSignIn={async () => {
|
|
205
|
+
// Close the SignInRequiredModal
|
|
206
|
+
setRootEl(null);
|
|
207
|
+
|
|
208
|
+
// Open the ConnectModal
|
|
209
|
+
try {
|
|
210
|
+
const connectedWallet = await connect({
|
|
211
|
+
client,
|
|
212
|
+
theme,
|
|
213
|
+
...options?.connectOptions,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// On successful connection, trigger onConnect callback with the wallet
|
|
217
|
+
data.onConnect(connectedWallet);
|
|
218
|
+
} catch (_error) {
|
|
219
|
+
// User cancelled the connection
|
|
220
|
+
data.onCancel();
|
|
221
|
+
}
|
|
222
|
+
}}
|
|
223
|
+
onCancel={() => {
|
|
224
|
+
setRootEl(null);
|
|
225
|
+
data.onCancel();
|
|
226
|
+
}}
|
|
227
|
+
/>,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
: undefined;
|
|
231
|
+
|
|
232
|
+
return useFetchWithPaymentCore(
|
|
233
|
+
client,
|
|
234
|
+
options,
|
|
235
|
+
showErrorModal,
|
|
236
|
+
showConnectModal,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { getCachedChain } from "../../../../chains/utils.js";
|
|
4
|
+
import type { ThirdwebClient } from "../../../../client/client.js";
|
|
5
|
+
import {
|
|
6
|
+
extractEvmChainId,
|
|
7
|
+
networkToCaip2ChainId,
|
|
8
|
+
type RequestedPaymentRequirements,
|
|
9
|
+
} from "../../../../x402/schemas.js";
|
|
10
|
+
import type { PaymentRequiredResult } from "../../../../x402/types.js";
|
|
11
|
+
import { CustomThemeProvider } from "../../../core/design-system/CustomThemeProvider.js";
|
|
12
|
+
import type { Theme } from "../../../core/design-system/index.js";
|
|
13
|
+
import { spacing } from "../../../core/design-system/index.js";
|
|
14
|
+
import { useActiveWallet } from "../../../core/hooks/wallets/useActiveWallet.js";
|
|
15
|
+
import { BuyWidget, type BuyWidgetProps } from "../Bridge/BuyWidget.js";
|
|
16
|
+
import {
|
|
17
|
+
Container,
|
|
18
|
+
ModalHeader,
|
|
19
|
+
ScreenBottomContainer,
|
|
20
|
+
} from "../components/basic.js";
|
|
21
|
+
import { Button } from "../components/buttons.js";
|
|
22
|
+
import { Modal } from "../components/Modal.js";
|
|
23
|
+
import { Text } from "../components/text.js";
|
|
24
|
+
|
|
25
|
+
type PaymentErrorModalProps = {
|
|
26
|
+
client: ThirdwebClient;
|
|
27
|
+
errorData: PaymentRequiredResult["responseBody"];
|
|
28
|
+
onRetry: () => void;
|
|
29
|
+
onCancel: () => void;
|
|
30
|
+
theme: Theme | "light" | "dark";
|
|
31
|
+
fundWalletOptions?: Partial<
|
|
32
|
+
Omit<
|
|
33
|
+
BuyWidgetProps,
|
|
34
|
+
| "client"
|
|
35
|
+
| "chain"
|
|
36
|
+
| "tokenAddress"
|
|
37
|
+
| "amount"
|
|
38
|
+
| "onSuccess"
|
|
39
|
+
| "onCancel"
|
|
40
|
+
| "theme"
|
|
41
|
+
>
|
|
42
|
+
>;
|
|
43
|
+
paymentRequirementsSelector?: (
|
|
44
|
+
paymentRequirements: RequestedPaymentRequirements[],
|
|
45
|
+
) => RequestedPaymentRequirements | undefined;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type Screen = "error" | "buy-widget";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
export function PaymentErrorModal(props: PaymentErrorModalProps) {
|
|
54
|
+
const {
|
|
55
|
+
client,
|
|
56
|
+
errorData,
|
|
57
|
+
onRetry,
|
|
58
|
+
onCancel,
|
|
59
|
+
theme,
|
|
60
|
+
fundWalletOptions,
|
|
61
|
+
paymentRequirementsSelector,
|
|
62
|
+
} = props;
|
|
63
|
+
const [screen, setScreen] = useState<Screen>("error");
|
|
64
|
+
const isInsufficientFunds = errorData.error === "insufficient_funds";
|
|
65
|
+
const wallet = useActiveWallet();
|
|
66
|
+
|
|
67
|
+
// Extract chain and token info from errorData for BuyWidget
|
|
68
|
+
const getBuyWidgetConfig = () => {
|
|
69
|
+
if (!errorData.accepts || errorData.accepts.length === 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get payment requirements from errorData
|
|
74
|
+
const parsedPaymentRequirements = errorData.accepts;
|
|
75
|
+
|
|
76
|
+
// Get the current chain from wallet
|
|
77
|
+
const currentChain = wallet?.getChain();
|
|
78
|
+
const currentChainId = currentChain?.id;
|
|
79
|
+
|
|
80
|
+
// Select payment requirement using the same logic as wrapFetchWithPayment
|
|
81
|
+
const selectedRequirement = paymentRequirementsSelector
|
|
82
|
+
? paymentRequirementsSelector(parsedPaymentRequirements)
|
|
83
|
+
: defaultPaymentRequirementsSelector(
|
|
84
|
+
parsedPaymentRequirements,
|
|
85
|
+
currentChainId,
|
|
86
|
+
"exact",
|
|
87
|
+
errorData.error,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (!selectedRequirement) return null;
|
|
91
|
+
|
|
92
|
+
const caip2ChainId = networkToCaip2ChainId(selectedRequirement.network);
|
|
93
|
+
const chainId = extractEvmChainId(caip2ChainId);
|
|
94
|
+
|
|
95
|
+
if (!chainId) return null;
|
|
96
|
+
|
|
97
|
+
const chain = getCachedChain(chainId);
|
|
98
|
+
const tokenAddress = selectedRequirement.asset as `0x${string}`;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
chain,
|
|
102
|
+
tokenAddress,
|
|
103
|
+
amount: undefined,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const buyWidgetConfig = isInsufficientFunds ? getBuyWidgetConfig() : null;
|
|
108
|
+
|
|
109
|
+
if (screen === "buy-widget" && buyWidgetConfig) {
|
|
110
|
+
return (
|
|
111
|
+
<CustomThemeProvider theme={theme}>
|
|
112
|
+
<Modal
|
|
113
|
+
className="tw-payment-error-modal"
|
|
114
|
+
hideCloseIcon={false}
|
|
115
|
+
open={true}
|
|
116
|
+
setOpen={(open) => {
|
|
117
|
+
if (!open) {
|
|
118
|
+
onCancel();
|
|
119
|
+
}
|
|
120
|
+
}}
|
|
121
|
+
size="compact"
|
|
122
|
+
title="Top up your wallet"
|
|
123
|
+
crossContainerStyles={{
|
|
124
|
+
position: "absolute",
|
|
125
|
+
right: spacing.lg,
|
|
126
|
+
top: spacing.lg,
|
|
127
|
+
zIndex: 1,
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{/* BuyWidget without padding */}
|
|
131
|
+
<BuyWidget
|
|
132
|
+
client={client}
|
|
133
|
+
theme={theme}
|
|
134
|
+
chain={buyWidgetConfig.chain}
|
|
135
|
+
tokenAddress={buyWidgetConfig.tokenAddress}
|
|
136
|
+
amount={buyWidgetConfig.amount}
|
|
137
|
+
style={{
|
|
138
|
+
border: "none",
|
|
139
|
+
width: "100%",
|
|
140
|
+
}}
|
|
141
|
+
buttonLabel="Continue"
|
|
142
|
+
title="Get funds"
|
|
143
|
+
description="Top up your wallet to complete your payment."
|
|
144
|
+
{...fundWalletOptions}
|
|
145
|
+
onSuccess={() => {
|
|
146
|
+
// Close modal and retry the payment
|
|
147
|
+
onRetry();
|
|
148
|
+
}}
|
|
149
|
+
onCancel={() => {
|
|
150
|
+
// Go back to error screen
|
|
151
|
+
setScreen("error");
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
</Modal>
|
|
155
|
+
</CustomThemeProvider>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Error screen (default)
|
|
160
|
+
return (
|
|
161
|
+
<CustomThemeProvider theme={theme}>
|
|
162
|
+
<Modal
|
|
163
|
+
className="tw-payment-error-modal"
|
|
164
|
+
hideCloseIcon={true}
|
|
165
|
+
open={true}
|
|
166
|
+
setOpen={(open) => {
|
|
167
|
+
if (!open) {
|
|
168
|
+
onCancel();
|
|
169
|
+
}
|
|
170
|
+
}}
|
|
171
|
+
size="compact"
|
|
172
|
+
title="Payment Failed"
|
|
173
|
+
>
|
|
174
|
+
<Container p="lg">
|
|
175
|
+
<ModalHeader title="Payment failed" />
|
|
176
|
+
|
|
177
|
+
<Container
|
|
178
|
+
flex="column"
|
|
179
|
+
gap="lg"
|
|
180
|
+
style={{
|
|
181
|
+
paddingTop: spacing.lg,
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{/* Error Message */}
|
|
185
|
+
<Text
|
|
186
|
+
size="sm"
|
|
187
|
+
style={{
|
|
188
|
+
color: "inherit",
|
|
189
|
+
lineHeight: 1.5,
|
|
190
|
+
}}
|
|
191
|
+
>
|
|
192
|
+
{isInsufficientFunds
|
|
193
|
+
? "Your wallet doesn't have enough funds to complete this payment. Please top up your wallet and try again."
|
|
194
|
+
: errorData.errorMessage ||
|
|
195
|
+
"An error occurred while processing your payment."}
|
|
196
|
+
</Text>
|
|
197
|
+
</Container>
|
|
198
|
+
</Container>
|
|
199
|
+
|
|
200
|
+
{/* Action Buttons */}
|
|
201
|
+
<ScreenBottomContainer>
|
|
202
|
+
{isInsufficientFunds && buyWidgetConfig ? (
|
|
203
|
+
<>
|
|
204
|
+
<Button
|
|
205
|
+
fullWidth
|
|
206
|
+
gap="xs"
|
|
207
|
+
onClick={() => setScreen("buy-widget")}
|
|
208
|
+
variant="accent"
|
|
209
|
+
>
|
|
210
|
+
Top up your wallet
|
|
211
|
+
</Button>
|
|
212
|
+
<Button fullWidth gap="xs" onClick={onCancel} variant="secondary">
|
|
213
|
+
Cancel
|
|
214
|
+
</Button>
|
|
215
|
+
</>
|
|
216
|
+
) : (
|
|
217
|
+
<>
|
|
218
|
+
<Button fullWidth gap="xs" onClick={onRetry} variant="accent">
|
|
219
|
+
Try Again
|
|
220
|
+
</Button>
|
|
221
|
+
<Button fullWidth gap="xs" onClick={onCancel} variant="secondary">
|
|
222
|
+
Cancel
|
|
223
|
+
</Button>
|
|
224
|
+
</>
|
|
225
|
+
)}
|
|
226
|
+
</ScreenBottomContainer>
|
|
227
|
+
</Modal>
|
|
228
|
+
</CustomThemeProvider>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Default payment requirement selector - same logic as in fetchWithPayment.ts
|
|
233
|
+
function defaultPaymentRequirementsSelector(
|
|
234
|
+
paymentRequirements: RequestedPaymentRequirements[],
|
|
235
|
+
chainId: number | undefined,
|
|
236
|
+
scheme: "exact",
|
|
237
|
+
_error?: string,
|
|
238
|
+
): RequestedPaymentRequirements | undefined {
|
|
239
|
+
if (!paymentRequirements.length) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// If we have a chainId, find matching payment requirements
|
|
244
|
+
if (chainId !== undefined) {
|
|
245
|
+
const matchingPaymentRequirements = paymentRequirements.find(
|
|
246
|
+
(x) =>
|
|
247
|
+
extractEvmChainId(networkToCaip2ChainId(x.network)) === chainId &&
|
|
248
|
+
x.scheme === scheme,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
if (matchingPaymentRequirements) {
|
|
252
|
+
return matchingPaymentRequirements;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// If no matching payment requirements, use the first payment requirement
|
|
257
|
+
const firstPaymentRequirement = paymentRequirements.find(
|
|
258
|
+
(x) => x.scheme === scheme,
|
|
259
|
+
);
|
|
260
|
+
return firstPaymentRequirement;
|
|
261
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { CustomThemeProvider } from "../../../core/design-system/CustomThemeProvider.js";
|
|
3
|
+
import type { Theme } from "../../../core/design-system/index.js";
|
|
4
|
+
import { spacing } from "../../../core/design-system/index.js";
|
|
5
|
+
import {
|
|
6
|
+
Container,
|
|
7
|
+
ModalHeader,
|
|
8
|
+
ScreenBottomContainer,
|
|
9
|
+
} from "../components/basic.js";
|
|
10
|
+
import { Button } from "../components/buttons.js";
|
|
11
|
+
import { Modal } from "../components/Modal.js";
|
|
12
|
+
import { Text } from "../components/text.js";
|
|
13
|
+
|
|
14
|
+
type SignInRequiredModalProps = {
|
|
15
|
+
theme: Theme | "light" | "dark";
|
|
16
|
+
onSignIn: () => void;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export function SignInRequiredModal(props: SignInRequiredModalProps) {
|
|
24
|
+
const { theme, onSignIn, onCancel } = props;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<CustomThemeProvider theme={theme}>
|
|
28
|
+
<Modal
|
|
29
|
+
className="tw-signin-required-modal"
|
|
30
|
+
hideCloseIcon={true}
|
|
31
|
+
open={true}
|
|
32
|
+
setOpen={(open) => {
|
|
33
|
+
if (!open) {
|
|
34
|
+
onCancel();
|
|
35
|
+
}
|
|
36
|
+
}}
|
|
37
|
+
size="compact"
|
|
38
|
+
title="Sign in required"
|
|
39
|
+
>
|
|
40
|
+
<Container p="lg">
|
|
41
|
+
<ModalHeader title="Sign in required" />
|
|
42
|
+
|
|
43
|
+
<Container
|
|
44
|
+
flex="column"
|
|
45
|
+
gap="lg"
|
|
46
|
+
style={{
|
|
47
|
+
paddingTop: spacing.lg,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{/* Description */}
|
|
51
|
+
<Text
|
|
52
|
+
size="sm"
|
|
53
|
+
style={{
|
|
54
|
+
color: "inherit",
|
|
55
|
+
lineHeight: 1.5,
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
Account required to complete payment, please sign in to continue.
|
|
59
|
+
</Text>
|
|
60
|
+
</Container>
|
|
61
|
+
</Container>
|
|
62
|
+
|
|
63
|
+
{/* Action Buttons */}
|
|
64
|
+
<ScreenBottomContainer>
|
|
65
|
+
<Button fullWidth gap="xs" onClick={onSignIn} variant="accent">
|
|
66
|
+
Sign in
|
|
67
|
+
</Button>
|
|
68
|
+
<Button fullWidth gap="xs" onClick={onCancel} variant="secondary">
|
|
69
|
+
Cancel
|
|
70
|
+
</Button>
|
|
71
|
+
</ScreenBottomContainer>
|
|
72
|
+
</Modal>
|
|
73
|
+
</CustomThemeProvider>
|
|
74
|
+
);
|
|
75
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "5.
|
|
1
|
+
export const version = "5.114.0";
|
|
@@ -474,6 +474,16 @@ async function switchChainCoinbaseWalletSDK(
|
|
|
474
474
|
provider: ProviderInterface,
|
|
475
475
|
chain: Chain,
|
|
476
476
|
) {
|
|
477
|
+
// check if chain is already connected
|
|
478
|
+
const connectedChainId = (await provider.request({
|
|
479
|
+
method: "eth_chainId",
|
|
480
|
+
})) as string | number;
|
|
481
|
+
const connectedChain = getCachedChain(normalizeChainId(connectedChainId));
|
|
482
|
+
if (connectedChain?.id === chain.id) {
|
|
483
|
+
// chain is already connected, no need to switch
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
477
487
|
const chainIdHex = numberToHex(chain.id);
|
|
478
488
|
|
|
479
489
|
try {
|