Skip to content

Commit f35f4ca

Browse files
committed
Segundo workshop: Transacciones y seguridad
1 parent f182a61 commit f35f4ca

File tree

7 files changed

+440
-33
lines changed

7 files changed

+440
-33
lines changed

src/components/AccountManager.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ import { saveAccountToStorage } from "../utils/local-storage";
44
import { stellarService } from "../services/stellar.service";
55
import AccountCard from "./AccountCard";
66
import { AccountBalance, IAccount } from "../interfaces/account";
7+
import PaymentModal from "./PaymentModal";
8+
import StellarExpertLink from "./StellarExpertLink";
9+
import useModal from "../hooks/useModal";
710

811
export default function AccountManager() {
9-
const { getAccount } = useStellarAccounts();
12+
const { getAccount, hashId } = useStellarAccounts();
13+
const paymentModal = useModal();
1014
const [, forceUpdate] = useState({});
1115

1216
const bobAccount = getAccount("bob");
@@ -41,6 +45,38 @@ export default function AccountManager() {
4145
forceUpdate({});
4246
};
4347

48+
const refreshAccountBalances = async () => {
49+
if (bobAccount) {
50+
const balancesData = await stellarService.getAccountBalance(
51+
bobAccount.publicKey,
52+
);
53+
const updatedBob: IAccount = {
54+
...bobAccount,
55+
balances: balancesData.map((balance: AccountBalance) => ({
56+
amount: balance.amount,
57+
assetCode: balance.assetCode,
58+
})),
59+
};
60+
saveAccountToStorage("bob", updatedBob);
61+
}
62+
63+
if (aliceAccount) {
64+
const balancesData = await stellarService.getAccountBalance(
65+
aliceAccount.publicKey,
66+
);
67+
const updatedAlice: IAccount = {
68+
...aliceAccount,
69+
balances: balancesData.map((balance: AccountBalance) => ({
70+
amount: balance.amount,
71+
assetCode: balance.assetCode,
72+
})),
73+
};
74+
saveAccountToStorage("alice", updatedAlice);
75+
}
76+
77+
forceUpdate({});
78+
};
79+
4480
return (
4581
<div className="max-w-screen">
4682
<div className="container mx-auto max-w-7xl">
@@ -73,6 +109,13 @@ export default function AccountManager() {
73109
Create Account for Alice
74110
</span>
75111
</button>
112+
113+
<button
114+
onClick={paymentModal.openModal}
115+
className="group px-6 py-3 bg-purple-600 text-white font-semibold rounded-xl shadow-lg hover:bg-purple-700 hover:shadow-xl disabled:bg-slate-300 disabled:cursor-not-allowed transition-all duration-200 transform hover:scale-105 disabled:transform-none cursor-pointer"
116+
>
117+
<span className="flex items-center gap-2">Send Payment</span>
118+
</button>
76119
</div>
77120

78121
<div className="grid lg:grid-cols-2 gap-8">
@@ -125,6 +168,14 @@ export default function AccountManager() {
125168
</div>
126169
)}
127170
</div>
171+
{paymentModal.showModal && (
172+
<PaymentModal
173+
closeModal={paymentModal.closeModal}
174+
getAccount={getAccount}
175+
onPaymentSuccess={refreshAccountBalances}
176+
/>
177+
)}
178+
{hashId && <StellarExpertLink url={hashId} />}
128179
</div>
129180
);
130181
}

src/components/Modal.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Icon } from "@stellar/design-system";
2+
3+
interface IModal {
4+
title: string;
5+
closeModal: () => void;
6+
children: React.ReactNode;
7+
}
8+
9+
function Modal({ title, closeModal, children }: IModal) {
10+
return (
11+
<div
12+
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 shadow-lg"
13+
data-test="modal-container"
14+
onClick={closeModal}
15+
>
16+
<div
17+
className="relative w-11/12 max-w-lg rounded-lg bg-white shadow"
18+
data-test="modal-outside-container"
19+
onClick={(e) => e.stopPropagation()}
20+
>
21+
<div className="flex items-center justify-between rounded-t p-4 md:p-5">
22+
<h3
23+
className="text-lg font-semibold text-gray-900"
24+
data-test="modal-title"
25+
>
26+
{title}
27+
</h3>
28+
<button
29+
type="button"
30+
className="end-2.5 ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-transparent text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 cursor-pointer"
31+
data-test="modal-btn-close"
32+
onClick={closeModal}
33+
>
34+
<Icon.XClose className="h-5 w-5" />
35+
<span className="sr-only">Close modal</span>
36+
</button>
37+
</div>
38+
{children}
39+
</div>
40+
</div>
41+
);
42+
}
43+
44+
export default Modal;

src/components/PaymentModal.tsx

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { useState } from "react";
2+
import { IAccount } from "../interfaces/account";
3+
import { useStellarAccounts } from "../providers/StellarAccountProvider";
4+
import { stellarService } from "../services/stellar.service";
5+
import Modal from "./Modal";
6+
7+
interface PaymentModalProps {
8+
closeModal: () => void;
9+
getAccount: (name: string) => IAccount | null;
10+
onPaymentSuccess: () => Promise<void>;
11+
}
12+
13+
export interface PaymentFormData {
14+
sourceAccount: string;
15+
destinationAccount: string;
16+
amount: string;
17+
}
18+
19+
function PaymentModal({
20+
closeModal,
21+
getAccount,
22+
onPaymentSuccess,
23+
}: PaymentModalProps) {
24+
const [sourceAccount, setSourceAccount] = useState<IAccount | null>(null);
25+
const [destinationAccount, setDestinationAccount] = useState<IAccount | null>(
26+
null,
27+
);
28+
const [amount, setAmount] = useState<string>("");
29+
const [isSubmitting, setIsSubmitting] = useState(false);
30+
const { setHashId } = useStellarAccounts();
31+
32+
const accountNames = ["bob", "alice"];
33+
const availableAccounts: IAccount[] = accountNames
34+
.map((name) => getAccount(name))
35+
.filter((account): account is IAccount => account !== null);
36+
37+
const getAccountName = (account: IAccount): string => {
38+
const name = accountNames.find(
39+
(name) => getAccount(name)?.publicKey === account.publicKey,
40+
);
41+
return name ? name.charAt(0).toUpperCase() + name.slice(1) : "Unknown";
42+
};
43+
44+
const handleSubmit = async () => {
45+
if (!sourceAccount || !destinationAccount || !amount) {
46+
alert("Please fill all fields");
47+
return;
48+
}
49+
50+
if (sourceAccount === destinationAccount) {
51+
alert("Source and destination accounts must be different");
52+
return;
53+
}
54+
55+
setIsSubmitting(true);
56+
try {
57+
const response = await stellarService.payment(
58+
sourceAccount.publicKey,
59+
sourceAccount.secretKey,
60+
destinationAccount.publicKey,
61+
amount,
62+
);
63+
64+
console.log("Payment successful:", response);
65+
setHashId(response.hash);
66+
67+
if (onPaymentSuccess) {
68+
await onPaymentSuccess();
69+
}
70+
71+
setSourceAccount(null);
72+
setDestinationAccount(null);
73+
setAmount("");
74+
closeModal();
75+
} catch (error) {
76+
console.error("Payment failed:", error);
77+
alert("Payment failed. Please try again.");
78+
} finally {
79+
setIsSubmitting(false);
80+
}
81+
};
82+
83+
return (
84+
<Modal title="Send Payment" closeModal={closeModal}>
85+
<div className="p-4 md:p-5 space-y-5">
86+
<div>
87+
<label className="block text-sm font-semibold text-slate-700 mb-2">
88+
Source Account
89+
</label>
90+
<select
91+
value={sourceAccount?.publicKey || ""}
92+
onChange={(e) => {
93+
const selected = availableAccounts.find(
94+
(acc) => acc.publicKey === e.target.value,
95+
);
96+
setSourceAccount(selected || null);
97+
}}
98+
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg focus:border-blue-500 focus:outline-none transition-colors bg-white text-slate-900 font-medium"
99+
>
100+
<option value="">Select source account</option>
101+
{availableAccounts.map((account) => (
102+
<option key={account.publicKey} value={account.publicKey}>
103+
{getAccountName(account)}
104+
</option>
105+
))}
106+
</select>
107+
</div>
108+
109+
<div>
110+
<label className="block text-sm font-semibold text-slate-700 mb-2">
111+
Amount (XLM)
112+
</label>
113+
<input
114+
type="text"
115+
value={amount}
116+
onChange={(e) => setAmount(e.target.value)}
117+
placeholder="0.00"
118+
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg focus:border-blue-500 focus:outline-none transition-colors font-mono text-lg"
119+
/>
120+
</div>
121+
122+
<div>
123+
<label className="block text-sm font-semibold text-slate-700 mb-2">
124+
Destination Account
125+
</label>
126+
<select
127+
value={destinationAccount?.publicKey || ""}
128+
onChange={(e) => {
129+
const selected = availableAccounts.find(
130+
(acc) => acc.publicKey === e.target.value,
131+
);
132+
setDestinationAccount(selected || null);
133+
}}
134+
className="w-full px-4 py-3 border-2 border-slate-200 rounded-lg focus:border-blue-500 focus:outline-none transition-colors bg-white text-slate-900 font-medium"
135+
>
136+
<option value="">Select destination account</option>
137+
{availableAccounts.map((account) => (
138+
<option key={account.publicKey} value={account.publicKey}>
139+
{getAccountName(account)}
140+
</option>
141+
))}
142+
</select>
143+
</div>
144+
145+
<div className="flex gap-3 pt-4">
146+
<button
147+
type="button"
148+
onClick={closeModal}
149+
className="flex-1 px-4 py-3 border-2 border-slate-300 text-slate-700 font-semibold rounded-lg hover:bg-slate-50 transition-colors cursor-pointer"
150+
disabled={isSubmitting}
151+
>
152+
Cancel
153+
</button>
154+
<button
155+
type="button"
156+
onClick={() => void handleSubmit()}
157+
disabled={isSubmitting}
158+
className="flex-1 px-4 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 disabled:bg-blue-300 disabled:cursor-not-allowed transition-colors cursor-pointer"
159+
>
160+
{isSubmitting ? "Sending..." : "Send Payment"}
161+
</button>
162+
</div>
163+
</div>
164+
</Modal>
165+
);
166+
}
167+
168+
export default PaymentModal;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
function StellarExpertLink({ url }: { url: string }) {
2+
return (
3+
<div className="flex justify-center mt-8">
4+
<a
5+
href={`https://stellar.expert/explorer/testnet/tx/${url}`}
6+
target="_blank"
7+
rel="noopener noreferrer"
8+
className="text-blue-600 underline"
9+
>
10+
View on explorer
11+
</a>
12+
</div>
13+
);
14+
}
15+
16+
export default StellarExpertLink;

src/hooks/useModal.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useState } from "react";
2+
3+
function useModal() {
4+
const [showModal, setShowModal] = useState<boolean>(false);
5+
6+
const openModal = () => {
7+
setShowModal(true);
8+
};
9+
10+
const closeModal = () => {
11+
setShowModal(false);
12+
};
13+
14+
return { showModal, openModal, closeModal };
15+
}
16+
17+
export default useModal;

src/providers/StellarAccountProvider.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { IAccount } from "../interfaces/account";
21
import { createContext, use, useCallback, useState } from "react";
2+
import { IAccount } from "../interfaces/account";
3+
import React from "react";
34
import {
45
getAccountFromStorage,
56
getCurrentAccountFromStorage,
6-
saveCurrentAccount,
77
} from "../utils/local-storage";
88

99
interface StellarContextType {
1010
currentAccount: string;
11+
hashId: string;
12+
setHashId: React.Dispatch<React.SetStateAction<string>>;
1113
setCurrentAccount: (name: string) => void;
1214
getAccount: (name: string) => IAccount | null;
1315
getCurrentAccountData: () => IAccount | null;
@@ -17,6 +19,7 @@ const StellarAccountContext = createContext<StellarContextType | undefined>(
1719
undefined,
1820
);
1921

22+
// eslint-disable-next-line react-refresh/only-export-components
2023
export const useStellarAccounts = () => {
2124
const context = use(StellarAccountContext);
2225
if (context === undefined) {
@@ -36,7 +39,7 @@ export const StellarAccountProvider: React.FC<{
3639

3740
const setCurrentAccount = useCallback((name: string) => {
3841
setCurrentAccountState(name);
39-
saveCurrentAccount(name);
42+
setCurrentAccount(name);
4043
}, []);
4144

4245
const getAccount = useCallback((name: string): IAccount | null => {
@@ -48,14 +51,18 @@ export const StellarAccountProvider: React.FC<{
4851
return getAccountFromStorage(currentAccount);
4952
}, [currentAccount]);
5053

54+
const [hashId, setHashId] = useState<string>("");
55+
5156
const value: StellarContextType = {
5257
currentAccount,
58+
hashId,
59+
setHashId,
5360
setCurrentAccount,
5461
getAccount,
5562
getCurrentAccountData,
5663
};
5764

5865
return (
59-
<StellarAccountContext value={value}>{children}</StellarAccountContext>
66+
<StellarAccountContext value={value}> {children} </StellarAccountContext>
6067
);
6168
};

0 commit comments

Comments
 (0)