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 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
+ }
@@ -0,0 +1,8 @@
1
+ import { useContext } from "react";
2
+ import { SolanaContext } from "../provider/SolanaProvider";
3
+
4
+ export const useWallet = () => {
5
+ const ctx = useContext(SolanaContext);
6
+ if (!ctx) throw new Error("Wrap app in SolanaProvider");
7
+ return ctx;
8
+ };