@coinbase/create-cdp-app 0.0.27 → 0.0.29

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.
@@ -1,9 +1,49 @@
1
1
  import { useCurrentUser, useSendUserOperation } from "@coinbase/cdp-hooks";
2
2
  import * as Clipboard from "expo-clipboard";
3
3
  import React, { useCallback, useEffect, useMemo, useState } from "react";
4
- import { View, Text, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from "react-native";
5
- import { createPublicClient, http, formatEther } from "viem";
4
+ import {
5
+ View,
6
+ Text,
7
+ TouchableOpacity,
8
+ StyleSheet,
9
+ Alert,
10
+ ActivityIndicator,
11
+ Linking,
12
+ } from "react-native";
13
+ import {
14
+ createPublicClient,
15
+ http,
16
+ formatEther,
17
+ parseUnits,
18
+ encodeFunctionData,
19
+ formatUnits,
20
+ } from "viem";
6
21
  import { baseSepolia } from "viem/chains";
22
+ import { useTheme } from "./theme/ThemeContext";
23
+
24
+ // USDC contract address on Base Sepolia
25
+ const USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as const;
26
+
27
+ // ERC20 ABI for balance and transfer
28
+ const ERC20_ABI = [
29
+ {
30
+ name: "balanceOf",
31
+ type: "function",
32
+ stateMutability: "view",
33
+ inputs: [{ name: "account", type: "address" }],
34
+ outputs: [{ name: "", type: "uint256" }],
35
+ },
36
+ {
37
+ name: "transfer",
38
+ type: "function",
39
+ stateMutability: "nonpayable",
40
+ inputs: [
41
+ { name: "to", type: "address" },
42
+ { name: "amount", type: "uint256" },
43
+ ],
44
+ outputs: [{ name: "", type: "bool" }],
45
+ },
46
+ ] as const;
7
47
 
8
48
  const client = createPublicClient({
9
49
  chain: baseSepolia,
@@ -26,7 +66,9 @@ function SmartAccountTransaction(props: Props) {
26
66
  const { currentUser } = useCurrentUser();
27
67
  const { sendUserOperation, data, error, status } = useSendUserOperation();
28
68
  const [balance, setBalance] = useState<bigint | undefined>(undefined);
69
+ const [usdcBalance, setUsdcBalance] = useState<bigint | undefined>(undefined);
29
70
  const [errorMessage, setErrorMessage] = useState("");
71
+ const { colors } = useTheme();
30
72
 
31
73
  const smartAccount = currentUser?.evmSmartAccounts?.[0];
32
74
 
@@ -35,12 +77,32 @@ function SmartAccountTransaction(props: Props) {
35
77
  return formatEther(balance);
36
78
  }, [balance]);
37
79
 
80
+ const formattedUsdcBalance = useMemo(() => {
81
+ if (usdcBalance === undefined) return undefined;
82
+ return formatUnits(usdcBalance, 6); // USDC has 6 decimals
83
+ }, [usdcBalance]);
84
+
38
85
  const getBalance = useCallback(async () => {
39
86
  if (!smartAccount) return;
40
- const balance = await client.getBalance({
41
- address: smartAccount,
42
- });
43
- setBalance(balance);
87
+
88
+ try {
89
+ // Get ETH balance
90
+ const ethBalance = await client.getBalance({
91
+ address: smartAccount,
92
+ });
93
+ setBalance(ethBalance);
94
+
95
+ // Get USDC balance
96
+ const usdcBalance = await client.readContract({
97
+ address: USDC_ADDRESS,
98
+ abi: ERC20_ABI,
99
+ functionName: "balanceOf",
100
+ args: [smartAccount],
101
+ });
102
+ setUsdcBalance(usdcBalance as bigint);
103
+ } catch (error) {
104
+ console.error("Error fetching balances:", error);
105
+ }
44
106
  }, [smartAccount]);
45
107
 
46
108
  useEffect(() => {
@@ -51,34 +113,42 @@ function SmartAccountTransaction(props: Props) {
51
113
 
52
114
  const handleSendUserOperation = async () => {
53
115
  if (!smartAccount) {
54
- Alert.alert("Error", "No Smart Account available");
116
+ Alert.alert("Error", "No Smart Account available.");
55
117
  return;
56
118
  }
57
119
 
58
120
  setErrorMessage("");
59
121
 
60
122
  try {
61
- // Example: Send a simple transaction (you can customize this)
123
+ // Send 1 USDC to self as an example
124
+ const usdcAmount = parseUnits("1", 6); // USDC has 6 decimals
125
+
126
+ const transferData = encodeFunctionData({
127
+ abi: ERC20_ABI,
128
+ functionName: "transfer",
129
+ args: [smartAccount, usdcAmount],
130
+ });
131
+
62
132
  const result = await sendUserOperation({
63
133
  evmSmartAccount: smartAccount,
64
134
  network: "base-sepolia",
65
135
  calls: [
66
136
  {
67
- to: smartAccount,
68
- value: 0n, // 0 ETH
137
+ to: USDC_ADDRESS,
138
+ data: transferData,
139
+ value: 0n,
69
140
  },
70
141
  ],
71
142
  });
72
143
 
73
144
  if (result?.userOperationHash) {
74
- Alert.alert("Success", "User Operation sent successfully!");
75
145
  onSuccess?.();
76
146
  getBalance();
77
147
  }
78
148
  } catch (err) {
79
149
  const message = err instanceof Error ? err.message : "Failed to send user operation";
80
150
  setErrorMessage(message);
81
- Alert.alert("Transaction Failed", message);
151
+ Alert.alert("Transaction Failed", message + (message.endsWith(".") ? "" : "."));
82
152
  }
83
153
  };
84
154
 
@@ -88,217 +158,291 @@ function SmartAccountTransaction(props: Props) {
88
158
  const copyToClipboard = async (text: string, label: string) => {
89
159
  try {
90
160
  await Clipboard.setStringAsync(text);
91
- Alert.alert("Copied!", `${label} copied to clipboard`);
161
+ Alert.alert("Copied!", `${label} copied to clipboard.`);
92
162
  } catch (error) {
93
- Alert.alert("Error", "Failed to copy to clipboard");
163
+ Alert.alert("Error", "Failed to copy to clipboard.");
94
164
  }
95
165
  };
96
166
 
167
+ const openFaucet = () => {
168
+ const usdcFaucetUrl = `https://portal.cdp.coinbase.com/products/faucet?address=${smartAccount}&token=USDC`;
169
+
170
+ Alert.alert(
171
+ "Get testnet USDC",
172
+ "",
173
+ [
174
+ {
175
+ text: "Copy USDC Faucet Link",
176
+ onPress: () => copyToClipboard(usdcFaucetUrl, "USDC Faucet Link"),
177
+ },
178
+ {
179
+ text: "Open USDC Faucet",
180
+ onPress: () => Linking.openURL(usdcFaucetUrl),
181
+ style: "default",
182
+ },
183
+ {
184
+ text: "Cancel",
185
+ style: "cancel",
186
+ },
187
+ ],
188
+ { cancelable: true },
189
+ );
190
+ };
191
+
192
+ const hasBalance = balance && balance > 0n; // Still check ETH for gas
193
+ const hasUsdcBalance = usdcBalance && usdcBalance > 0n;
194
+
195
+ const createStyles = () =>
196
+ StyleSheet.create({
197
+ container: {
198
+ flex: 1,
199
+ paddingHorizontal: 20,
200
+ paddingVertical: 20,
201
+ },
202
+ balanceSection: {
203
+ backgroundColor: colors.cardBackground,
204
+ borderRadius: 12,
205
+ padding: 24,
206
+ marginBottom: 16,
207
+ alignItems: "center",
208
+ borderWidth: 1,
209
+ borderColor: colors.border,
210
+ },
211
+ balanceTitle: {
212
+ fontSize: 16,
213
+ fontWeight: "500",
214
+ color: colors.textSecondary,
215
+ marginBottom: 8,
216
+ },
217
+ balanceAmount: {
218
+ fontSize: 32,
219
+ fontWeight: "bold",
220
+ color: colors.text,
221
+ marginBottom: 16,
222
+ },
223
+ faucetButton: {
224
+ backgroundColor: colors.accent,
225
+ paddingHorizontal: 20,
226
+ paddingVertical: 12,
227
+ borderRadius: 8,
228
+ alignItems: "center",
229
+ width: "100%",
230
+ },
231
+ faucetButtonText: {
232
+ color: "#ffffff",
233
+ fontSize: 16,
234
+ fontWeight: "600",
235
+ },
236
+ transactionSection: {
237
+ backgroundColor: colors.cardBackground,
238
+ borderRadius: 12,
239
+ padding: 24,
240
+ borderWidth: 1,
241
+ borderColor: colors.border,
242
+ },
243
+ sectionTitle: {
244
+ fontSize: 18,
245
+ fontWeight: "600",
246
+ color: colors.text,
247
+ marginBottom: 8,
248
+ },
249
+ sectionSubtitle: {
250
+ fontSize: 14,
251
+ color: colors.textSecondary,
252
+ marginBottom: 20,
253
+ },
254
+ sendButton: {
255
+ backgroundColor: colors.accent,
256
+ paddingHorizontal: 20,
257
+ paddingVertical: 12,
258
+ borderRadius: 8,
259
+ alignItems: "center",
260
+ marginBottom: 16,
261
+ },
262
+ sendButtonDisabled: {
263
+ opacity: 0.6,
264
+ },
265
+ sendButtonText: {
266
+ color: "#ffffff",
267
+ fontSize: 16,
268
+ fontWeight: "600",
269
+ },
270
+ disabledText: {
271
+ fontSize: 14,
272
+ color: colors.textSecondary,
273
+ textAlign: "center",
274
+ fontStyle: "italic",
275
+ },
276
+ noteContainer: {
277
+ flexDirection: "row",
278
+ backgroundColor: "rgba(0, 128, 128, 0.1)",
279
+ borderRadius: 8,
280
+ padding: 16,
281
+ marginBottom: 20,
282
+ borderWidth: 1,
283
+ borderColor: "rgba(0, 128, 128, 0.3)",
284
+ },
285
+ noteIcon: {
286
+ fontSize: 16,
287
+ marginRight: 8,
288
+ marginTop: 2,
289
+ },
290
+ noteTextContainer: {
291
+ flex: 1,
292
+ },
293
+ noteTitle: {
294
+ fontSize: 14,
295
+ fontWeight: "600",
296
+ color: colors.text,
297
+ marginRight: 4,
298
+ },
299
+ noteText: {
300
+ fontSize: 14,
301
+ color: colors.text,
302
+ lineHeight: 20,
303
+ },
304
+ faucetLink: {
305
+ color: colors.accent,
306
+ textDecorationLine: "underline",
307
+ },
308
+ errorContainer: {
309
+ backgroundColor: colors.errorBackground,
310
+ padding: 12,
311
+ borderRadius: 8,
312
+ marginTop: 16,
313
+ },
314
+ errorText: {
315
+ color: "#cc0000",
316
+ fontSize: 14,
317
+ },
318
+ successContainer: {
319
+ backgroundColor: colors.successBackground,
320
+ padding: 16,
321
+ borderRadius: 8,
322
+ marginTop: 16,
323
+ },
324
+ successTitle: {
325
+ color: colors.accent,
326
+ fontSize: 16,
327
+ fontWeight: "600",
328
+ marginBottom: 12,
329
+ },
330
+ hashContainer: {
331
+ marginBottom: 12,
332
+ },
333
+ hashLabel: {
334
+ color: colors.accent,
335
+ fontSize: 14,
336
+ fontWeight: "600",
337
+ marginBottom: 6,
338
+ },
339
+ hashButton: {
340
+ backgroundColor: "rgba(0, 128, 128, 0.1)",
341
+ borderRadius: 6,
342
+ padding: 12,
343
+ borderWidth: 1,
344
+ borderColor: "rgba(0, 128, 128, 0.3)",
345
+ },
346
+ hashText: {
347
+ color: colors.accent,
348
+ fontSize: 12,
349
+ fontFamily: "monospace",
350
+ marginBottom: 4,
351
+ },
352
+ copyHint: {
353
+ color: colors.accent,
354
+ fontSize: 10,
355
+ fontStyle: "italic",
356
+ textAlign: "center",
357
+ },
358
+ });
359
+
360
+ const styles = createStyles();
361
+
97
362
  return (
98
363
  <View style={styles.container}>
99
- <Text style={styles.title}>Smart Account Transaction</Text>
100
- <Text style={styles.subtitle}>Gasless transactions with Smart Accounts</Text>
101
-
102
- <View style={styles.infoContainer}>
103
- <Text style={styles.label}>Smart Account Address:</Text>
104
- <TouchableOpacity
105
- style={styles.copyableInfo}
106
- onPress={() => copyToClipboard(smartAccount!, "Smart Account Address")}
107
- >
108
- <Text style={styles.copyableText}>{smartAccount}</Text>
109
- <Text style={styles.addressCopyHint}>Tap to copy</Text>
110
- </TouchableOpacity>
364
+ {/* Balance Section */}
365
+ <View style={styles.balanceSection}>
366
+ <Text style={styles.balanceTitle}>Current Balance</Text>
367
+ <Text style={styles.balanceAmount}>
368
+ {formattedUsdcBalance === undefined ? "Loading..." : `${formattedUsdcBalance} USDC`}
369
+ </Text>
370
+ {!hasUsdcBalance && (
371
+ <TouchableOpacity style={styles.faucetButton} onPress={openFaucet}>
372
+ <Text style={styles.faucetButtonText}>Get funds from faucet</Text>
373
+ </TouchableOpacity>
374
+ )}
111
375
  </View>
112
376
 
113
- <View style={styles.infoContainer}>
114
- <Text style={styles.label}>Balance:</Text>
115
- <Text style={styles.value}>
116
- {formattedBalance === undefined ? "Loading..." : `${formattedBalance} ETH`}
377
+ {/* Transaction Section */}
378
+ <View style={styles.transactionSection}>
379
+ <Text style={styles.sectionTitle}>Transfer 1 USDC</Text>
380
+ <Text style={styles.sectionSubtitle}>
381
+ This example transaction sends 1 USDC from your wallet to itself.
117
382
  </Text>
118
- </View>
119
383
 
120
- <TouchableOpacity
121
- style={[styles.button, (!smartAccount || isLoading) && styles.buttonDisabled]}
122
- onPress={handleSendUserOperation}
123
- disabled={!smartAccount || isLoading}
124
- >
125
- {isLoading ? (
126
- <ActivityIndicator color="#fff" />
127
- ) : (
128
- <Text style={styles.buttonText}>Send User Operation</Text>
384
+ {!hasUsdcBalance && (
385
+ <View style={styles.noteContainer}>
386
+ <Text style={styles.noteIcon}>ℹ️</Text>
387
+ <View style={styles.noteTextContainer}>
388
+ <Text style={styles.noteTitle}>Note:</Text>
389
+ <Text style={styles.noteText}>
390
+ Even though this is a gasless transaction, you still need USDC in your account to
391
+ send it. Get some from the{" "}
392
+ <Text style={styles.faucetLink} onPress={openFaucet}>
393
+ CDP Faucet
394
+ </Text>
395
+ .
396
+ </Text>
397
+ </View>
398
+ </View>
129
399
  )}
130
- </TouchableOpacity>
131
-
132
- {errorMessage || error ? (
133
- <View style={styles.errorContainer}>
134
- <Text style={styles.errorText}>{errorMessage || error?.message}</Text>
135
- </View>
136
- ) : null}
137
-
138
- {isSuccess && data?.transactionHash ? (
139
- <View style={styles.successContainer}>
140
- <Text style={styles.successTitle}>User Operation Sent!</Text>
141
-
142
- <View style={styles.hashContainer}>
143
- <Text style={styles.hashLabel}>Transaction Hash:</Text>
144
- <TouchableOpacity
145
- style={styles.hashButton}
146
- onPress={() => copyToClipboard(data.transactionHash!, "Transaction Hash")}
147
- >
148
- <Text style={styles.hashText}>{data.transactionHash}</Text>
149
- <Text style={styles.copyHint}>📋 Tap to copy</Text>
150
- </TouchableOpacity>
400
+
401
+ <TouchableOpacity
402
+ style={[
403
+ styles.sendButton,
404
+ (!smartAccount || isLoading || !hasUsdcBalance) && styles.sendButtonDisabled,
405
+ ]}
406
+ onPress={handleSendUserOperation}
407
+ disabled={!smartAccount || isLoading || !hasUsdcBalance}
408
+ >
409
+ {isLoading ? (
410
+ <ActivityIndicator color="#fff" />
411
+ ) : (
412
+ <Text style={styles.sendButtonText}>Transfer</Text>
413
+ )}
414
+ </TouchableOpacity>
415
+
416
+ {errorMessage || error ? (
417
+ <View style={styles.errorContainer}>
418
+ <Text style={styles.errorText}>{errorMessage || error?.message}</Text>
151
419
  </View>
420
+ ) : null}
421
+
422
+ {isSuccess && data?.transactionHash ? (
423
+ <View style={styles.successContainer}>
424
+ <Text style={styles.successTitle}>Transfer Complete</Text>
152
425
 
153
- {data.userOpHash && (
154
426
  <View style={styles.hashContainer}>
155
- <Text style={styles.hashLabel}>User Op Hash:</Text>
427
+ <Text style={styles.hashLabel}>Transaction Hash:</Text>
156
428
  <TouchableOpacity
157
429
  style={styles.hashButton}
158
- onPress={() => copyToClipboard(data.userOpHash!, "User Operation Hash")}
430
+ onPress={() =>
431
+ copyToClipboard(
432
+ `https://sepolia.basescan.org/tx/${data.transactionHash}`,
433
+ "Block Explorer Link",
434
+ )
435
+ }
159
436
  >
160
- <Text style={styles.hashText}>{data.userOpHash}</Text>
161
- <Text style={styles.copyHint}>📋 Tap to copy</Text>
437
+ <Text style={styles.hashText}>{data.transactionHash}</Text>
438
+ <Text style={styles.copyHint}>Tap to copy block explorer link</Text>
162
439
  </TouchableOpacity>
163
440
  </View>
164
- )}
165
- </View>
166
- ) : null}
441
+ </View>
442
+ ) : null}
443
+ </View>
167
444
  </View>
168
445
  );
169
446
  }
170
447
 
171
- const styles = StyleSheet.create({
172
- container: {
173
- padding: 32,
174
- backgroundColor: "#ffffff",
175
- borderRadius: 16,
176
- borderWidth: 1,
177
- borderColor: "#dcdcdc",
178
- marginHorizontal: 0,
179
- marginVertical: 8,
180
- alignItems: "stretch",
181
- },
182
- title: {
183
- fontSize: 20,
184
- fontWeight: "500",
185
- marginBottom: 4,
186
- textAlign: "center",
187
- color: "#111111",
188
- },
189
- subtitle: {
190
- fontSize: 14,
191
- color: "#757575",
192
- textAlign: "center",
193
- marginBottom: 16,
194
- },
195
- infoContainer: {
196
- marginBottom: 16,
197
- width: "100%",
198
- },
199
- label: {
200
- fontSize: 14,
201
- fontWeight: "600",
202
- color: "#757575",
203
- marginBottom: 4,
204
- },
205
- value: {
206
- fontSize: 14,
207
- color: "#111111",
208
- fontFamily: "monospace",
209
- textAlign: "left",
210
- },
211
- button: {
212
- backgroundColor: "#4CAF50",
213
- padding: 15,
214
- borderRadius: 8,
215
- alignItems: "center",
216
- alignSelf: "center",
217
- marginVertical: 16,
218
- minWidth: 188,
219
- paddingHorizontal: 32,
220
- },
221
- buttonDisabled: {
222
- opacity: 0.6,
223
- },
224
- buttonText: {
225
- color: "#ffffff",
226
- fontSize: 16,
227
- fontWeight: "600",
228
- },
229
- errorContainer: {
230
- backgroundColor: "#ffebee",
231
- padding: 12,
232
- borderRadius: 8,
233
- borderColor: "#f44336",
234
- borderWidth: 1,
235
- width: "100%",
236
- },
237
- errorText: {
238
- color: "#c62828",
239
- fontSize: 14,
240
- },
241
- successContainer: {
242
- backgroundColor: "rgba(76, 175, 80, 0.1)",
243
- padding: 12,
244
- borderRadius: 8,
245
- borderColor: "rgba(76, 175, 80, 0.3)",
246
- borderWidth: 1,
247
- width: "100%",
248
- },
249
- successTitle: {
250
- color: "#4caf50",
251
- fontSize: 16,
252
- fontWeight: "600",
253
- marginBottom: 8,
254
- },
255
- hashContainer: {
256
- marginBottom: 12,
257
- },
258
- hashLabel: {
259
- color: "#4caf50",
260
- fontSize: 14,
261
- fontWeight: "600",
262
- marginBottom: 4,
263
- },
264
- hashButton: {
265
- backgroundColor: "rgba(76, 175, 80, 0.1)",
266
- borderRadius: 8,
267
- padding: 8,
268
- borderWidth: 1,
269
- borderColor: "rgba(76, 175, 80, 0.3)",
270
- },
271
- hashText: {
272
- color: "#4caf50",
273
- fontSize: 12,
274
- fontFamily: "monospace",
275
- marginBottom: 4,
276
- },
277
- copyHint: {
278
- color: "#4caf50",
279
- fontSize: 10,
280
- fontStyle: "italic",
281
- textAlign: "center",
282
- },
283
- copyableInfo: {
284
- backgroundColor: "rgba(0, 128, 128, 0.1)",
285
- borderRadius: 8,
286
- padding: 12,
287
- borderWidth: 1,
288
- borderColor: "rgba(0, 128, 128, 0.3)",
289
- },
290
- copyableText: {
291
- fontSize: 12,
292
- fontFamily: "monospace",
293
- color: "#008080",
294
- textAlign: "center",
295
- },
296
- addressCopyHint: {
297
- fontSize: 10,
298
- color: "#008080",
299
- fontStyle: "italic",
300
- textAlign: "center",
301
- },
302
- });
303
-
304
448
  export default SmartAccountTransaction;
@@ -0,0 +1,49 @@
1
+ import React from "react";
2
+ import { TouchableOpacity, Text, StyleSheet, View } from "react-native";
3
+ import { useTheme } from "../theme/ThemeContext";
4
+ import { DarkModeToggleProps } from "../types";
5
+ import { SunMoonIcon } from "./SunMoonIcon";
6
+
7
+ export const DarkModeToggle: React.FC<DarkModeToggleProps> = ({
8
+ style,
9
+ iconStyle,
10
+ showText = false,
11
+ }) => {
12
+ const { isDarkMode, toggleDarkMode, colors } = useTheme();
13
+
14
+ const createStyles = () =>
15
+ StyleSheet.create({
16
+ button: {
17
+ backgroundColor: colors.inputBackground,
18
+ borderWidth: 1,
19
+ borderColor: colors.border,
20
+ borderRadius: showText ? 8 : 20,
21
+ padding: showText ? 15 : 10,
22
+ alignItems: "center",
23
+ flexDirection: showText ? "row" : "column",
24
+ justifyContent: "center",
25
+ ...style,
26
+ },
27
+ iconContainer: {
28
+ marginRight: showText ? 8 : 0,
29
+ alignItems: "center",
30
+ justifyContent: "center",
31
+ },
32
+ text: {
33
+ color: colors.text,
34
+ fontSize: 16,
35
+ fontWeight: "500",
36
+ },
37
+ });
38
+
39
+ const styles = createStyles();
40
+
41
+ return (
42
+ <TouchableOpacity style={styles.button} onPress={toggleDarkMode}>
43
+ <View style={styles.iconContainer}>
44
+ <SunMoonIcon isDarkMode={isDarkMode} size={showText ? 18 : 16} color={colors.text} />
45
+ </View>
46
+ {showText && <Text style={styles.text}>{isDarkMode ? "Light Mode" : "Dark Mode"}</Text>}
47
+ </TouchableOpacity>
48
+ );
49
+ };