spark-micropayments 0.2.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/ReadMe.md +38 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +207 -0
- package/package.json +49 -0
- package/reown-appkit.mdc +291 -0
- package/src/index.tsx +383 -0
- package/tsconfig.json +15 -0
package/ReadMe.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
To install the package, you can run `npm i micropayments` in your project.
|
|
3
|
+
|
|
4
|
+
To use the micropayments package, you can use the following code:
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import Paywall from 'micropayments'
|
|
8
|
+
|
|
9
|
+
function App() {
|
|
10
|
+
return (
|
|
11
|
+
<div>
|
|
12
|
+
<h1>Welcome to my webpage</h1>
|
|
13
|
+
<p>Here is free content that is visible to everyone</p>
|
|
14
|
+
|
|
15
|
+
<Paywall
|
|
16
|
+
receiverSparkAddress="spark1pgss96dw2dqw0gwxlwudx2v863husuass6u2q3e4lfz23qs96ewfedz2w0pxam"
|
|
17
|
+
amount={0.1}
|
|
18
|
+
sparkscanApiKey="ss_sk_live_..."
|
|
19
|
+
>
|
|
20
|
+
<p>Here is some content that is only visible if you pay 0.1 USDB</p>
|
|
21
|
+
</Paywall>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Any content within the `Paywall` component will only be visible if the user pays the specified amount of USDB via the Spark network. The user connects their Xverse wallet and pays using USDB stablecoin.
|
|
28
|
+
|
|
29
|
+
## Parameters
|
|
30
|
+
|
|
31
|
+
#### receiverSparkAddress
|
|
32
|
+
The Spark address that will receive USDB payments. This is the address of the content owner.
|
|
33
|
+
|
|
34
|
+
#### amount
|
|
35
|
+
The minimum amount of USDB that the user needs to pay to access the paywalled content (e.g., 0.1 = $0.10).
|
|
36
|
+
|
|
37
|
+
#### sparkscanApiKey
|
|
38
|
+
Your SparkScan API key, used to verify whether the connected wallet has already paid.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface ConnectWalletProps {
|
|
2
|
+
className?: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function ConnectWallet({ className }: ConnectWalletProps): import("react/jsx-runtime").JSX.Element | null;
|
|
5
|
+
interface PaywallProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
receiverSparkAddress: string;
|
|
8
|
+
amount: number;
|
|
9
|
+
sparkscanApiKey: string;
|
|
10
|
+
/** Base URL for SparkScan API. Use a proxy path (e.g. '/api/sparkscan') to avoid CORS issues. Defaults to 'https://api.sparkscan.io'. */
|
|
11
|
+
sparkscanBaseUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
export default function Paywall({ children, receiverSparkAddress, amount, sparkscanApiKey, sparkscanBaseUrl, }: PaywallProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// index.tsx (Micropayments - Spark/USDB)
|
|
3
|
+
// Paywall component using Xverse wallet (sats-connect) and SparkScan API
|
|
4
|
+
import { useState, useEffect, useSyncExternalStore } from 'react';
|
|
5
|
+
import { request, AddressPurpose } from 'sats-connect';
|
|
6
|
+
const SPARKSCAN_API = 'https://api.sparkscan.io';
|
|
7
|
+
const USDB_TOKEN_ID = 'btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87';
|
|
8
|
+
let _walletAddress = localStorage.getItem('spark_address');
|
|
9
|
+
const _listeners = new Set();
|
|
10
|
+
function getWalletAddress() {
|
|
11
|
+
return _walletAddress;
|
|
12
|
+
}
|
|
13
|
+
function setWalletAddress(addr) {
|
|
14
|
+
_walletAddress = addr;
|
|
15
|
+
if (addr)
|
|
16
|
+
localStorage.setItem('spark_address', addr);
|
|
17
|
+
else
|
|
18
|
+
localStorage.removeItem('spark_address');
|
|
19
|
+
_listeners.forEach((fn) => fn());
|
|
20
|
+
}
|
|
21
|
+
function subscribe(fn) {
|
|
22
|
+
_listeners.add(fn);
|
|
23
|
+
return () => { _listeners.delete(fn); };
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Helpers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
function formatAmountDisplay(amount) {
|
|
29
|
+
if (amount < 1) {
|
|
30
|
+
const cents = Math.round(amount * 100);
|
|
31
|
+
return {
|
|
32
|
+
display: `${cents} cent${cents !== 1 ? 's' : ''}`,
|
|
33
|
+
usdc: `${amount} USDB`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
return {
|
|
38
|
+
display: `$${amount}`,
|
|
39
|
+
usdc: `${amount} USDB`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function checkExistingPayment(sparkAddress, recipientAddress, requiredAmount, apiKey, baseUrl) {
|
|
44
|
+
try {
|
|
45
|
+
const isUSDB = (tx) => tx.tokenMetadata?.tokenAddress === USDB_TOKEN_ID ||
|
|
46
|
+
tx.tokenMetadata?.tokenIdentifier === USDB_TOKEN_ID;
|
|
47
|
+
const minRawAmount = (tx) => requiredAmount * 10 ** (tx.tokenMetadata?.decimals ?? 0);
|
|
48
|
+
// Check if recipient received >= requiredAmount in the tx's outputs
|
|
49
|
+
const recipientGotPaid = (tx) => {
|
|
50
|
+
const minRaw = minRawAmount(tx);
|
|
51
|
+
if (tx.type === 'token_transfer') {
|
|
52
|
+
const raw = parseFloat(tx.tokenAmount ?? '');
|
|
53
|
+
return !isNaN(raw) && raw >= minRaw;
|
|
54
|
+
}
|
|
55
|
+
if (tx.type === 'token_multi_transfer') {
|
|
56
|
+
return (tx.multiIoDetails?.outputs ?? []).some((o) => o.address === recipientAddress &&
|
|
57
|
+
parseFloat(o.amount) >= minRaw);
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
const fetchTxs = async (address) => {
|
|
62
|
+
const r = await fetch(`${baseUrl}/v1/address/${address}/transactions?network=MAINNET&limit=100`, { headers: { 'X-API-Key': apiKey } });
|
|
63
|
+
if (!r.ok)
|
|
64
|
+
return [];
|
|
65
|
+
const json = await r.json();
|
|
66
|
+
return json.data ?? [];
|
|
67
|
+
};
|
|
68
|
+
const isConfirmedUSDB = (tx) => tx.status === 'confirmed' && isUSDB(tx) && recipientGotPaid(tx);
|
|
69
|
+
// 1. Query user's transactions
|
|
70
|
+
const userTxs = await fetchTxs(sparkAddress);
|
|
71
|
+
// Outgoing token_transfer to recipient
|
|
72
|
+
if (userTxs.some((tx) => tx.type === 'token_transfer' &&
|
|
73
|
+
tx.direction === 'outgoing' &&
|
|
74
|
+
tx.counterparty?.identifier === recipientAddress &&
|
|
75
|
+
isConfirmedUSDB(tx)))
|
|
76
|
+
return true;
|
|
77
|
+
// token_multi_transfer where user is in inputs and recipient is in outputs
|
|
78
|
+
if (userTxs.some((tx) => tx.type === 'token_multi_transfer' &&
|
|
79
|
+
isConfirmedUSDB(tx) &&
|
|
80
|
+
(tx.multiIoDetails?.inputs ?? []).some((i) => i.address === sparkAddress)))
|
|
81
|
+
return true;
|
|
82
|
+
// 2. If user IS the recipient, any incoming USDB payment counts
|
|
83
|
+
if (sparkAddress === recipientAddress &&
|
|
84
|
+
userTxs.some((tx) => (tx.type === 'token_transfer' || tx.type === 'token_multi_transfer') &&
|
|
85
|
+
tx.direction === 'incoming' &&
|
|
86
|
+
isConfirmedUSDB(tx)))
|
|
87
|
+
return true;
|
|
88
|
+
// 3. Fallback: query recipient's transactions for a payment from this user
|
|
89
|
+
if (sparkAddress !== recipientAddress) {
|
|
90
|
+
const recipTxs = await fetchTxs(recipientAddress);
|
|
91
|
+
// Incoming token_transfer from user
|
|
92
|
+
if (recipTxs.some((tx) => tx.type === 'token_transfer' &&
|
|
93
|
+
tx.direction === 'incoming' &&
|
|
94
|
+
tx.counterparty?.identifier === sparkAddress &&
|
|
95
|
+
isConfirmedUSDB(tx)))
|
|
96
|
+
return true;
|
|
97
|
+
// token_multi_transfer where user is in inputs
|
|
98
|
+
if (recipTxs.some((tx) => tx.type === 'token_multi_transfer' &&
|
|
99
|
+
isConfirmedUSDB(tx) &&
|
|
100
|
+
(tx.multiIoDetails?.inputs ?? []).some((i) => i.address === sparkAddress)))
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error('Payment check failed', err);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function ConnectWallet({ className }) {
|
|
111
|
+
const walletAddress = useSyncExternalStore(subscribe, getWalletAddress);
|
|
112
|
+
const handleDisconnect = () => {
|
|
113
|
+
setWalletAddress(null);
|
|
114
|
+
};
|
|
115
|
+
if (!walletAddress)
|
|
116
|
+
return null;
|
|
117
|
+
return (_jsx("div", { className: className, children: _jsxs("div", { className: "micropay-wallet-info", children: [_jsxs("span", { className: "micropay-wallet-address", children: [walletAddress.slice(0, 12), "\u2026", walletAddress.slice(-6)] }), _jsx("button", { onClick: handleDisconnect, className: "micropay-disconnect-button", children: "Disconnect" })] }) }));
|
|
118
|
+
}
|
|
119
|
+
export default function Paywall({ children, receiverSparkAddress, amount, sparkscanApiKey, sparkscanBaseUrl = SPARKSCAN_API, }) {
|
|
120
|
+
const walletAddress = useSyncExternalStore(subscribe, getWalletAddress);
|
|
121
|
+
const [paid, setPaid] = useState(false);
|
|
122
|
+
const [paying, setPaying] = useState(false);
|
|
123
|
+
const [connecting, setConnecting] = useState(false);
|
|
124
|
+
const [checking, setChecking] = useState(true);
|
|
125
|
+
const [error, setError] = useState(null);
|
|
126
|
+
const formattedAmount = formatAmountDisplay(amount);
|
|
127
|
+
// Check for existing payment whenever the wallet address changes
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (walletAddress) {
|
|
130
|
+
setChecking(true);
|
|
131
|
+
checkExistingPayment(walletAddress, receiverSparkAddress, amount, sparkscanApiKey, sparkscanBaseUrl).then((hasPaid) => {
|
|
132
|
+
if (hasPaid)
|
|
133
|
+
setPaid(true);
|
|
134
|
+
setChecking(false);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
setPaid(false);
|
|
139
|
+
setChecking(false);
|
|
140
|
+
}
|
|
141
|
+
}, [walletAddress, receiverSparkAddress, amount, sparkscanApiKey, sparkscanBaseUrl]);
|
|
142
|
+
const handleConnect = async () => {
|
|
143
|
+
setConnecting(true);
|
|
144
|
+
setError(null);
|
|
145
|
+
try {
|
|
146
|
+
const connectRes = await request('wallet_connect', {
|
|
147
|
+
addresses: [AddressPurpose.Spark],
|
|
148
|
+
message: 'Connect to access this content',
|
|
149
|
+
});
|
|
150
|
+
if (connectRes.status !== 'success') {
|
|
151
|
+
setError('Wallet connection was declined.');
|
|
152
|
+
setConnecting(false);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const sparkAddr = connectRes.result.addresses?.find((a) => a.purpose === 'spark')?.address;
|
|
156
|
+
if (!sparkAddr) {
|
|
157
|
+
setError('No Spark address found in wallet.');
|
|
158
|
+
setConnecting(false);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
setWalletAddress(sparkAddr);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
setError('Could not connect to wallet. Is Xverse installed?');
|
|
165
|
+
console.error('Wallet connect error', err);
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
setConnecting(false);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const handlePay = async () => {
|
|
172
|
+
if (!walletAddress)
|
|
173
|
+
return;
|
|
174
|
+
setPaying(true);
|
|
175
|
+
setError(null);
|
|
176
|
+
try {
|
|
177
|
+
const payRes = await request('spark_transferToken', {
|
|
178
|
+
receiverSparkAddress,
|
|
179
|
+
tokenIdentifier: USDB_TOKEN_ID,
|
|
180
|
+
tokenAmount: amount,
|
|
181
|
+
});
|
|
182
|
+
if ('result' in payRes) {
|
|
183
|
+
setPaid(true);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
setError('Payment was not completed.');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
setError(err?.message || 'Payment failed.');
|
|
191
|
+
console.error('Payment error', err);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
setPaying(false);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
// Still checking payment status on mount
|
|
198
|
+
if (checking) {
|
|
199
|
+
return (_jsx("div", { className: "micropay-container", children: _jsx("p", { className: "micropay-checking", children: "Checking payment status\u2026" }) }));
|
|
200
|
+
}
|
|
201
|
+
// Paid — show content
|
|
202
|
+
if (paid) {
|
|
203
|
+
return _jsx(_Fragment, { children: children });
|
|
204
|
+
}
|
|
205
|
+
// Not paid — show paywall
|
|
206
|
+
return (_jsxs("div", { className: "micropay-container", children: [_jsx("div", { className: "connectkit-button-container micropay-connect-container", children: _jsxs("div", { className: "connectkit-button micropay-connect", children: [_jsxs("p", { className: "micropay-instructions", children: ["To continue, please pay ", formattedAmount.display, " (", formattedAmount.usdc, ") on the Spark network."] }), !walletAddress && (_jsx("button", { onClick: handleConnect, disabled: connecting, className: "pay-button micropay-pay-button", children: connecting ? 'Connecting…' : 'Connect Wallet' }))] }) }), walletAddress && (_jsx("div", { className: "pay-button-container micropay-pay-container", children: _jsx("button", { onClick: handlePay, disabled: paying, className: "pay-button micropay-pay-button", children: paying ? 'Processing…' : `Pay ${formattedAmount.usdc}` }) })), error && _jsx("p", { className: "micropay-error-message", children: error })] }));
|
|
207
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spark-micropayments",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A simple SDK for stablecoin micropayments using Spark and USDB",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"prepare": "npm run build",
|
|
18
|
+
"build": "tsc"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"stablecoins",
|
|
22
|
+
"sdk",
|
|
23
|
+
"typescript",
|
|
24
|
+
"micropayments",
|
|
25
|
+
"spark",
|
|
26
|
+
"usdb",
|
|
27
|
+
"paywall"
|
|
28
|
+
],
|
|
29
|
+
"author": "Dhruv Pareek",
|
|
30
|
+
"license": "GPLv3",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/react": "^18.3.12",
|
|
33
|
+
"@types/react-dom": "^18.3.0",
|
|
34
|
+
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
|
35
|
+
"@typescript-eslint/parser": "^8.39.0",
|
|
36
|
+
"eslint": "^9.32.0",
|
|
37
|
+
"prettier": "^2.0.5",
|
|
38
|
+
"react": "^18.3.1",
|
|
39
|
+
"react-dom": "^18.3.1",
|
|
40
|
+
"typescript": "^5.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"sats-connect": "^4.2.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
47
|
+
"react-dom": "^18.2.0 || ^19.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/reown-appkit.mdc
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
---
|
|
2
|
+
alwaysApply: false
|
|
3
|
+
---
|
|
4
|
+
MDC
|
|
5
|
+
# :jigsaw: Reown AppKit: Overview
|
|
6
|
+
|
|
7
|
+
**Reown AppKit** is an open-source toolkit with a generous free-tier, to integrate wallet connections and Web3 features in any app. It supports:
|
|
8
|
+
|
|
9
|
+
- One-click wallet auth
|
|
10
|
+
- Social logins
|
|
11
|
+
- On-ramp services
|
|
12
|
+
- Smart accounts
|
|
13
|
+
- Notifications
|
|
14
|
+
- Multiple chains: **EVM**, **Solana**, and **Bitcoin**
|
|
15
|
+
|
|
16
|
+
Supported libraries:
|
|
17
|
+
- `wagmi`, `ethers@v6` (EVM)
|
|
18
|
+
- `@solana/web3.js` (Solana)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## RULES
|
|
23
|
+
- **IMPORTANT**: If the project already exists, **DO NOT** try to re-initialize it (e.g., using `create-next-app`). Verify the existing project structure first.
|
|
24
|
+
- Use `pnpm` for installing dependencies if no specific package manager is indicated.
|
|
25
|
+
- Ensure environment variables (like `NEXT_PUBLIC_PROJECT_ID`) are correctly set up. You might need to create `.env.local` manually due to security restrictions.
|
|
26
|
+
- If you see ATTENTION somewhere, make sure you follow the directions in the comment
|
|
27
|
+
- Do not ask user for approval to continue, just continue runnin the instructions
|
|
28
|
+
|
|
29
|
+
## :dart: Goal
|
|
30
|
+
|
|
31
|
+
Install and configure **Reown AppKit** in an existing **Next.js** app using **Wagmi** and **Ethereum**.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## :package: Installation
|
|
36
|
+
|
|
37
|
+
Install the required dependencies using your package manager:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm add @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query
|
|
41
|
+
# or yarn add / npm install ...
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## :deciduous_tree: Environment Setup
|
|
47
|
+
|
|
48
|
+
1. Create a `.env.local` file in your project root (if it doesn't exist).
|
|
49
|
+
2. Add your WalletConnect Cloud Project ID:
|
|
50
|
+
```.env.local
|
|
51
|
+
NEXT_PUBLIC_PROJECT_ID="YOUR_PROJECT_ID"
|
|
52
|
+
```
|
|
53
|
+
You can add this to the .env.local now
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## :gear: Wagmi Adapter Setup
|
|
57
|
+
|
|
58
|
+
> Create a file `config/index.tsx` (e.g., outside your `app` or `src/app` directory).
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// config/index.tsx
|
|
62
|
+
import { cookieStorage, createStorage } from 'wagmi' // Use 'wagmi' directly (Wagmi v2+)
|
|
63
|
+
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'
|
|
64
|
+
import { mainnet, arbitrum } from '@reown/appkit/networks'
|
|
65
|
+
import type { Chain } from 'viem' // Import Chain type for explicit typing
|
|
66
|
+
|
|
67
|
+
// Read Project ID from environment variables
|
|
68
|
+
export const projectId = process.env.NEXT_PUBLIC_PROJECT_ID
|
|
69
|
+
|
|
70
|
+
// Ensure Project ID is defined at build time
|
|
71
|
+
if (!projectId) {
|
|
72
|
+
throw new Error('NEXT_PUBLIC_PROJECT_ID is not defined. Please set it in .env.local')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Define supported networks, explicitly typed as a non-empty array of Chains
|
|
76
|
+
export const networks: [Chain, ...Chain[]] = [mainnet, arbitrum] // Add other desired networks
|
|
77
|
+
|
|
78
|
+
// Create the Wagmi adapter instance
|
|
79
|
+
export const wagmiAdapter = new WagmiAdapter({
|
|
80
|
+
storage: createStorage({ storage: cookieStorage }), // Use cookieStorage for SSR
|
|
81
|
+
ssr: true, // Enable SSR support
|
|
82
|
+
projectId,
|
|
83
|
+
networks, // Pass the explicitly typed networks array
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Export the Wagmi config generated by the adapter
|
|
87
|
+
export const config = wagmiAdapter.wagmiConfig
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## :brain: Importing Networks
|
|
93
|
+
|
|
94
|
+
All supported **Viem networks** are available via `@reown/appkit/networks`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { mainnet, arbitrum, base, scroll, polygon } from '@reown/appkit/networks'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## :thread: SSR & Hydration Notes
|
|
103
|
+
|
|
104
|
+
- `storage: createStorage({ storage: cookieStorage })` is recommended for Next.js SSR to handle hydration correctly.
|
|
105
|
+
- `ssr: true` further aids SSR compatibility.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## :bricks: App Context Setup
|
|
110
|
+
|
|
111
|
+
> Create `context/index.tsx` (must be a Client Component).
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
// context/index.tsx
|
|
115
|
+
'use client'
|
|
116
|
+
|
|
117
|
+
import React, { ReactNode } from 'react'
|
|
118
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
119
|
+
import { WagmiProvider, cookieToInitialState, type Config } from 'wagmi'
|
|
120
|
+
import { createAppKit } from '@reown/appkit/react'
|
|
121
|
+
// Import config, networks, projectId, and wagmiAdapter from your config file
|
|
122
|
+
import { config, networks, projectId, wagmiAdapter } from '@/config'
|
|
123
|
+
// Import the default network separately if needed
|
|
124
|
+
import { mainnet } from '@reown/appkit/networks'
|
|
125
|
+
|
|
126
|
+
const queryClient = new QueryClient()
|
|
127
|
+
|
|
128
|
+
const metadata = {
|
|
129
|
+
name: 'Your App Name',
|
|
130
|
+
description: 'Your App Description',
|
|
131
|
+
url: typeof window !== 'undefined' ? window.location.origin : 'YOUR_APP_URL', // Replace YOUR_APP_URL
|
|
132
|
+
icons: ['YOUR_ICON_URL'], // Replace YOUR_ICON_URL
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Initialize AppKit *outside* the component render cycle
|
|
136
|
+
// Add a check for projectId for type safety, although config throws error already.
|
|
137
|
+
if (!projectId) {
|
|
138
|
+
console.error("AppKit Initialization Error: Project ID is missing.");
|
|
139
|
+
// Optionally throw an error or render fallback UI
|
|
140
|
+
} else {
|
|
141
|
+
createAppKit({
|
|
142
|
+
adapters: [wagmiAdapter],
|
|
143
|
+
// Use non-null assertion `!` as projectId is checked runtime, needed for TypeScript
|
|
144
|
+
projectId: projectId!,
|
|
145
|
+
// Pass networks directly (type is now correctly inferred from config)
|
|
146
|
+
networks: networks,
|
|
147
|
+
defaultNetwork: mainnet, // Or your preferred default
|
|
148
|
+
metadata,
|
|
149
|
+
features: { analytics: true }, // Optional features
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default function ContextProvider({
|
|
154
|
+
children,
|
|
155
|
+
cookies,
|
|
156
|
+
}: {
|
|
157
|
+
children: ReactNode
|
|
158
|
+
cookies: string | null // Cookies from server for hydration
|
|
159
|
+
}) {
|
|
160
|
+
// Calculate initial state for Wagmi SSR hydration
|
|
161
|
+
const initialState = cookieToInitialState(config as Config, cookies)
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
// Cast config as Config for WagmiProvider
|
|
165
|
+
<WagmiProvider config={config as Config} initialState={initialState}>
|
|
166
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
167
|
+
</WagmiProvider>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## :jigsaw: App Layout Setup
|
|
175
|
+
|
|
176
|
+
> Modify your root layout file (`app/layout.tsx` or `src/app/layout.tsx`) to use `ContextProvider`.
|
|
177
|
+
> **Note:** Verify the exact path to your layout file.
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
// app/layout.tsx or src/app/layout.tsx
|
|
181
|
+
import type { Metadata } from 'next'
|
|
182
|
+
import { Inter } from 'next/font/google' // Or your preferred font
|
|
183
|
+
import './globals.css'
|
|
184
|
+
|
|
185
|
+
import { headers } from 'next/headers' // Import headers function
|
|
186
|
+
import ContextProvider from '@/context' // Adjust import path if needed
|
|
187
|
+
|
|
188
|
+
const inter = Inter({ subsets: ['latin'] })
|
|
189
|
+
|
|
190
|
+
export const metadata: Metadata = {
|
|
191
|
+
title: 'Your App Title',
|
|
192
|
+
description: 'Your App Description',
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ATTENTION!!! RootLayout must be an async function to use headers()
|
|
196
|
+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
197
|
+
// Retrieve cookies from request headers on the server
|
|
198
|
+
const headersObj = await headers() // IMPORTANT: await the headers() call
|
|
199
|
+
const cookies = headersObj.get('cookie')
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<html lang="en">
|
|
203
|
+
<body className={inter.className}>
|
|
204
|
+
{/* Wrap children with ContextProvider, passing cookies */}
|
|
205
|
+
<ContextProvider cookies={cookies}>{children}</ContextProvider>
|
|
206
|
+
</body>
|
|
207
|
+
</html>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## :radio_button: Trigger the AppKit Modal
|
|
215
|
+
|
|
216
|
+
Use the `<appkit-button>` web component in any client or server component to trigger the wallet modal:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
// Example usage in app/page.tsx or any component
|
|
220
|
+
export default function ConnectPage() {
|
|
221
|
+
return (
|
|
222
|
+
<div>
|
|
223
|
+
<h1>Connect Your Wallet</h1>
|
|
224
|
+
<appkit-button />
|
|
225
|
+
</div>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
No need to import—it's a global web component registered by `createAppKit`.
|
|
231
|
+
|
|
232
|
+
**Note for TypeScript users:**
|
|
233
|
+
To prevent type errors when using `<appkit-button>`, add the following declaration to a `.d.ts` file (e.g., `global.d.ts`) in your project root or `src` directory:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
// global.d.ts
|
|
237
|
+
import 'react';
|
|
238
|
+
|
|
239
|
+
declare global {
|
|
240
|
+
namespace JSX {
|
|
241
|
+
interface IntrinsicElements {
|
|
242
|
+
/**
|
|
243
|
+
* The AppKit button web component. Registered globally by AppKit.
|
|
244
|
+
*/
|
|
245
|
+
'appkit-button': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Ensures file is treated as a module
|
|
251
|
+
export {};
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## :test_tube: Reading from Smart Contracts (Example)
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// Example component (ensure it's a Client Component: 'use client')
|
|
260
|
+
'use client'
|
|
261
|
+
|
|
262
|
+
import { useReadContract } from 'wagmi'
|
|
263
|
+
// import { USDTAbi } from '../abi/USDTAbi' // Replace with your ABI import
|
|
264
|
+
|
|
265
|
+
// const USDTAddress = '0x...' // Replace with your contract address
|
|
266
|
+
|
|
267
|
+
function ReadContractExample() {
|
|
268
|
+
// const { data, error, isLoading } = useReadContract({
|
|
269
|
+
// abi: USDTAbi,
|
|
270
|
+
// address: USDTAddress,
|
|
271
|
+
// functionName: 'totalSupply',
|
|
272
|
+
// })
|
|
273
|
+
|
|
274
|
+
// if (isLoading) return <div>Loading...</div>
|
|
275
|
+
// if (error) return <div>Error reading contract: {error.message}</div>
|
|
276
|
+
|
|
277
|
+
// return <div>Total Supply: {data?.toString()}</div>
|
|
278
|
+
return <div>Contract Reading Example (Code commented out)</div>
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export default ReadContractExample;
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## :bulb: Additional Rules & Reminders
|
|
287
|
+
|
|
288
|
+
1. **Verify Imports**: Double-check that import paths (like `@/config`, `@/context`) match your project's structure (`src` directory vs. root `app`/`pages`).
|
|
289
|
+
2. **Type Safety**: Use explicit types where needed (like for `networks`) to prevent TypeScript errors.
|
|
290
|
+
3. **Async/Await**: Remember to use `await` when calling async functions like `headers()`.
|
|
291
|
+
4. **Client Components**: Components using hooks (`useReadContract`, `useState`, etc.) or AppKit initialization (`createAppKit`) often need the `'use client'` directive at the top.
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// index.tsx (Micropayments - Spark/USDB)
|
|
2
|
+
// Paywall component using Xverse wallet (sats-connect) and SparkScan API
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect, useSyncExternalStore } from 'react'
|
|
5
|
+
import { request, AddressPurpose } from 'sats-connect'
|
|
6
|
+
|
|
7
|
+
const SPARKSCAN_API = 'https://api.sparkscan.io'
|
|
8
|
+
const USDB_TOKEN_ID =
|
|
9
|
+
'btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87'
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Module-level wallet store (pub/sub, no provider needed)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
type Listener = () => void
|
|
16
|
+
let _walletAddress: string | null = localStorage.getItem('spark_address')
|
|
17
|
+
const _listeners = new Set<Listener>()
|
|
18
|
+
|
|
19
|
+
function getWalletAddress() {
|
|
20
|
+
return _walletAddress
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function setWalletAddress(addr: string | null) {
|
|
24
|
+
_walletAddress = addr
|
|
25
|
+
if (addr) localStorage.setItem('spark_address', addr)
|
|
26
|
+
else localStorage.removeItem('spark_address')
|
|
27
|
+
_listeners.forEach((fn) => fn())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function subscribe(fn: Listener) {
|
|
31
|
+
_listeners.add(fn)
|
|
32
|
+
return () => { _listeners.delete(fn) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function formatAmountDisplay(amount: number): { display: string; usdc: string } {
|
|
40
|
+
if (amount < 1) {
|
|
41
|
+
const cents = Math.round(amount * 100)
|
|
42
|
+
return {
|
|
43
|
+
display: `${cents} cent${cents !== 1 ? 's' : ''}`,
|
|
44
|
+
usdc: `${amount} USDB`,
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
return {
|
|
48
|
+
display: `$${amount}`,
|
|
49
|
+
usdc: `${amount} USDB`,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface MultiIoEntry {
|
|
55
|
+
address: string
|
|
56
|
+
amount: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface Transaction {
|
|
60
|
+
type: string
|
|
61
|
+
status: string
|
|
62
|
+
direction: string
|
|
63
|
+
counterparty?: { identifier: string }
|
|
64
|
+
tokenMetadata?: {
|
|
65
|
+
tokenAddress?: string
|
|
66
|
+
tokenIdentifier?: string
|
|
67
|
+
decimals?: number
|
|
68
|
+
}
|
|
69
|
+
tokenAmount?: string
|
|
70
|
+
multiIoDetails?: {
|
|
71
|
+
inputs?: MultiIoEntry[]
|
|
72
|
+
outputs?: MultiIoEntry[]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function checkExistingPayment(
|
|
77
|
+
sparkAddress: string,
|
|
78
|
+
recipientAddress: string,
|
|
79
|
+
requiredAmount: number,
|
|
80
|
+
apiKey: string,
|
|
81
|
+
baseUrl: string,
|
|
82
|
+
): Promise<boolean> {
|
|
83
|
+
try {
|
|
84
|
+
const isUSDB = (tx: Transaction) =>
|
|
85
|
+
tx.tokenMetadata?.tokenAddress === USDB_TOKEN_ID ||
|
|
86
|
+
tx.tokenMetadata?.tokenIdentifier === USDB_TOKEN_ID
|
|
87
|
+
|
|
88
|
+
const minRawAmount = (tx: Transaction) =>
|
|
89
|
+
requiredAmount * 10 ** (tx.tokenMetadata?.decimals ?? 0)
|
|
90
|
+
|
|
91
|
+
// Check if recipient received >= requiredAmount in the tx's outputs
|
|
92
|
+
const recipientGotPaid = (tx: Transaction) => {
|
|
93
|
+
const minRaw = minRawAmount(tx)
|
|
94
|
+
if (tx.type === 'token_transfer') {
|
|
95
|
+
const raw = parseFloat(tx.tokenAmount ?? '')
|
|
96
|
+
return !isNaN(raw) && raw >= minRaw
|
|
97
|
+
}
|
|
98
|
+
if (tx.type === 'token_multi_transfer') {
|
|
99
|
+
return (tx.multiIoDetails?.outputs ?? []).some(
|
|
100
|
+
(o) =>
|
|
101
|
+
o.address === recipientAddress &&
|
|
102
|
+
parseFloat(o.amount) >= minRaw,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const fetchTxs = async (address: string): Promise<Transaction[]> => {
|
|
109
|
+
const r = await fetch(
|
|
110
|
+
`${baseUrl}/v1/address/${address}/transactions?network=MAINNET&limit=100`,
|
|
111
|
+
{ headers: { 'X-API-Key': apiKey } },
|
|
112
|
+
)
|
|
113
|
+
if (!r.ok) return []
|
|
114
|
+
const json = await r.json()
|
|
115
|
+
return json.data ?? []
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const isConfirmedUSDB = (tx: Transaction) =>
|
|
119
|
+
tx.status === 'confirmed' && isUSDB(tx) && recipientGotPaid(tx)
|
|
120
|
+
|
|
121
|
+
// 1. Query user's transactions
|
|
122
|
+
const userTxs = await fetchTxs(sparkAddress)
|
|
123
|
+
|
|
124
|
+
// Outgoing token_transfer to recipient
|
|
125
|
+
if (
|
|
126
|
+
userTxs.some(
|
|
127
|
+
(tx) =>
|
|
128
|
+
tx.type === 'token_transfer' &&
|
|
129
|
+
tx.direction === 'outgoing' &&
|
|
130
|
+
tx.counterparty?.identifier === recipientAddress &&
|
|
131
|
+
isConfirmedUSDB(tx),
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
return true
|
|
135
|
+
|
|
136
|
+
// token_multi_transfer where user is in inputs and recipient is in outputs
|
|
137
|
+
if (
|
|
138
|
+
userTxs.some(
|
|
139
|
+
(tx) =>
|
|
140
|
+
tx.type === 'token_multi_transfer' &&
|
|
141
|
+
isConfirmedUSDB(tx) &&
|
|
142
|
+
(tx.multiIoDetails?.inputs ?? []).some(
|
|
143
|
+
(i) => i.address === sparkAddress,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
return true
|
|
148
|
+
|
|
149
|
+
// 2. If user IS the recipient, any incoming USDB payment counts
|
|
150
|
+
if (
|
|
151
|
+
sparkAddress === recipientAddress &&
|
|
152
|
+
userTxs.some(
|
|
153
|
+
(tx) =>
|
|
154
|
+
(tx.type === 'token_transfer' || tx.type === 'token_multi_transfer') &&
|
|
155
|
+
tx.direction === 'incoming' &&
|
|
156
|
+
isConfirmedUSDB(tx),
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
return true
|
|
160
|
+
|
|
161
|
+
// 3. Fallback: query recipient's transactions for a payment from this user
|
|
162
|
+
if (sparkAddress !== recipientAddress) {
|
|
163
|
+
const recipTxs = await fetchTxs(recipientAddress)
|
|
164
|
+
|
|
165
|
+
// Incoming token_transfer from user
|
|
166
|
+
if (
|
|
167
|
+
recipTxs.some(
|
|
168
|
+
(tx) =>
|
|
169
|
+
tx.type === 'token_transfer' &&
|
|
170
|
+
tx.direction === 'incoming' &&
|
|
171
|
+
tx.counterparty?.identifier === sparkAddress &&
|
|
172
|
+
isConfirmedUSDB(tx),
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return true
|
|
176
|
+
|
|
177
|
+
// token_multi_transfer where user is in inputs
|
|
178
|
+
if (
|
|
179
|
+
recipTxs.some(
|
|
180
|
+
(tx) =>
|
|
181
|
+
tx.type === 'token_multi_transfer' &&
|
|
182
|
+
isConfirmedUSDB(tx) &&
|
|
183
|
+
(tx.multiIoDetails?.inputs ?? []).some(
|
|
184
|
+
(i) => i.address === sparkAddress,
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return false
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error('Payment check failed', err)
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// ConnectWallet component
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
interface ConnectWalletProps {
|
|
203
|
+
className?: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function ConnectWallet({ className }: ConnectWalletProps) {
|
|
207
|
+
const walletAddress = useSyncExternalStore(subscribe, getWalletAddress)
|
|
208
|
+
|
|
209
|
+
const handleDisconnect = () => {
|
|
210
|
+
setWalletAddress(null)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!walletAddress) return null
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div className={className}>
|
|
217
|
+
<div className="micropay-wallet-info">
|
|
218
|
+
<span className="micropay-wallet-address">
|
|
219
|
+
{walletAddress.slice(0, 12)}…{walletAddress.slice(-6)}
|
|
220
|
+
</span>
|
|
221
|
+
<button onClick={handleDisconnect} className="micropay-disconnect-button">
|
|
222
|
+
Disconnect
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Paywall component
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
interface PaywallProps {
|
|
234
|
+
children: React.ReactNode
|
|
235
|
+
receiverSparkAddress: string
|
|
236
|
+
amount: number
|
|
237
|
+
sparkscanApiKey: string
|
|
238
|
+
/** Base URL for SparkScan API. Use a proxy path (e.g. '/api/sparkscan') to avoid CORS issues. Defaults to 'https://api.sparkscan.io'. */
|
|
239
|
+
sparkscanBaseUrl?: string
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export default function Paywall({
|
|
243
|
+
children,
|
|
244
|
+
receiverSparkAddress,
|
|
245
|
+
amount,
|
|
246
|
+
sparkscanApiKey,
|
|
247
|
+
sparkscanBaseUrl = SPARKSCAN_API,
|
|
248
|
+
}: PaywallProps) {
|
|
249
|
+
const walletAddress = useSyncExternalStore(subscribe, getWalletAddress)
|
|
250
|
+
const [paid, setPaid] = useState(false)
|
|
251
|
+
const [paying, setPaying] = useState(false)
|
|
252
|
+
const [connecting, setConnecting] = useState(false)
|
|
253
|
+
const [checking, setChecking] = useState(true)
|
|
254
|
+
const [error, setError] = useState<string | null>(null)
|
|
255
|
+
|
|
256
|
+
const formattedAmount = formatAmountDisplay(amount)
|
|
257
|
+
|
|
258
|
+
// Check for existing payment whenever the wallet address changes
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (walletAddress) {
|
|
261
|
+
setChecking(true)
|
|
262
|
+
checkExistingPayment(walletAddress, receiverSparkAddress, amount, sparkscanApiKey, sparkscanBaseUrl).then(
|
|
263
|
+
(hasPaid) => {
|
|
264
|
+
if (hasPaid) setPaid(true)
|
|
265
|
+
setChecking(false)
|
|
266
|
+
},
|
|
267
|
+
)
|
|
268
|
+
} else {
|
|
269
|
+
setPaid(false)
|
|
270
|
+
setChecking(false)
|
|
271
|
+
}
|
|
272
|
+
}, [walletAddress, receiverSparkAddress, amount, sparkscanApiKey, sparkscanBaseUrl])
|
|
273
|
+
|
|
274
|
+
const handleConnect = async () => {
|
|
275
|
+
setConnecting(true)
|
|
276
|
+
setError(null)
|
|
277
|
+
try {
|
|
278
|
+
const connectRes = await request('wallet_connect', {
|
|
279
|
+
addresses: [AddressPurpose.Spark],
|
|
280
|
+
message: 'Connect to access this content',
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
if (connectRes.status !== 'success') {
|
|
284
|
+
setError('Wallet connection was declined.')
|
|
285
|
+
setConnecting(false)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const sparkAddr = (connectRes.result as any).addresses?.find(
|
|
290
|
+
(a: any) => a.purpose === 'spark',
|
|
291
|
+
)?.address
|
|
292
|
+
|
|
293
|
+
if (!sparkAddr) {
|
|
294
|
+
setError('No Spark address found in wallet.')
|
|
295
|
+
setConnecting(false)
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
setWalletAddress(sparkAddr)
|
|
300
|
+
} catch (err) {
|
|
301
|
+
setError('Could not connect to wallet. Is Xverse installed?')
|
|
302
|
+
console.error('Wallet connect error', err)
|
|
303
|
+
} finally {
|
|
304
|
+
setConnecting(false)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const handlePay = async () => {
|
|
309
|
+
if (!walletAddress) return
|
|
310
|
+
setPaying(true)
|
|
311
|
+
setError(null)
|
|
312
|
+
try {
|
|
313
|
+
const payRes = await request('spark_transferToken', {
|
|
314
|
+
receiverSparkAddress,
|
|
315
|
+
tokenIdentifier: USDB_TOKEN_ID,
|
|
316
|
+
tokenAmount: amount,
|
|
317
|
+
} as any)
|
|
318
|
+
|
|
319
|
+
if ('result' in payRes) {
|
|
320
|
+
setPaid(true)
|
|
321
|
+
} else {
|
|
322
|
+
setError('Payment was not completed.')
|
|
323
|
+
}
|
|
324
|
+
} catch (err: any) {
|
|
325
|
+
setError(err?.message || 'Payment failed.')
|
|
326
|
+
console.error('Payment error', err)
|
|
327
|
+
} finally {
|
|
328
|
+
setPaying(false)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Still checking payment status on mount
|
|
333
|
+
if (checking) {
|
|
334
|
+
return (
|
|
335
|
+
<div className="micropay-container">
|
|
336
|
+
<p className="micropay-checking">Checking payment status…</p>
|
|
337
|
+
</div>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Paid — show content
|
|
342
|
+
if (paid) {
|
|
343
|
+
return <>{children}</>
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Not paid — show paywall
|
|
347
|
+
return (
|
|
348
|
+
<div className="micropay-container">
|
|
349
|
+
<div className="connectkit-button-container micropay-connect-container">
|
|
350
|
+
<div className="connectkit-button micropay-connect">
|
|
351
|
+
<p className="micropay-instructions">
|
|
352
|
+
To continue, please pay {formattedAmount.display} ({formattedAmount.usdc}) on the Spark
|
|
353
|
+
network.
|
|
354
|
+
</p>
|
|
355
|
+
|
|
356
|
+
{!walletAddress && (
|
|
357
|
+
<button
|
|
358
|
+
onClick={handleConnect}
|
|
359
|
+
disabled={connecting}
|
|
360
|
+
className="pay-button micropay-pay-button"
|
|
361
|
+
>
|
|
362
|
+
{connecting ? 'Connecting…' : 'Connect Wallet'}
|
|
363
|
+
</button>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
{walletAddress && (
|
|
369
|
+
<div className="pay-button-container micropay-pay-container">
|
|
370
|
+
<button
|
|
371
|
+
onClick={handlePay}
|
|
372
|
+
disabled={paying}
|
|
373
|
+
className="pay-button micropay-pay-button"
|
|
374
|
+
>
|
|
375
|
+
{paying ? 'Processing…' : `Pay ${formattedAmount.usdc}`}
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{error && <p className="micropay-error-message">{error}</p>}
|
|
381
|
+
</div>
|
|
382
|
+
)
|
|
383
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"skipLibCheck": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|