@coinbase/create-cdp-app 0.0.25 → 0.0.27

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 (82) hide show
  1. package/dist/index.js +33 -10
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/template-react-native/App.tsx +359 -0
  5. package/template-react-native/EOATransaction.tsx +360 -0
  6. package/template-react-native/README.md +57 -0
  7. package/template-react-native/SmartAccountTransaction.tsx +304 -0
  8. package/template-react-native/Transaction.tsx +1 -0
  9. package/template-react-native/_gitignore +37 -0
  10. package/template-react-native/android/app/build.gradle +177 -0
  11. package/template-react-native/android/app/debug.keystore +0 -0
  12. package/template-react-native/android/app/proguard-rules.pro +14 -0
  13. package/template-react-native/android/app/src/debug/AndroidManifest.xml +7 -0
  14. package/template-react-native/android/app/src/main/AndroidManifest.xml +25 -0
  15. package/template-react-native/android/app/src/main/java/com/anonymous/reactnativeexpo/MainActivity.kt +61 -0
  16. package/template-react-native/android/app/src/main/java/com/anonymous/reactnativeexpo/MainApplication.kt +57 -0
  17. package/template-react-native/android/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
  18. package/template-react-native/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  19. package/template-react-native/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
  20. package/template-react-native/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
  21. package/template-react-native/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
  22. package/template-react-native/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
  23. package/template-react-native/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
  24. package/template-react-native/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  25. package/template-react-native/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  26. package/template-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  27. package/template-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
  28. package/template-react-native/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  29. package/template-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  30. package/template-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
  31. package/template-react-native/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  32. package/template-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  33. package/template-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
  34. package/template-react-native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  35. package/template-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  36. package/template-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
  37. package/template-react-native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  38. package/template-react-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  39. package/template-react-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
  40. package/template-react-native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  41. package/template-react-native/android/app/src/main/res/values/colors.xml +6 -0
  42. package/template-react-native/android/app/src/main/res/values/strings.xml +5 -0
  43. package/template-react-native/android/app/src/main/res/values/styles.xml +10 -0
  44. package/template-react-native/android/app/src/main/res/values-night/colors.xml +1 -0
  45. package/template-react-native/android/build.gradle +37 -0
  46. package/template-react-native/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  47. package/template-react-native/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  48. package/template-react-native/android/gradle.properties +59 -0
  49. package/template-react-native/android/gradlew +251 -0
  50. package/template-react-native/android/gradlew.bat +94 -0
  51. package/template-react-native/android/settings.gradle +39 -0
  52. package/template-react-native/app.json +34 -0
  53. package/template-react-native/assets/adaptive-icon.png +0 -0
  54. package/template-react-native/assets/favicon.png +0 -0
  55. package/template-react-native/assets/icon.png +0 -0
  56. package/template-react-native/assets/splash-icon.png +0 -0
  57. package/template-react-native/env.example +5 -0
  58. package/template-react-native/index.ts +20 -0
  59. package/template-react-native/ios/.xcode.env +11 -0
  60. package/template-react-native/ios/Podfile +64 -0
  61. package/template-react-native/ios/Podfile.lock +2125 -0
  62. package/template-react-native/ios/Podfile.properties.json +5 -0
  63. package/template-react-native/ios/reactnativeexpo/AppDelegate.swift +70 -0
  64. package/template-react-native/ios/reactnativeexpo/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
  65. package/template-react-native/ios/reactnativeexpo/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
  66. package/template-react-native/ios/reactnativeexpo/Images.xcassets/Contents.json +6 -0
  67. package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
  68. package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/Contents.json +23 -0
  69. package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/image.png +0 -0
  70. package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/image@2x.png +0 -0
  71. package/template-react-native/ios/reactnativeexpo/Images.xcassets/SplashScreenLogo.imageset/image@3x.png +0 -0
  72. package/template-react-native/ios/reactnativeexpo/Info.plist +74 -0
  73. package/template-react-native/ios/reactnativeexpo/PrivacyInfo.xcprivacy +48 -0
  74. package/template-react-native/ios/reactnativeexpo/SplashScreen.storyboard +44 -0
  75. package/template-react-native/ios/reactnativeexpo/Supporting/Expo.plist +12 -0
  76. package/template-react-native/ios/reactnativeexpo/reactnativeexpo-Bridging-Header.h +3 -0
  77. package/template-react-native/ios/reactnativeexpo/reactnativeexpo.entitlements +5 -0
  78. package/template-react-native/ios/reactnativeexpo.xcodeproj/project.pbxproj +545 -0
  79. package/template-react-native/ios/reactnativeexpo.xcodeproj/xcshareddata/xcschemes/reactnativeexpo.xcscheme +88 -0
  80. package/template-react-native/ios/reactnativeexpo.xcworkspace/contents.xcworkspacedata +10 -0
  81. package/template-react-native/package.json +30 -0
  82. package/template-react-native/tsconfig.json +6 -0
@@ -0,0 +1,360 @@
1
+ import { useEvmAddress, useSendEvmTransaction } from "@coinbase/cdp-hooks";
2
+ import * as Clipboard from "expo-clipboard";
3
+ import React, { useCallback, useEffect, useMemo, useState } from "react";
4
+ // eslint-disable-next-line import/namespace
5
+ import { View, Text, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from "react-native";
6
+ import { createPublicClient, http, formatEther } from "viem";
7
+ import { baseSepolia } from "viem/chains";
8
+
9
+ const client = createPublicClient({
10
+ chain: baseSepolia,
11
+ transport: http(),
12
+ });
13
+
14
+ interface Props {
15
+ onSuccess?: () => void;
16
+ }
17
+
18
+ /**
19
+ * This component demonstrates how to send an EVM transaction using EOA (Externally Owned Accounts).
20
+ *
21
+ * @param {Props} props - The props for the EOATransaction component.
22
+ * @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
23
+ * @returns A component that displays a transaction form and result.
24
+ */
25
+ function EOATransaction(props: Props) {
26
+ const { onSuccess } = props;
27
+ const { evmAddress } = useEvmAddress();
28
+ const [balance, setBalance] = useState<bigint | undefined>(undefined);
29
+ const [error, setError] = useState("");
30
+ const [isLoading, setIsLoading] = useState(false);
31
+ const { sendEvmTransaction: sendTransaction, data: transactionData } = useSendEvmTransaction();
32
+
33
+ const formattedBalance = useMemo(() => {
34
+ if (balance === undefined) return undefined;
35
+ return formatEther(balance);
36
+ }, [balance]);
37
+
38
+ const hasBalance = useMemo(() => {
39
+ return formattedBalance && formattedBalance !== "0";
40
+ }, [formattedBalance]);
41
+
42
+ const getBalance = useCallback(async () => {
43
+ if (!evmAddress) return;
44
+ const balance = await client.getBalance({
45
+ address: evmAddress,
46
+ });
47
+ setBalance(balance);
48
+ }, [evmAddress]);
49
+
50
+ useEffect(() => {
51
+ getBalance();
52
+ const interval = setInterval(getBalance, 5000);
53
+ return () => clearInterval(interval);
54
+ }, [getBalance]);
55
+
56
+ const handleSendTransaction = async () => {
57
+ if (!evmAddress || !hasBalance) {
58
+ Alert.alert("Error", "Insufficient balance or no address available");
59
+ return;
60
+ }
61
+
62
+ setIsLoading(true);
63
+ setError("");
64
+
65
+ try {
66
+ await sendTransaction({
67
+ network: "base-sepolia",
68
+ evmAccount: evmAddress,
69
+ transaction: {
70
+ to: evmAddress,
71
+ value: 1000000000000n,
72
+ gas: 21000n,
73
+ chainId: 84532,
74
+ type: "eip1559",
75
+ },
76
+ });
77
+
78
+ Alert.alert("Success", "Transaction sent successfully!");
79
+
80
+ onSuccess?.();
81
+ getBalance();
82
+ } catch (err) {
83
+ const errorMessage = err instanceof Error ? err.message : "Transaction failed";
84
+ setError(errorMessage);
85
+ Alert.alert("Transaction Failed", errorMessage);
86
+ } finally {
87
+ setIsLoading(false);
88
+ }
89
+ };
90
+
91
+ const copyToClipboard = async (text: string, label: string) => {
92
+ try {
93
+ await Clipboard.setStringAsync(text);
94
+ Alert.alert("Copied!", `${label} copied to clipboard`);
95
+ } catch (error) {
96
+ Alert.alert("Error", "Failed to copy to clipboard");
97
+ }
98
+ };
99
+
100
+ return (
101
+ <View style={styles.container}>
102
+ <Text style={styles.title}>EOA Transaction</Text>
103
+
104
+ <View style={styles.infoContainer}>
105
+ <Text style={styles.label}>EVM Address:</Text>
106
+ <TouchableOpacity
107
+ style={styles.copyableInfo}
108
+ onPress={() => copyToClipboard(evmAddress!, "EVM Address")}
109
+ >
110
+ <Text style={styles.copyableText}>{evmAddress || "Not available"}</Text>
111
+ <Text style={styles.addressCopyHint}>Tap to copy</Text>
112
+ </TouchableOpacity>
113
+ </View>
114
+
115
+ <View style={styles.infoContainer}>
116
+ <Text style={styles.label}>Balance:</Text>
117
+ <Text style={styles.value}>
118
+ {formattedBalance === undefined ? "Loading..." : `${formattedBalance} ETH`}
119
+ </Text>
120
+ </View>
121
+
122
+ <TouchableOpacity
123
+ style={[styles.button, (!hasBalance || isLoading) && styles.buttonDisabled]}
124
+ onPress={handleSendTransaction}
125
+ disabled={!hasBalance || isLoading}
126
+ >
127
+ {isLoading ? (
128
+ <ActivityIndicator color="#fff" />
129
+ ) : (
130
+ <Text style={styles.buttonText}>Send Transaction</Text>
131
+ )}
132
+ </TouchableOpacity>
133
+
134
+ {error ? (
135
+ <View style={styles.errorContainer}>
136
+ <Text style={styles.errorText}>{error}</Text>
137
+ </View>
138
+ ) : null}
139
+
140
+ {transactionData.status === "success" ? (
141
+ <View style={styles.successContainer}>
142
+ <Text style={styles.successTitle}>Transaction Sent!</Text>
143
+ <View style={styles.hashContainer}>
144
+ <Text style={styles.hashLabel}>Transaction Hash:</Text>
145
+ <TouchableOpacity
146
+ style={styles.hashButton}
147
+ onPress={() =>
148
+ copyToClipboard(transactionData.receipt.transactionHash, "Transaction Hash")
149
+ }
150
+ >
151
+ <Text style={styles.hashText}>{transactionData.receipt.transactionHash}</Text>
152
+ <Text style={styles.copyHint}>📋 Tap to copy</Text>
153
+ </TouchableOpacity>
154
+ </View>
155
+ </View>
156
+ ) : null}
157
+
158
+ {!hasBalance && (
159
+ <View style={styles.fundingContainer}>
160
+ <Text style={styles.fundingTitle}>Get Testnet Funds</Text>
161
+ <Text style={styles.fundingText}>
162
+ This example transaction sends a tiny amount of ETH from your wallet to itself.
163
+ </Text>
164
+ <Text style={styles.fundingText}>Get some testnet ETH from the Base Sepolia Faucet:</Text>
165
+ <TouchableOpacity
166
+ style={styles.faucetButton}
167
+ onPress={() => {
168
+ Alert.alert(
169
+ "Base Sepolia Faucet",
170
+ `Visit: https://portal.cdp.coinbase.com/products/faucet?token=ETH&address=${evmAddress}`,
171
+ [
172
+ {
173
+ text: "Copy URL",
174
+ onPress: async () => {
175
+ try {
176
+ await Clipboard.setStringAsync(
177
+ `https://portal.cdp.coinbase.com/products/faucet?token=ETH&address=${evmAddress}`,
178
+ );
179
+ Alert.alert("Copied!", "Faucet URL copied to clipboard");
180
+ } catch (error) {
181
+ Alert.alert("Error", "Failed to copy URL");
182
+ }
183
+ },
184
+ },
185
+ { text: "OK", style: "default" },
186
+ ],
187
+ );
188
+ }}
189
+ >
190
+ <Text style={styles.faucetButtonText}>Base Sepolia Faucet</Text>
191
+ </TouchableOpacity>
192
+ </View>
193
+ )}
194
+ </View>
195
+ );
196
+ }
197
+
198
+ const styles = StyleSheet.create({
199
+ container: {
200
+ padding: 32,
201
+ backgroundColor: "#ffffff",
202
+ borderRadius: 16,
203
+ borderWidth: 1,
204
+ borderColor: "#dcdcdc",
205
+ marginHorizontal: 0,
206
+ marginVertical: 8,
207
+ alignItems: "stretch",
208
+ },
209
+ title: {
210
+ fontSize: 20,
211
+ fontWeight: "500",
212
+ marginBottom: 16,
213
+ textAlign: "center",
214
+ color: "#111111",
215
+ },
216
+ infoContainer: {
217
+ marginBottom: 16,
218
+ width: "100%",
219
+ },
220
+ label: {
221
+ fontSize: 14,
222
+ fontWeight: "600",
223
+ color: "#757575",
224
+ marginBottom: 4,
225
+ },
226
+ value: {
227
+ fontSize: 14,
228
+ color: "#111111",
229
+ fontFamily: "monospace",
230
+ textAlign: "left",
231
+ },
232
+ button: {
233
+ backgroundColor: "#008080",
234
+ padding: 15,
235
+ borderRadius: 8,
236
+ alignItems: "center",
237
+ alignSelf: "center",
238
+ marginVertical: 16,
239
+ minWidth: 188,
240
+ paddingHorizontal: 32,
241
+ },
242
+ buttonDisabled: {
243
+ opacity: 0.6,
244
+ },
245
+ buttonText: {
246
+ color: "#ffffff",
247
+ fontSize: 16,
248
+ fontWeight: "600",
249
+ },
250
+ errorContainer: {
251
+ backgroundColor: "#ffebee",
252
+ padding: 12,
253
+ borderRadius: 8,
254
+ borderColor: "#f44336",
255
+ borderWidth: 1,
256
+ width: "100%",
257
+ },
258
+ errorText: {
259
+ color: "#c62828",
260
+ fontSize: 14,
261
+ },
262
+ successContainer: {
263
+ backgroundColor: "rgba(76, 175, 80, 0.1)",
264
+ padding: 12,
265
+ borderRadius: 8,
266
+ borderColor: "rgba(76, 175, 80, 0.3)",
267
+ borderWidth: 1,
268
+ width: "100%",
269
+ },
270
+ successTitle: {
271
+ color: "#4caf50",
272
+ fontSize: 16,
273
+ fontWeight: "600",
274
+ marginBottom: 8,
275
+ },
276
+ hashText: {
277
+ color: "#4caf50",
278
+ fontSize: 12,
279
+ fontFamily: "monospace",
280
+ marginBottom: 4,
281
+ },
282
+ fundingContainer: {
283
+ marginTop: 20,
284
+ padding: 16,
285
+ backgroundColor: "rgba(255, 193, 7, 0.1)",
286
+ borderRadius: 8,
287
+ borderWidth: 1,
288
+ borderColor: "rgba(255, 193, 7, 0.3)",
289
+ width: "100%",
290
+ },
291
+ fundingTitle: {
292
+ fontSize: 16,
293
+ fontWeight: "600",
294
+ color: "#111111",
295
+ marginBottom: 8,
296
+ textAlign: "center",
297
+ },
298
+ fundingText: {
299
+ fontSize: 14,
300
+ color: "#757575",
301
+ marginBottom: 8,
302
+ textAlign: "center",
303
+ lineHeight: 20,
304
+ },
305
+ faucetButton: {
306
+ backgroundColor: "#008080",
307
+ padding: 12,
308
+ borderRadius: 8,
309
+ alignItems: "center",
310
+ marginTop: 8,
311
+ },
312
+ faucetButtonText: {
313
+ color: "#ffffff",
314
+ fontSize: 14,
315
+ fontWeight: "600",
316
+ },
317
+ hashContainer: {
318
+ marginBottom: 12,
319
+ },
320
+ hashLabel: {
321
+ color: "#4caf50",
322
+ fontSize: 14,
323
+ fontWeight: "600",
324
+ marginBottom: 4,
325
+ },
326
+ hashButton: {
327
+ backgroundColor: "rgba(76, 175, 80, 0.1)",
328
+ borderRadius: 8,
329
+ padding: 8,
330
+ borderWidth: 1,
331
+ borderColor: "rgba(76, 175, 80, 0.3)",
332
+ },
333
+ copyHint: {
334
+ color: "#4caf50",
335
+ fontSize: 10,
336
+ fontStyle: "italic",
337
+ textAlign: "center",
338
+ },
339
+ copyableInfo: {
340
+ backgroundColor: "rgba(0, 128, 128, 0.1)",
341
+ borderRadius: 8,
342
+ padding: 12,
343
+ borderWidth: 1,
344
+ borderColor: "rgba(0, 128, 128, 0.3)",
345
+ },
346
+ copyableText: {
347
+ fontSize: 12,
348
+ fontFamily: "monospace",
349
+ color: "#008080",
350
+ textAlign: "center",
351
+ },
352
+ addressCopyHint: {
353
+ fontSize: 10,
354
+ color: "#008080",
355
+ fontStyle: "italic",
356
+ textAlign: "center",
357
+ },
358
+ });
359
+
360
+ export default EOATransaction;
@@ -0,0 +1,57 @@
1
+ # React Native Expo Example
2
+
3
+ A React Native Expo example demonstrating CDP Embedded Wallet SDK integration with React Native hooks.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 20+ and pnpm.
8
+ - For iOS: Xcode and iOS Simulator.
9
+ - For Android: Android Studio and Android emulator.
10
+
11
+ ## Setup
12
+
13
+ ### Setting up Simulators
14
+
15
+ #### iOS Simulator
16
+
17
+ 1. Install [Xcode](https://developer.apple.com/xcode/).
18
+ 2. On initial load, Xcode will ask which platforms you want to install. Select the iOS platform.
19
+ - a. If you missed it on initial load, then open Xcode → Preferences → Components.
20
+ - b. Install the iOS simulator version you want to test
21
+ 3. Expo will automatically start the simulator when you run the app.
22
+
23
+ #### Android Emulator
24
+
25
+ 1. Install Android Studio.
26
+ 2. Open Android Studio → AVD Manager.
27
+ 3. Create a new virtual device:
28
+ - Choose a device definition (e.g., Pixel 7).
29
+ - Select a system image (API level 30+).
30
+ - Configure settings and finish.
31
+ 4. Expo will automatically start the emulator when you run the app.
32
+
33
+ ### Setting up the app
34
+
35
+ 1. Install dependencies:
36
+ ```bash
37
+ pnpm install
38
+ ```
39
+
40
+ 2. Copy environment variables:
41
+ ```bash
42
+ cp .env.example .env
43
+ ```
44
+
45
+ 3. Fill in your CDP API credentials in `.env`.
46
+
47
+ ## Running the App
48
+
49
+ **iOS:**
50
+ ```bash
51
+ pnpm run ios
52
+ ```
53
+
54
+ **Android:**
55
+ ```bash
56
+ pnpm run android
57
+ ```
@@ -0,0 +1,304 @@
1
+ import { useCurrentUser, useSendUserOperation } from "@coinbase/cdp-hooks";
2
+ import * as Clipboard from "expo-clipboard";
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";
6
+ import { baseSepolia } from "viem/chains";
7
+
8
+ const client = createPublicClient({
9
+ chain: baseSepolia,
10
+ transport: http(),
11
+ });
12
+
13
+ interface Props {
14
+ onSuccess?: () => void;
15
+ }
16
+
17
+ /**
18
+ * This component demonstrates how to send a gasless transaction using Smart Accounts.
19
+ *
20
+ * @param {Props} props - The props for the SmartAccountTransaction component.
21
+ * @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
22
+ * @returns A component that displays a Smart Account transaction form and result.
23
+ */
24
+ function SmartAccountTransaction(props: Props) {
25
+ const { onSuccess } = props;
26
+ const { currentUser } = useCurrentUser();
27
+ const { sendUserOperation, data, error, status } = useSendUserOperation();
28
+ const [balance, setBalance] = useState<bigint | undefined>(undefined);
29
+ const [errorMessage, setErrorMessage] = useState("");
30
+
31
+ const smartAccount = currentUser?.evmSmartAccounts?.[0];
32
+
33
+ const formattedBalance = useMemo(() => {
34
+ if (balance === undefined) return undefined;
35
+ return formatEther(balance);
36
+ }, [balance]);
37
+
38
+ const getBalance = useCallback(async () => {
39
+ if (!smartAccount) return;
40
+ const balance = await client.getBalance({
41
+ address: smartAccount,
42
+ });
43
+ setBalance(balance);
44
+ }, [smartAccount]);
45
+
46
+ useEffect(() => {
47
+ getBalance();
48
+ const interval = setInterval(getBalance, 5000);
49
+ return () => clearInterval(interval);
50
+ }, [getBalance]);
51
+
52
+ const handleSendUserOperation = async () => {
53
+ if (!smartAccount) {
54
+ Alert.alert("Error", "No Smart Account available");
55
+ return;
56
+ }
57
+
58
+ setErrorMessage("");
59
+
60
+ try {
61
+ // Example: Send a simple transaction (you can customize this)
62
+ const result = await sendUserOperation({
63
+ evmSmartAccount: smartAccount,
64
+ network: "base-sepolia",
65
+ calls: [
66
+ {
67
+ to: smartAccount,
68
+ value: 0n, // 0 ETH
69
+ },
70
+ ],
71
+ });
72
+
73
+ if (result?.userOperationHash) {
74
+ Alert.alert("Success", "User Operation sent successfully!");
75
+ onSuccess?.();
76
+ getBalance();
77
+ }
78
+ } catch (err) {
79
+ const message = err instanceof Error ? err.message : "Failed to send user operation";
80
+ setErrorMessage(message);
81
+ Alert.alert("Transaction Failed", message);
82
+ }
83
+ };
84
+
85
+ const isLoading = status === "pending";
86
+ const isSuccess = status === "success" && data;
87
+
88
+ const copyToClipboard = async (text: string, label: string) => {
89
+ try {
90
+ await Clipboard.setStringAsync(text);
91
+ Alert.alert("Copied!", `${label} copied to clipboard`);
92
+ } catch (error) {
93
+ Alert.alert("Error", "Failed to copy to clipboard");
94
+ }
95
+ };
96
+
97
+ return (
98
+ <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>
111
+ </View>
112
+
113
+ <View style={styles.infoContainer}>
114
+ <Text style={styles.label}>Balance:</Text>
115
+ <Text style={styles.value}>
116
+ {formattedBalance === undefined ? "Loading..." : `${formattedBalance} ETH`}
117
+ </Text>
118
+ </View>
119
+
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>
129
+ )}
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>
151
+ </View>
152
+
153
+ {data.userOpHash && (
154
+ <View style={styles.hashContainer}>
155
+ <Text style={styles.hashLabel}>User Op Hash:</Text>
156
+ <TouchableOpacity
157
+ style={styles.hashButton}
158
+ onPress={() => copyToClipboard(data.userOpHash!, "User Operation Hash")}
159
+ >
160
+ <Text style={styles.hashText}>{data.userOpHash}</Text>
161
+ <Text style={styles.copyHint}>📋 Tap to copy</Text>
162
+ </TouchableOpacity>
163
+ </View>
164
+ )}
165
+ </View>
166
+ ) : null}
167
+ </View>
168
+ );
169
+ }
170
+
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
+ export default SmartAccountTransaction;