create-sbc-app 0.1.5 → 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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -6
  3. package/bin/cli.js +30 -7
  4. package/package.json +1 -2
  5. package/templates/README.md +0 -2
  6. package/templates/react/package.json.template +3 -5
  7. package/templates/react-dynamic/.env.template +7 -0
  8. package/templates/react-dynamic/README.md.template +24 -0
  9. package/templates/react-dynamic/eslint.config.js.template +34 -0
  10. package/templates/react-dynamic/index.html.template +14 -0
  11. package/templates/react-dynamic/package.json.template +33 -0
  12. package/templates/react-dynamic/postcss.config.js.template +8 -0
  13. package/templates/react-dynamic/public/sbc-logo.png +0 -0
  14. package/templates/react-dynamic/src/App.css.template +5 -0
  15. package/templates/react-dynamic/src/App.tsx.template +322 -0
  16. package/templates/react-dynamic/src/env.d.ts.template +14 -0
  17. package/templates/react-dynamic/src/index.css.template +15 -0
  18. package/templates/react-dynamic/src/main.tsx.template +12 -0
  19. package/templates/react-dynamic/tailwind.config.js.template +13 -0
  20. package/templates/react-dynamic/tsconfig.json.template +18 -0
  21. package/templates/react-dynamic/vite.config.ts.template +11 -0
  22. package/templates/react-para/.env.template +7 -0
  23. package/templates/react-para/README.md.template +24 -0
  24. package/templates/react-para/eslint.config.js.template +34 -0
  25. package/templates/react-para/index.html.template +14 -0
  26. package/templates/react-para/package.json.template +35 -0
  27. package/templates/react-para/postcss.config.js.template +8 -0
  28. package/templates/react-para/public/sbc-logo.png +0 -0
  29. package/templates/react-para/src/App.tsx.template +333 -0
  30. package/templates/react-para/src/components/ConnectButton.tsx.template +99 -0
  31. package/templates/react-para/src/env.d.ts.template +14 -0
  32. package/templates/react-para/src/hooks/usePara.ts.template +34 -0
  33. package/templates/react-para/src/hooks/useParaViem.ts.template +61 -0
  34. package/templates/react-para/src/index.css.template +5 -0
  35. package/templates/react-para/src/main.tsx.template +12 -0
  36. package/templates/react-para/src/providers.tsx.template +39 -0
  37. package/templates/react-para/src/utils/permit.ts.template +217 -0
  38. package/templates/react-para/tailwind.config.js.template +13 -0
  39. package/templates/react-para/tsconfig.json.template +18 -0
  40. package/templates/react-para/vite.config.ts.template +74 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Stable Coin Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -33,7 +33,7 @@ Arguments:
33
33
 
34
34
  Options:
35
35
  -V, --version output the version number
36
- -t, --template <type> Template to use: react, nextjs, or backend
36
+ -t, --template <type> Template to use: react, react-dynamic, or react-para
37
37
  --api-key <apiKey> Your SBC API key for immediate configuration
38
38
  --wallet <wallet> Wallet integration (not yet implemented)
39
39
  -h, --help display help for command
@@ -41,11 +41,14 @@ Options:
41
41
  Examples:
42
42
  $ create-sbc-app my-app
43
43
  $ create-sbc-app my-app --template react
44
- $ create-sbc-app my-app --template react --api-key your-api-key
44
+ $ create-sbc-app my-app --template react-dynamic
45
+ $ create-sbc-app my-app --template react-para
46
+ # Next.js template removed for now
45
47
 
46
48
  Available Templates:
47
- - react React + Vite template with SBC integration
48
- - nextjs Next.js template with SBC integration (coming soon)
49
+ - react React + Vite template with SBC integration
50
+ - react-dynamic React + Vite with Dynamic wallet integration
51
+ - react-para React + Vite with Para wallet integration
49
52
  ```
50
53
 
51
54
  ## ✨ Features
@@ -166,8 +169,8 @@ npm run dev
166
169
 
167
170
  ## 📚 Documentation
168
171
 
169
- - **[SBC API Documentation](https://docs.stablecoin.xyz)** - Complete API reference
170
- - **[GitHub Repository](https://github.com/stablecoinxyz/app-kit)** - Source code and examples
172
+ - **[SBC Documentation](https://docs.stablecoin.xyz)** - Official docs
173
+ - **[GitHub Repository](https://github.com/stablecoinxyz/app-kit)** - AppKit API Reference. Source code and examples
171
174
 
172
175
  ## 📄 License
173
176
 
package/bin/cli.js CHANGED
@@ -10,9 +10,9 @@ const program = new Command();
10
10
  program
11
11
  .name('create-sbc-app')
12
12
  .description('Create a new SBC App Kit project with an opinionated template')
13
- .version('0.1.0')
13
+ .version('0.2.0')
14
14
  .argument('[project-directory]', 'Directory to create the new app in')
15
- .option('-t, --template <template>', 'Template to use: react, nextjs, or backend')
15
+ .option('-t, --template <template>', 'Template to use: react, react-dynamic, or react-para')
16
16
  .option('--api-key <apiKey>', 'Your SBC API key for immediate configuration')
17
17
  .option('--wallet <wallet>', 'Wallet integration (not yet implemented)')
18
18
  .addHelpText('after', `
@@ -22,8 +22,9 @@ Examples:
22
22
  $ create-sbc-app my-app --template react --api-key your-api-key
23
23
 
24
24
  Available Templates:
25
- - react React + Vite template with SBC integration
26
- - nextjs Next.js template with SBC integration (coming soon)
25
+ - react React + Vite template with SBC integration
26
+ - react-dynamic React + Vite with Dynamic wallet integration
27
+ - react-para React + Vite with Para wallet integration
27
28
  `)
28
29
  .action(async (dir, options) => {
29
30
  if (options.wallet) {
@@ -32,7 +33,8 @@ Available Templates:
32
33
  }
33
34
  const templateChoices = [
34
35
  { title: 'React', value: 'react' },
35
- { title: 'Next.js', value: 'nextjs' }
36
+ { title: 'React (Dynamic wallet)', value: 'react-dynamic' },
37
+ { title: 'React (Para wallet)', value: 'react-para' }
36
38
  ];
37
39
  // Use provided argument or prompt for project directory
38
40
  let projectDir = dir && dir.trim() ? dir.trim() : '';
@@ -49,7 +51,7 @@ Available Templates:
49
51
  projectDir = res.dir.trim();
50
52
  }
51
53
  // Use provided option or prompt for template
52
- let template = options.template && ['react', 'nextjs'].includes(options.template) ? options.template : '';
54
+ let template = options.template && ['react', 'react-dynamic', 'react-para'].includes(options.template) ? options.template : '';
53
55
  if (!template) {
54
56
  const res = await prompts({
55
57
  type: 'select',
@@ -62,7 +64,7 @@ Available Templates:
62
64
  process.exit(1);
63
65
  }
64
66
  template = res.template; // The value is already what we want from the choices
65
- if (!template || !['react', 'nextjs'].includes(template)) {
67
+ if (!template || !['react', 'react-dynamic', 'react-para'].includes(template)) {
66
68
  console.log('Template selection is required.');
67
69
  process.exit(1);
68
70
  }
@@ -97,6 +99,27 @@ Available Templates:
97
99
  chain: 'baseSepolia',
98
100
  apiKey: apiKey
99
101
  });
102
+ // Ensure SBC logo exists in public/ for all templates
103
+ try {
104
+ const publicDir = path.join(targetDir, 'public');
105
+ await fs.ensureDir(publicDir);
106
+ const sourceLogo = path.resolve(__dirname, '../templates/react/public/sbc-logo.png');
107
+ const destLogo = path.join(publicDir, 'sbc-logo.png');
108
+ if (!(await fs.pathExists(destLogo)) && (await fs.pathExists(sourceLogo))) {
109
+ await fs.copy(sourceLogo, destLogo);
110
+ }
111
+ }
112
+ catch { }
113
+ // If .env.template exists, ensure apiKey placeholder is applied (copyTemplate already replaces)
114
+ // Also, create a default .env if none exists to make onboarding faster
115
+ try {
116
+ const envTemplatePath = path.join(targetDir, '.env.template');
117
+ const envPath = path.join(targetDir, '.env');
118
+ if (await fs.pathExists(envTemplatePath) && !(await fs.pathExists(envPath))) {
119
+ await fs.copy(envTemplatePath, envPath);
120
+ }
121
+ }
122
+ catch { }
100
123
  console.log(`\nSuccess! Created ${projectDir} using the ${template} template.`);
101
124
  console.log(`\nNext steps:`);
102
125
  console.log(` cd ${projectDir}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sbc-app",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold a new SBC App Kit project with one command.",
5
5
  "bin": {
6
6
  "create-sbc-app": "bin/cli.js"
@@ -47,7 +47,6 @@
47
47
  "create-app",
48
48
  "template",
49
49
  "react",
50
- "nextjs",
51
50
  "vite"
52
51
  ],
53
52
  "author": "SBC Team",
@@ -12,8 +12,6 @@ This directory contains ready-to-use templates for quickly starting new projects
12
12
 
13
13
  ```bash
14
14
  cp -r create-sbc-app/react my-new-sbc-app
15
- # or for Next.js:
16
- cp -r create-sbc-app/nextjs my-new-sbc-next-app
17
15
  ```
18
16
 
19
17
  2. **Install dependencies:**
@@ -11,13 +11,11 @@
11
11
  "dependencies": {
12
12
  "@stablecoin.xyz/core": "latest",
13
13
  "@stablecoin.xyz/react": "latest",
14
- "react": "^18.2.0",
15
- "react-dom": "^18.2.0",
16
- "viem": "^2.31.0"
14
+ "react": "^19.1.0",
15
+ "react-dom": "^19.1.0",
16
+ "viem": "^2.33.0"
17
17
  },
18
18
  "devDependencies": {
19
- "@types/react": "^18.2.0",
20
- "@types/react-dom": "^18.2.0",
21
19
  "@vitejs/plugin-react": "^4.0.0",
22
20
  "typescript": "^5.0.0",
23
21
  "vite": "^5.0.0"
@@ -0,0 +1,7 @@
1
+ # Get your SBC API Key at https://dashboard.stablecoin.xyz
2
+ VITE_SBC_API_KEY={{apiKey}}
3
+ # Get your Dynamic Environment ID at https://app.dynamic.xyz/
4
+ VITE_DYNAMIC_ENVIRONMENT_ID=your_dynamic_env_id
5
+ # Optional:
6
+ VITE_CHAIN=baseSepolia
7
+ VITE_RPC_URL=
@@ -0,0 +1,24 @@
1
+ # React + Dynamic + SBC App Kit
2
+
3
+ Gasless transactions on Base using Dynamic SDK with SBC App Kit.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm dev
10
+ ```
11
+
12
+ ## Environment
13
+
14
+ Create `.env` from `.env.template` and fill in values:
15
+
16
+ ```env
17
+ VITE_SBC_API_KEY={{apiKey}}
18
+ VITE_DYNAMIC_ENVIRONMENT_ID=your_dynamic_env_id
19
+ # Optional
20
+ VITE_CHAIN=baseSepolia # or "base"
21
+ VITE_RPC_URL=
22
+ ```
23
+
24
+
@@ -0,0 +1,34 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from '@typescript-eslint/eslint-plugin'
6
+ import tsparser from '@typescript-eslint/parser'
7
+
8
+ export default [
9
+ { ignores: ['dist'] },
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ parser: tsparser,
16
+ },
17
+ plugins: {
18
+ '@typescript-eslint': tseslint,
19
+ 'react-hooks': reactHooks,
20
+ 'react-refresh': reactRefresh,
21
+ },
22
+ rules: {
23
+ ...js.configs.recommended.rules,
24
+ ...tseslint.configs.recommended.rules,
25
+ ...reactHooks.configs.recommended.rules,
26
+ 'react-refresh/only-export-components': [
27
+ 'warn',
28
+ { allowConstantExport: true },
29
+ ],
30
+ },
31
+ },
32
+ ]
33
+
34
+
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{projectName}} – SBC + Dynamic</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
13
+
14
+
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@dynamic-labs/ethereum": "^4.25.7",
13
+ "@dynamic-labs/ethereum-aa": "^4.25.7",
14
+ "@dynamic-labs/sdk-react-core": "^4.25.7",
15
+ "@stablecoin.xyz/core": "latest",
16
+ "@stablecoin.xyz/react": "latest",
17
+ "react": "^19.1.0",
18
+ "react-dom": "^19.1.0",
19
+ "viem": "^2.33.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/react": "^18.2.0",
23
+ "@types/react-dom": "^18.2.0",
24
+ "@vitejs/plugin-react": "^4.0.0",
25
+ "autoprefixer": "^10.4.0",
26
+ "postcss": "^8.4.0",
27
+ "tailwindcss": "^3.4.17",
28
+ "typescript": "^5.0.0",
29
+ "vite": "^5.0.0"
30
+ }
31
+ }
32
+
33
+
@@ -0,0 +1,8 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
7
+
8
+
@@ -0,0 +1,5 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+
@@ -0,0 +1,322 @@
1
+ import { DynamicContextProvider, useDynamicContext, DynamicUserProfile, DynamicWidget } from '@dynamic-labs/sdk-react-core';
2
+ import { EthereumWalletConnectors } from '@dynamic-labs/ethereum';
3
+ import { ZeroDevSmartWalletConnectors } from '@dynamic-labs/ethereum-aa';
4
+ import { useSbcDynamic } from '@stablecoin.xyz/react';
5
+ import { baseSepolia, base, type Chain } from 'viem/chains';
6
+ import { createPublicClient, http, getAddress, parseUnits, encodeFunctionData, erc20Abi } from 'viem';
7
+ import { useEffect, useState } from 'react';
8
+ import './App.css';
9
+
10
+ const chain = (import.meta.env.VITE_CHAIN === 'base') ? base : baseSepolia;
11
+ const rpcUrl = import.meta.env.VITE_RPC_URL;
12
+
13
+ const SBC_TOKEN_ADDRESS = (chain: Chain) => {
14
+ if (chain.id === baseSepolia.id) return '0xf9FB20B8E097904f0aB7d12e9DbeE88f2dcd0F16';
15
+ if (chain.id === base.id) return '0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798';
16
+ throw new Error('Unsupported chain');
17
+ };
18
+
19
+ const SBC_DECIMALS = (chain: Chain) => chain.id === baseSepolia.id ? 6 : 18;
20
+
21
+ const chainExplorer = (chain: Chain) => {
22
+ if (chain.id === baseSepolia.id) return 'https://sepolia.basescan.org';
23
+ if (chain.id === base.id) return 'https://basescan.org';
24
+ return '';
25
+ };
26
+
27
+ const publicClient = createPublicClient({ chain, transport: http(rpcUrl) });
28
+
29
+ // ERC20 + EIP-2612 nonces helper ABI
30
+ const erc20PermitAbi = [
31
+ ...erc20Abi,
32
+ {
33
+ "inputs": [
34
+ { "internalType": "address", "name": "owner", "type": "address" }
35
+ ],
36
+ "name": "nonces",
37
+ "outputs": [
38
+ { "internalType": "uint256", "name": "", "type": "uint256" }
39
+ ],
40
+ "stateMutability": "view",
41
+ "type": "function"
42
+ }
43
+ ] as const;
44
+
45
+ // Wrapper to render DynamicWidget only after SDK is ready
46
+ const DynamicWidgetWrapper = () => {
47
+ const { sdkHasLoaded } = useDynamicContext();
48
+ if (!sdkHasLoaded) return null;
49
+ return <DynamicWidget />;
50
+ };
51
+
52
+ // Wrapper to render DynamicUserProfile only after SDK is ready
53
+ const DynamicUserProfileWrapper = () => {
54
+ const { sdkHasLoaded } = useDynamicContext();
55
+ if (!sdkHasLoaded) return null;
56
+ return <DynamicUserProfile />;
57
+ };
58
+
59
+ function WalletStatus() {
60
+ const { primaryWallet } = useDynamicContext();
61
+ const [balances, setBalances] = useState<{ eth: string | null; sbc: string | null }>({ eth: null, sbc: null });
62
+
63
+ useEffect(() => {
64
+ if (!primaryWallet?.address) return;
65
+ (async () => {
66
+ try {
67
+ const [ethBalance, sbcBalance] = await Promise.all([
68
+ publicClient.getBalance({ address: primaryWallet.address as `0x${string}` }),
69
+ publicClient.readContract({
70
+ address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
71
+ abi: erc20Abi,
72
+ functionName: 'balanceOf',
73
+ args: [primaryWallet.address as `0x${string}`],
74
+ })
75
+ ]);
76
+ setBalances({ eth: ethBalance.toString(), sbc: (sbcBalance as bigint).toString() });
77
+ } catch {
78
+ setBalances({ eth: null, sbc: null });
79
+ }
80
+ })();
81
+ }, [primaryWallet?.address]);
82
+
83
+ if (!primaryWallet) return null;
84
+ const fmtEth = (v: string | null) => v ? (Number(v) / 1e18).toFixed(4) : '0.0000';
85
+ const fmtSbc = (v: string | null) => v ? (Number(v) / Math.pow(10, SBC_DECIMALS(chain))).toFixed(4) : '0.0000';
86
+
87
+ return (
88
+ <div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
89
+ <div className="flex justify-between items-start mb-3">
90
+ <div className="flex-1">
91
+ <h3 className="font-semibold text-green-800 mb-1">✅ Dynamic Wallet Connected</h3>
92
+ <p className="text-xs text-green-600 font-mono break-all mb-2">EOA: {primaryWallet.address}</p>
93
+ <p className="text-xs text-green-600 mb-2">Connected via Dynamic SDK</p>
94
+ <p className="text-xs text-green-600 mb-2"><strong>Chain:</strong> {chain.name} (ID: {chain.id})</p>
95
+ <div className="mt-2 pt-2 border-t border-green-200">
96
+ <p className="text-xs font-medium text-green-700 mb-1">Wallet Balances:</p>
97
+ <div className="flex gap-4">
98
+ <span className="text-xs text-green-600"><strong>ETH:</strong> {fmtEth(balances.eth)}</span>
99
+ <span className="text-xs text-green-600"><strong>SBC:</strong> {fmtSbc(balances.sbc)}</span>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ function SmartAccountInfo({ account, refreshAccount, isLoadingAccount, accountError }: any) {
109
+ const [sbcBalance, setSbcBalance] = useState<string | null>(null);
110
+ useEffect(() => {
111
+ if (!account?.address) return;
112
+ (async () => {
113
+ try {
114
+ const bal = await publicClient.readContract({
115
+ address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`,
116
+ abi: erc20Abi,
117
+ functionName: 'balanceOf',
118
+ args: [account.address as `0x${string}`],
119
+ });
120
+ setSbcBalance((bal as bigint).toString());
121
+ } catch { setSbcBalance('0'); }
122
+ })();
123
+ }, [account?.address]);
124
+
125
+ if (!account) return null;
126
+ const fmtEth = (v: string | null) => v ? (Number(v) / 1e18).toFixed(6) : '0.000000';
127
+ const fmtSbc = (v: string | null) => v ? (Number(v) / Math.pow(10, SBC_DECIMALS(chain))).toFixed(2) : '0.00';
128
+
129
+ return (
130
+ <div className="mb-6 p-4 bg-purple-50 border border-purple-200 rounded-lg">
131
+ <div className="flex justify-between items-center mb-2">
132
+ <h3 className="font-semibold text-purple-800">🔐 Smart Account Status</h3>
133
+ <button onClick={refreshAccount} disabled={isLoadingAccount} className="text-xs bg-purple-600 text-white px-3 py-1 rounded hover:bg-purple-700 disabled:opacity-50">{isLoadingAccount ? '🔄 Refreshing...' : '🔄 Refresh'}</button>
134
+ </div>
135
+ <div className="space-y-2 text-sm">
136
+ <div className="flex justify-between"><span className="text-purple-700">Smart Account Address:</span><span className="font-mono text-xs text-purple-600 break-all">{account.address}</span></div>
137
+ <div className="flex justify-between"><span className="text-purple-700">Deployed:</span><span className="text-purple-600">{account.isDeployed ? '✅ Yes' : '⏳ On first transaction'}</span></div>
138
+ <div className="flex justify-between"><span className="text-purple-700">Nonce:</span><span className="text-purple-600">{account.nonce}</span></div>
139
+ <div className="pt-2 border-t border-purple-200">
140
+ <p className="text-xs font-medium text-purple-700 mb-2">Smart Account Balances:</p>
141
+ <div className="space-y-1">
142
+ <div className="flex justify-between"><span className="text-purple-700">ETH:</span><span className="text-purple-600 font-mono text-xs">{fmtEth(account.balance)} ETH</span></div>
143
+ <div className="flex justify-between"><span className="text-purple-700">SBC:</span><span className="text-purple-600 font-mono text-xs">{fmtSbc(sbcBalance)} SBC</span></div>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ {accountError && <p className="mt-2 text-xs text-red-600">{String(accountError)}</p>}
148
+ </div>
149
+ );
150
+ }
151
+
152
+ function TransactionForm({ account, sbcAppKit }: { account: any; sbcAppKit: any }) {
153
+ const [recipient, setRecipient] = useState('');
154
+ const [amount, setAmount] = useState('1');
155
+ const [status, setStatus] = useState<'idle'|'loading'|'success'|'error'>('idle');
156
+ const [error, setError] = useState<string | null>(null);
157
+ const [result, setResult] = useState<any>(null);
158
+ const isValid = recipient && /^0x[a-fA-F0-9]{40}$/.test(recipient) && parseFloat(amount) > 0;
159
+
160
+ const sendTx = async () => {
161
+ if (!isValid || !account || !sbcAppKit) return;
162
+ try {
163
+ setStatus('loading'); setError(null);
164
+ const owner = sbcAppKit.getOwnerAddress();
165
+ const value = parseUnits(amount, SBC_DECIMALS(chain));
166
+ const deadline = Math.floor(Date.now() / 1000) + 60 * 30;
167
+ const { publicClient: pc, walletClient: wc } = (sbcAppKit as any);
168
+ const [nonce, tokenName] = await Promise.all([
169
+ pc.readContract({ address: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, abi: erc20PermitAbi, functionName: 'nonces', args: [owner] }),
170
+ pc.readContract({ address: SBC_TOKEN_ADDRESS(chain), abi: erc20Abi, functionName: 'name' })
171
+ ]);
172
+ const domain = { name: tokenName as string, version: '1', chainId: chain.id, verifyingContract: SBC_TOKEN_ADDRESS(chain) };
173
+ const types = { Permit: [
174
+ { name: 'owner', type: 'address' },
175
+ { name: 'spender', type: 'address' },
176
+ { name: 'value', type: 'uint256' },
177
+ { name: 'nonce', type: 'uint256' },
178
+ { name: 'deadline', type: 'uint256' },
179
+ ] } as const;
180
+ const message = { owner, spender: account.address, value, nonce, deadline: BigInt(deadline) } as const;
181
+ const signature = await wc.signTypedData({ domain, types, primaryType: 'Permit', message });
182
+ const { r, s, v } = (await import('viem')).parseSignature(signature);
183
+ const permitData = encodeFunctionData({
184
+ abi: [{ name: 'permit', type: 'function', inputs: [
185
+ { name: 'owner', type: 'address' },
186
+ { name: 'spender', type: 'address' },
187
+ { name: 'value', type: 'uint256' },
188
+ { name: 'deadline', type: 'uint256' },
189
+ { name: 'v', type: 'uint8' },
190
+ { name: 'r', type: 'bytes32' },
191
+ { name: 's', type: 'bytes32' }
192
+ ] }],
193
+ functionName: 'permit',
194
+ args: [owner, account.address, value, BigInt(deadline), v, r, s]
195
+ });
196
+ const transferFromData = encodeFunctionData({
197
+ abi: erc20Abi,
198
+ functionName: 'transferFrom',
199
+ args: [owner, recipient as `0x${string}`, value]
200
+ });
201
+ const res = await sbcAppKit.sendUserOperation({
202
+ calls: [
203
+ { to: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, data: permitData },
204
+ { to: SBC_TOKEN_ADDRESS(chain) as `0x${string}`, data: transferFromData }
205
+ ]
206
+ });
207
+ setResult(res);
208
+ setStatus('success');
209
+ } catch (e: any) {
210
+ setError(e?.message || 'Transaction failed');
211
+ setStatus('error');
212
+ }
213
+ };
214
+
215
+ if (!account) return null;
216
+
217
+ return (
218
+ <div className="p-4 bg-white border border-gray-200 rounded-lg shadow-sm">
219
+ <h3 className="font-semibold text-gray-800 mb-4">💸 Send SBC Tokens</h3>
220
+ <div className="space-y-4">
221
+ <div>
222
+ <label className="block text-sm font-medium text-gray-700 mb-2">Recipient Address</label>
223
+ <input type="text" value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="0x..." className="w-full px-3 py-2 text-xs font-mono border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 border-gray-300" />
224
+ </div>
225
+ <div>
226
+ <label className="block text-sm font-medium text-gray-700 mb-2">Amount (SBC)</label>
227
+ <input type="number" value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="1.0" step="0.000001" min="0" className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 border-gray-300" />
228
+ </div>
229
+ <div className="p-3 bg-gray-50 rounded">
230
+ <div className="flex justify-between text-sm"><span>Amount:</span><span className="font-medium">{amount} SBC</span></div>
231
+ <div className="flex justify-between text-xs text-gray-600"><span>Gas fees:</span><span>Covered by SBC Paymaster ✨</span></div>
232
+ <div className="flex justify-between text-xs text-gray-600"><span>Signing:</span><span>Your Dynamic wallet will prompt to sign 🖊️</span></div>
233
+ </div>
234
+ <button onClick={sendTx} disabled={!isValid || status==='loading' || !account} className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
235
+ {status==='loading' ? 'Waiting for signature...' : `Send ${amount} SBC`}
236
+ </button>
237
+ {status==='success' && result && (
238
+ <div className="p-3 bg-green-50 border border-green-200 rounded">
239
+ <p className="text-sm text-green-800 font-medium">✅ Transaction Submitted</p>
240
+ <p className="text-xs text-green-600 font-mono break-all mt-1">
241
+ <a href={`${chainExplorer(chain)}/tx/${result.transactionHash}`} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">View on BaseScan: {result.transactionHash}</a>
242
+ </p>
243
+ </div>
244
+ )}
245
+ {status==='error' && error && (
246
+ <div className="p-3 bg-red-50 border border-red-200 rounded">
247
+ <p className="text-sm text-red-800 font-medium">❌ Transaction Failed</p>
248
+ <p className="text-xs text-red-600 mt-1">{error}</p>
249
+ </div>
250
+ )}
251
+ </div>
252
+ </div>
253
+ );
254
+ }
255
+
256
+ function DynamicApp() {
257
+ const { primaryWallet } = useDynamicContext();
258
+ const { sbcAppKit, isInitialized, error, account, isLoadingAccount, accountError, refreshAccount } = useSbcDynamic({
259
+ apiKey: import.meta.env.VITE_SBC_API_KEY,
260
+ chain,
261
+ primaryWallet,
262
+ rpcUrl,
263
+ debug: true
264
+ });
265
+
266
+ return (
267
+ <>
268
+ <div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
269
+ <h3 className="font-semibold text-blue-800 mb-2">🔗 Connect to Dynamic</h3>
270
+ <p className="text-sm text-blue-600 mb-3">
271
+ Connect your wallet or sign in with email to create a smart account with Dynamic
272
+ </p>
273
+ <DynamicWidgetWrapper />
274
+ </div>
275
+ {primaryWallet && isInitialized && (
276
+ <>
277
+ <WalletStatus />
278
+ <SmartAccountInfo account={account} refreshAccount={refreshAccount} isLoadingAccount={isLoadingAccount} accountError={accountError} />
279
+ <TransactionForm account={account} sbcAppKit={sbcAppKit} />
280
+ </>
281
+ )}
282
+ {error && <div className="error">{error.message}</div>}
283
+ </>
284
+ );
285
+ }
286
+
287
+ export default function App() {
288
+ return (
289
+ <DynamicContextProvider
290
+ settings={{
291
+ environmentId: import.meta.env.VITE_DYNAMIC_ENVIRONMENT_ID || '',
292
+ walletConnectors: [EthereumWalletConnectors, ZeroDevSmartWalletConnectors],
293
+ }}
294
+ >
295
+ <div className="min-h-screen bg-gray-50 py-8">
296
+ <div className="max-w-2xl mx-auto px-4">
297
+ <div className="text-center mb-8">
298
+ <h1 className="text-3xl font-bold text-gray-900 mb-2 flex items-center justify-center gap-3">
299
+ <img src="/sbc-logo.png" alt="SBC Logo" width={36} height={36} />
300
+ SBC (Dynamic) Integration
301
+ </h1>
302
+ <p className="text-gray-600">Gasless transactions with Dynamic SDK integration</p>
303
+ </div>
304
+
305
+ <DynamicApp />
306
+
307
+ <DynamicUserProfileWrapper />
308
+
309
+ <div className="mt-8 text-center text-xs text-gray-500">
310
+ <p>
311
+ Powered by{' '}
312
+ <a href="https://github.com/stablecoinxyz/app-kit" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">SBC AppKit</a>
313
+ {' '}• Dynamic SDK integration
314
+ </p>
315
+ </div>
316
+ </div>
317
+ </div>
318
+ </DynamicContextProvider>
319
+ );
320
+ }
321
+
322
+
@@ -0,0 +1,14 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_SBC_API_KEY: string
5
+ readonly VITE_DYNAMIC_ENVIRONMENT_ID: string
6
+ readonly VITE_CHAIN?: string
7
+ readonly VITE_RPC_URL?: string
8
+ }
9
+
10
+ interface ImportMeta {
11
+ readonly env: ImportMetaEnv
12
+ }
13
+
14
+