create-solana-mobile-app 1.1.1
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 +109 -0
- package/index.js +184 -0
- package/package.json +23 -0
- package/template/kit-only/src/App.tsx +312 -0
- package/template/kit-only/src/components/WalletButton.tsx +9 -0
- package/template/kit-only/src/hooks/useWallet.ts +8 -0
- package/template/kit-only/src/provider/SolanaProvider.tsx +140 -0
- package/template/package-lock.json +12310 -0
- package/template/package.json +26 -0
- package/template/solana-kit.d.ts +13 -0
- package/template/wallet-ui/src/App.tsx +89 -0
- package/template/wallet-ui/src/components/WalletButton.tsx +60 -0
- package/template/wallet-ui/src/hooks/useWallet.ts +8 -0
- package/template/wallet-ui/src/provider/SolanaProvider.tsx +139 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# create-solana-mobile-app
|
|
2
|
+
|
|
3
|
+
CLI to scaffold Expo + Solana Mobile starter apps with Mobile Wallet Adapter support.
|
|
4
|
+
|
|
5
|
+
## What It Creates
|
|
6
|
+
|
|
7
|
+
This tool generates a new Expo TypeScript app and injects Solana-ready starter code.
|
|
8
|
+
|
|
9
|
+
You can choose one of two variants:
|
|
10
|
+
|
|
11
|
+
- `--kit-only`: Solana Kit provider and logic only.
|
|
12
|
+
- `--wallet-ui`: Solana Kit provider plus a ready-made wallet button UI.
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- Node.js 18+
|
|
17
|
+
- npm
|
|
18
|
+
- Android development environment (for Solana Mobile testing)
|
|
19
|
+
- Expo dev client workflow (`expo run:android`), not Expo Go
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Create a New App
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx create-solana-mobile-app <app-name> --kit-only
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
or
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx create-solana-mobile-app <app-name> --wallet-ui
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
If no variant flag is provided, `--kit-only` is used by default.
|
|
36
|
+
|
|
37
|
+
### Run the App
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd <app-name>
|
|
41
|
+
npx expo run:android
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Environment
|
|
45
|
+
|
|
46
|
+
You can optionally set a custom RPC URL:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
EXPO_PUBLIC_RPC_URL=https://api.devnet.solana.com
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If not set, the starter defaults to Solana devnet.
|
|
53
|
+
|
|
54
|
+
## Generated Project Includes
|
|
55
|
+
|
|
56
|
+
- `@solana/kit`
|
|
57
|
+
- `@solana-mobile/mobile-wallet-adapter-protocol-web3js`
|
|
58
|
+
- `react-native-quick-crypto`
|
|
59
|
+
- `buffer`
|
|
60
|
+
- Metro config updates for crypto/buffer compatibility
|
|
61
|
+
- Buffer polyfill injection in app entry file
|
|
62
|
+
|
|
63
|
+
## Local Development (This Repo)
|
|
64
|
+
|
|
65
|
+
Run the CLI directly from source:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node index.js my-app --wallet-ui
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Publish to npm
|
|
72
|
+
|
|
73
|
+
1. Update `package.json` version.
|
|
74
|
+
2. Login to npm:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm login
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
3. Check package contents:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm pack --dry-run
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
4. Publish:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm publish --access public
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Troubleshooting
|
|
93
|
+
|
|
94
|
+
### "Wallet connected, but no valid base58 account address was returned"
|
|
95
|
+
|
|
96
|
+
Use the latest package version. Recent updates normalize wallet account addresses returned in multiple formats (base58, base64/base64url, and raw bytes).
|
|
97
|
+
|
|
98
|
+
### Android build issues
|
|
99
|
+
|
|
100
|
+
- Confirm Android SDK and emulator/device are configured.
|
|
101
|
+
- Rebuild the app after dependency changes:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx expo run:android
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
ISC
|
package/index.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs-extra");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const appName = args[0];
|
|
8
|
+
const flags = {
|
|
9
|
+
kitOnly: args.includes("--kit-only"),
|
|
10
|
+
walletUI: args.includes("--wallet-ui"),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const TEMPLATE_DIR = "template";
|
|
14
|
+
const DEFAULT_RPC_URL = "https://api.devnet.solana.com";
|
|
15
|
+
|
|
16
|
+
async function loadDeps() {
|
|
17
|
+
const { execa } = await import("execa");
|
|
18
|
+
const { default: chalk } = await import("chalk");
|
|
19
|
+
return { execa, chalk };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeAndroidPackageId(name) {
|
|
23
|
+
const safe = name.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
24
|
+
const suffix = safe.length > 0 ? safe : "app";
|
|
25
|
+
return `com.solanamobile.${suffix}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function ensureAndroidPackage(appPath, appNameArg) {
|
|
29
|
+
const appJsonPath = path.join(appPath, "app.json");
|
|
30
|
+
if (!(await fs.pathExists(appJsonPath))) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const appJson = await fs.readJson(appJsonPath);
|
|
35
|
+
appJson.expo = appJson.expo || {};
|
|
36
|
+
appJson.expo.android = appJson.expo.android || {};
|
|
37
|
+
|
|
38
|
+
if (!appJson.expo.android.package) {
|
|
39
|
+
appJson.expo.android.package = makeAndroidPackageId(appNameArg);
|
|
40
|
+
await fs.writeJson(appJsonPath, appJson, { spaces: 2 });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function injectBufferPolyfill(appPath) {
|
|
45
|
+
const candidates = [
|
|
46
|
+
"App.tsx",
|
|
47
|
+
"App.js",
|
|
48
|
+
path.join("app", "_layout.tsx"),
|
|
49
|
+
path.join("app", "_layout.js"),
|
|
50
|
+
path.join("app", "index.tsx"),
|
|
51
|
+
path.join("app", "index.js"),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
let targetFile = null;
|
|
55
|
+
for (const relativePath of candidates) {
|
|
56
|
+
const filePath = path.join(appPath, relativePath);
|
|
57
|
+
if (await fs.pathExists(filePath)) {
|
|
58
|
+
targetFile = filePath;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!targetFile) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Could not find an app entry file for Buffer polyfill injection.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const content = await fs.readFile(targetFile, "utf-8");
|
|
70
|
+
if (content.includes("global.Buffer = Buffer")) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const withPolyfill = `import { Buffer } from 'buffer';\nglobal.Buffer = Buffer;\n\n${content}`;
|
|
75
|
+
await fs.writeFile(targetFile, withPolyfill);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function createApp() {
|
|
79
|
+
const { execa, chalk } = await loadDeps();
|
|
80
|
+
|
|
81
|
+
if (!appName) {
|
|
82
|
+
console.log(
|
|
83
|
+
"Please provide app name. Usage: create-solana-mobile-app <app-name> [--kit-only|--wallet-ui]",
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (flags.kitOnly && flags.walletUI) {
|
|
89
|
+
console.log(
|
|
90
|
+
chalk.red("Choose only one variant: --kit-only or --wallet-ui."),
|
|
91
|
+
);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const variant = flags.walletUI ? "wallet-ui" : "kit-only";
|
|
96
|
+
const templatePath = path.join(__dirname, TEMPLATE_DIR, variant, "src");
|
|
97
|
+
if (!(await fs.pathExists(templatePath))) {
|
|
98
|
+
throw new Error(`Template variant not found: ${variant}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(chalk.blue(`Creating ${appName} (${variant})...`));
|
|
102
|
+
|
|
103
|
+
await execa(
|
|
104
|
+
"npx",
|
|
105
|
+
[
|
|
106
|
+
"create-expo-app@latest",
|
|
107
|
+
appName,
|
|
108
|
+
"--template",
|
|
109
|
+
"blank-typescript",
|
|
110
|
+
"--yes",
|
|
111
|
+
],
|
|
112
|
+
{ stdio: "inherit" },
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const appPath = path.join(process.cwd(), appName);
|
|
116
|
+
|
|
117
|
+
await ensureAndroidPackage(appPath, appName);
|
|
118
|
+
|
|
119
|
+
console.log(chalk.yellow("Installing Solana mobile dependencies..."));
|
|
120
|
+
await execa(
|
|
121
|
+
"npm",
|
|
122
|
+
[
|
|
123
|
+
"install",
|
|
124
|
+
"@solana/kit",
|
|
125
|
+
"@solana-mobile/mobile-wallet-adapter-protocol-web3js",
|
|
126
|
+
"react-native-quick-crypto",
|
|
127
|
+
"buffer",
|
|
128
|
+
],
|
|
129
|
+
{ cwd: appPath, stdio: "inherit" },
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
console.log(chalk.yellow("Configuring Metro..."));
|
|
133
|
+
const metroConfig = `const { getDefaultConfig } = require('expo/metro-config');
|
|
134
|
+
|
|
135
|
+
const config = getDefaultConfig(__dirname);
|
|
136
|
+
|
|
137
|
+
config.resolver.extraNodeModules = {
|
|
138
|
+
...(config.resolver.extraNodeModules || {}),
|
|
139
|
+
crypto: require.resolve('react-native-quick-crypto'),
|
|
140
|
+
buffer: require.resolve('buffer'),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
module.exports = config;
|
|
144
|
+
`;
|
|
145
|
+
await fs.writeFile(path.join(appPath, "metro.config.js"), metroConfig);
|
|
146
|
+
|
|
147
|
+
await injectBufferPolyfill(appPath);
|
|
148
|
+
|
|
149
|
+
console.log(chalk.yellow("Injecting starter template..."));
|
|
150
|
+
await fs.copy(templatePath, path.join(appPath, "src"), { overwrite: true });
|
|
151
|
+
|
|
152
|
+
const rootApp = `import App from './src/App';
|
|
153
|
+
|
|
154
|
+
export default App;
|
|
155
|
+
`;
|
|
156
|
+
await fs.writeFile(path.join(appPath, "App.tsx"), rootApp);
|
|
157
|
+
|
|
158
|
+
await fs.writeFile(
|
|
159
|
+
path.join(appPath, ".env"),
|
|
160
|
+
`EXPO_PUBLIC_RPC_URL=${DEFAULT_RPC_URL}\n`,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
console.log(chalk.yellow("Installing Expo Dev Client..."));
|
|
164
|
+
await execa("npx", ["expo", "install", "expo-dev-client"], {
|
|
165
|
+
cwd: appPath,
|
|
166
|
+
stdio: "inherit",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
console.log(
|
|
170
|
+
chalk.green(
|
|
171
|
+
`\nSolana mobile starter is ready.\n\nNext steps:\ncd ${appName}\nnpx expo run:android\n`,
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
createApp().catch((error) => {
|
|
177
|
+
console.error("Failed to scaffold Solana mobile app.");
|
|
178
|
+
if (error && error.shortMessage) {
|
|
179
|
+
console.error(error.shortMessage);
|
|
180
|
+
} else {
|
|
181
|
+
console.error(error && error.message ? error.message : error);
|
|
182
|
+
}
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-solana-mobile-app",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-solana-mobile-app": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"type": "commonjs",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"chalk": "^5.6.2",
|
|
18
|
+
"execa": "^9.6.1",
|
|
19
|
+
"expo": "^55.0.8",
|
|
20
|
+
"fs-extra": "^11.3.4",
|
|
21
|
+
"ora": "^9.3.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
Alert,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
View,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import { SolanaProvider } from "./provider/SolanaProvider";
|
|
12
|
+
import { useWallet } from "./hooks/useWallet";
|
|
13
|
+
|
|
14
|
+
const LAMPORTS_PER_SOL = 1_000_000_000;
|
|
15
|
+
|
|
16
|
+
function shortAddress(value: string | null): string {
|
|
17
|
+
if (!value) {
|
|
18
|
+
return "-";
|
|
19
|
+
}
|
|
20
|
+
if (value.length < 12) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
return `${value.slice(0, 6)}...${value.slice(-6)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function PrimaryButton({
|
|
27
|
+
label,
|
|
28
|
+
onPress,
|
|
29
|
+
disabled,
|
|
30
|
+
subtle,
|
|
31
|
+
}: {
|
|
32
|
+
label: string;
|
|
33
|
+
onPress: () => void | Promise<void>;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
subtle?: boolean;
|
|
36
|
+
}) {
|
|
37
|
+
return (
|
|
38
|
+
<Pressable
|
|
39
|
+
onPress={onPress}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
style={({ pressed }) => [
|
|
42
|
+
styles.button,
|
|
43
|
+
subtle ? styles.subtleButton : styles.primaryButton,
|
|
44
|
+
pressed && !disabled ? styles.buttonPressed : null,
|
|
45
|
+
disabled ? styles.buttonDisabled : null,
|
|
46
|
+
]}
|
|
47
|
+
>
|
|
48
|
+
<Text style={[styles.buttonText, subtle ? styles.subtleText : null]}>
|
|
49
|
+
{label}
|
|
50
|
+
</Text>
|
|
51
|
+
</Pressable>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function Home() {
|
|
56
|
+
const { wallet, connectWallet, getBalance } = useWallet();
|
|
57
|
+
const [balance, setBalance] = useState<bigint | null>(null);
|
|
58
|
+
const [busy, setBusy] = useState<false | "connect" | "balance">(false);
|
|
59
|
+
|
|
60
|
+
const balanceSol = useMemo(() => {
|
|
61
|
+
if (balance === null) {
|
|
62
|
+
return "-";
|
|
63
|
+
}
|
|
64
|
+
return (Number(balance) / LAMPORTS_PER_SOL).toFixed(6);
|
|
65
|
+
}, [balance]);
|
|
66
|
+
|
|
67
|
+
const runAction = async (
|
|
68
|
+
op: "connect" | "balance",
|
|
69
|
+
action: () => Promise<void>,
|
|
70
|
+
) => {
|
|
71
|
+
setBusy(op);
|
|
72
|
+
try {
|
|
73
|
+
await action();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const message =
|
|
76
|
+
error instanceof Error ? error.message : "Unexpected wallet error.";
|
|
77
|
+
Alert.alert("Solana Starter", message);
|
|
78
|
+
} finally {
|
|
79
|
+
setBusy(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<ScrollView contentContainerStyle={styles.container}>
|
|
85
|
+
<View style={styles.backdropOrbA} />
|
|
86
|
+
<View style={styles.backdropOrbB} />
|
|
87
|
+
|
|
88
|
+
<View style={styles.heroCard}>
|
|
89
|
+
<Text style={styles.eyebrow}>Solana Mobile Starter</Text>
|
|
90
|
+
<Text style={styles.title}>Kit-first Wallet Starter</Text>
|
|
91
|
+
<Text style={styles.subtitle}>
|
|
92
|
+
Production-lean starter with Mobile Wallet Adapter auth and Solana Kit
|
|
93
|
+
RPC.
|
|
94
|
+
</Text>
|
|
95
|
+
</View>
|
|
96
|
+
|
|
97
|
+
<View style={styles.panel}>
|
|
98
|
+
<Text style={styles.panelTitle}>Wallet</Text>
|
|
99
|
+
<View style={styles.rowBetween}>
|
|
100
|
+
<Text style={styles.label}>Status</Text>
|
|
101
|
+
<Text style={styles.value}>
|
|
102
|
+
{wallet.connected ? "Connected" : "Disconnected"}
|
|
103
|
+
</Text>
|
|
104
|
+
</View>
|
|
105
|
+
<View style={styles.rowBetween}>
|
|
106
|
+
<Text style={styles.label}>Address</Text>
|
|
107
|
+
<Text style={styles.mono}>{shortAddress(wallet.address)}</Text>
|
|
108
|
+
</View>
|
|
109
|
+
<View style={styles.rowBetween}>
|
|
110
|
+
<Text style={styles.label}>Balance</Text>
|
|
111
|
+
<Text style={styles.value}>{balanceSol} SOL</Text>
|
|
112
|
+
</View>
|
|
113
|
+
|
|
114
|
+
<View style={styles.buttonRow}>
|
|
115
|
+
<PrimaryButton
|
|
116
|
+
label={busy === "connect" ? "Connecting..." : "Connect Wallet"}
|
|
117
|
+
onPress={() => runAction("connect", connectWallet)}
|
|
118
|
+
disabled={busy !== false}
|
|
119
|
+
/>
|
|
120
|
+
<PrimaryButton
|
|
121
|
+
label={busy === "balance" ? "Refreshing..." : "Get Balance"}
|
|
122
|
+
onPress={() =>
|
|
123
|
+
runAction("balance", async () => {
|
|
124
|
+
const bal = await getBalance();
|
|
125
|
+
setBalance(bal);
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
disabled={!wallet.connected || busy !== false}
|
|
129
|
+
subtle
|
|
130
|
+
/>
|
|
131
|
+
</View>
|
|
132
|
+
</View>
|
|
133
|
+
|
|
134
|
+
<View style={styles.panelMuted}>
|
|
135
|
+
<Text style={styles.panelMutedTitle}>Dev Build Only</Text>
|
|
136
|
+
<Text style={styles.helper}>
|
|
137
|
+
This app requires a custom dev build because Solana mobile
|
|
138
|
+
dependencies use native modules.
|
|
139
|
+
</Text>
|
|
140
|
+
</View>
|
|
141
|
+
|
|
142
|
+
{busy !== false ? (
|
|
143
|
+
<View style={styles.loadingPill}>
|
|
144
|
+
<ActivityIndicator color="#f6f8fb" />
|
|
145
|
+
</View>
|
|
146
|
+
) : null}
|
|
147
|
+
</ScrollView>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export default function App() {
|
|
152
|
+
return (
|
|
153
|
+
<SolanaProvider>
|
|
154
|
+
<Home />
|
|
155
|
+
</SolanaProvider>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const styles = StyleSheet.create({
|
|
160
|
+
container: {
|
|
161
|
+
minHeight: "100%",
|
|
162
|
+
backgroundColor: "#07111f",
|
|
163
|
+
padding: 18,
|
|
164
|
+
gap: 14,
|
|
165
|
+
},
|
|
166
|
+
backdropOrbA: {
|
|
167
|
+
position: "absolute",
|
|
168
|
+
width: 220,
|
|
169
|
+
height: 220,
|
|
170
|
+
borderRadius: 999,
|
|
171
|
+
right: -70,
|
|
172
|
+
top: -40,
|
|
173
|
+
backgroundColor: "rgba(0, 204, 255, 0.18)",
|
|
174
|
+
},
|
|
175
|
+
backdropOrbB: {
|
|
176
|
+
position: "absolute",
|
|
177
|
+
width: 180,
|
|
178
|
+
height: 180,
|
|
179
|
+
borderRadius: 999,
|
|
180
|
+
left: -60,
|
|
181
|
+
top: 210,
|
|
182
|
+
backgroundColor: "rgba(255, 163, 26, 0.14)",
|
|
183
|
+
},
|
|
184
|
+
heroCard: {
|
|
185
|
+
marginTop: 8,
|
|
186
|
+
backgroundColor: "#0c1b31",
|
|
187
|
+
borderWidth: 1,
|
|
188
|
+
borderColor: "#214167",
|
|
189
|
+
borderRadius: 24,
|
|
190
|
+
padding: 18,
|
|
191
|
+
gap: 8,
|
|
192
|
+
},
|
|
193
|
+
eyebrow: {
|
|
194
|
+
color: "#8ec5ff",
|
|
195
|
+
fontSize: 12,
|
|
196
|
+
letterSpacing: 1.2,
|
|
197
|
+
textTransform: "uppercase",
|
|
198
|
+
fontWeight: "700",
|
|
199
|
+
},
|
|
200
|
+
title: {
|
|
201
|
+
color: "#f6f8fb",
|
|
202
|
+
fontSize: 30,
|
|
203
|
+
lineHeight: 34,
|
|
204
|
+
fontWeight: "800",
|
|
205
|
+
},
|
|
206
|
+
subtitle: {
|
|
207
|
+
color: "#bdd5ee",
|
|
208
|
+
fontSize: 14,
|
|
209
|
+
lineHeight: 21,
|
|
210
|
+
},
|
|
211
|
+
panel: {
|
|
212
|
+
backgroundColor: "#112744",
|
|
213
|
+
borderRadius: 20,
|
|
214
|
+
padding: 16,
|
|
215
|
+
borderWidth: 1,
|
|
216
|
+
borderColor: "#295381",
|
|
217
|
+
gap: 10,
|
|
218
|
+
},
|
|
219
|
+
panelTitle: {
|
|
220
|
+
color: "#f6f8fb",
|
|
221
|
+
fontSize: 18,
|
|
222
|
+
fontWeight: "700",
|
|
223
|
+
},
|
|
224
|
+
panelMuted: {
|
|
225
|
+
backgroundColor: "#0e1c31",
|
|
226
|
+
borderRadius: 18,
|
|
227
|
+
padding: 14,
|
|
228
|
+
borderWidth: 1,
|
|
229
|
+
borderColor: "#273d5a",
|
|
230
|
+
gap: 8,
|
|
231
|
+
},
|
|
232
|
+
panelMutedTitle: {
|
|
233
|
+
color: "#f6f8fb",
|
|
234
|
+
fontSize: 15,
|
|
235
|
+
fontWeight: "700",
|
|
236
|
+
},
|
|
237
|
+
helper: {
|
|
238
|
+
color: "#aac3dd",
|
|
239
|
+
fontSize: 13,
|
|
240
|
+
lineHeight: 19,
|
|
241
|
+
},
|
|
242
|
+
rowBetween: {
|
|
243
|
+
flexDirection: "row",
|
|
244
|
+
justifyContent: "space-between",
|
|
245
|
+
alignItems: "center",
|
|
246
|
+
gap: 10,
|
|
247
|
+
},
|
|
248
|
+
label: {
|
|
249
|
+
color: "#9ec0df",
|
|
250
|
+
fontSize: 12,
|
|
251
|
+
textTransform: "uppercase",
|
|
252
|
+
letterSpacing: 0.8,
|
|
253
|
+
},
|
|
254
|
+
value: {
|
|
255
|
+
color: "#eff6ff",
|
|
256
|
+
fontSize: 14,
|
|
257
|
+
fontWeight: "700",
|
|
258
|
+
},
|
|
259
|
+
mono: {
|
|
260
|
+
color: "#e8f1fb",
|
|
261
|
+
fontFamily: "monospace",
|
|
262
|
+
fontSize: 13,
|
|
263
|
+
maxWidth: "66%",
|
|
264
|
+
},
|
|
265
|
+
buttonRow: {
|
|
266
|
+
flexDirection: "row",
|
|
267
|
+
gap: 10,
|
|
268
|
+
marginTop: 6,
|
|
269
|
+
},
|
|
270
|
+
button: {
|
|
271
|
+
minHeight: 46,
|
|
272
|
+
borderRadius: 14,
|
|
273
|
+
paddingHorizontal: 14,
|
|
274
|
+
flex: 1,
|
|
275
|
+
justifyContent: "center",
|
|
276
|
+
alignItems: "center",
|
|
277
|
+
},
|
|
278
|
+
primaryButton: {
|
|
279
|
+
backgroundColor: "#00a8e8",
|
|
280
|
+
},
|
|
281
|
+
subtleButton: {
|
|
282
|
+
backgroundColor: "#1c3a5c",
|
|
283
|
+
borderWidth: 1,
|
|
284
|
+
borderColor: "#3c628c",
|
|
285
|
+
},
|
|
286
|
+
buttonText: {
|
|
287
|
+
color: "#032038",
|
|
288
|
+
fontWeight: "800",
|
|
289
|
+
fontSize: 13,
|
|
290
|
+
},
|
|
291
|
+
subtleText: {
|
|
292
|
+
color: "#d6e8fa",
|
|
293
|
+
},
|
|
294
|
+
buttonPressed: {
|
|
295
|
+
opacity: 0.9,
|
|
296
|
+
transform: [{ scale: 0.98 }],
|
|
297
|
+
},
|
|
298
|
+
buttonDisabled: {
|
|
299
|
+
opacity: 0.45,
|
|
300
|
+
},
|
|
301
|
+
loadingPill: {
|
|
302
|
+
position: "absolute",
|
|
303
|
+
right: 16,
|
|
304
|
+
bottom: 16,
|
|
305
|
+
paddingVertical: 10,
|
|
306
|
+
paddingHorizontal: 12,
|
|
307
|
+
borderRadius: 999,
|
|
308
|
+
backgroundColor: "rgba(4, 17, 31, 0.95)",
|
|
309
|
+
borderWidth: 1,
|
|
310
|
+
borderColor: "#2c5078",
|
|
311
|
+
},
|
|
312
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button } from "react-native";
|
|
3
|
+
import { useWallet } from "../hooks/useWallet";
|
|
4
|
+
|
|
5
|
+
export default function WalletButton() {
|
|
6
|
+
const { connectWallet } = useWallet();
|
|
7
|
+
|
|
8
|
+
return <Button title="Connect Wallet UI" onPress={connectWallet} />;
|
|
9
|
+
}
|