diff --git a/frontend/.nvmrc b/frontend/.nvmrc
new file mode 100644
index 0000000..209e3ef
--- /dev/null
+++ b/frontend/.nvmrc
@@ -0,0 +1 @@
+20
diff --git a/frontend/README.md b/frontend/README.md
index 3dd0d1a..fb409a4 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -1 +1,17 @@
-# Final Frontend Polishing
+# S-pay Frontend
+
+Next.js frontend for S-pay protocol.
+
+## Setup
+
+```bash
+npm install
+npm run dev
+```
+
+## Features
+
+- Connect wallet (Leather, Xverse)
+- Register user / merchant
+- Process payments, vault deposit
+- Read-only contract queries
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index 01bed6c..7a46f5e 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -2,10 +2,13 @@ import type { Metadata } from "next";
import "./globals.css";
import { StacksProvider } from "@/context/StacksContext";
import Navbar from "@/components/Navbar/Navbar";
+import { Footer } from "@/components/Footer/Footer";
export const metadata: Metadata = {
title: "S-pay | Premium Stacks Payments",
description: "Experience the future of decentralized payments on Stacks.",
+ keywords: ["Stacks", "Bitcoin", "payments", "crypto", "decentralized"],
+ openGraph: { title: "S-pay | Premium Stacks Payments", description: "Fast, decentralized payments on Stacks." },
};
export default function RootLayout({
@@ -20,10 +23,11 @@ export default function RootLayout({
-
+
- {children}
+ {children}
+
diff --git a/frontend/app/merchant/register/page.tsx b/frontend/app/merchant/register/page.tsx
index ea3b917..ea6e31d 100644
--- a/frontend/app/merchant/register/page.tsx
+++ b/frontend/app/merchant/register/page.tsx
@@ -8,7 +8,7 @@ export default function MerchantRegisterPage() {
Register as Merchant
- Requires 50 STX verification stake (register-merchant)
+ Requires 50 STX verification stake. Refundable after verification.
diff --git a/frontend/app/pay/page.tsx b/frontend/app/pay/page.tsx
index 84c87da..fc45cf8 100644
--- a/frontend/app/pay/page.tsx
+++ b/frontend/app/pay/page.tsx
@@ -1,6 +1,7 @@
"use client";
import { PaymentForm } from "@/components/PaymentForm/PaymentForm";
+import { SPAY_FULL_CONTRACT } from "@/lib/constants";
import styles from "./pay.module.css";
export default function PayPage() {
@@ -10,6 +11,7 @@ export default function PayPage() {
Send STX via S-pay protocol (process-payment)
+ Contract: {SPAY_FULL_CONTRACT}
);
diff --git a/frontend/app/pay/pay.module.css b/frontend/app/pay/pay.module.css
index 180a9df..21b2c41 100644
--- a/frontend/app/pay/pay.module.css
+++ b/frontend/app/pay/pay.module.css
@@ -14,5 +14,12 @@
.subtitle {
color: #a0a0aa;
+ margin-bottom: 0.5rem;
+}
+
+.contract {
+ font-size: 0.8rem;
+ font-family: monospace;
+ color: #6b6b7b;
margin-bottom: 2rem;
}
diff --git a/frontend/app/register/page.tsx b/frontend/app/register/page.tsx
index 977b569..289488d 100644
--- a/frontend/app/register/page.tsx
+++ b/frontend/app/register/page.tsx
@@ -1,15 +1,24 @@
"use client";
+import Link from "next/link";
import { RegisterUserForm } from "@/components/RegisterUserForm/RegisterUserForm";
+import { useStacks } from "@/context/StacksContext";
import styles from "./register.module.css";
export default function RegisterPage() {
+ const { address } = useStacks();
+
return (
Register as User
Pick a unique username to join S-pay (string-ascii 24 max)
+ {!address && (
+
+ Connect your wallet to get started.
+
+ )}
);
diff --git a/frontend/app/register/register.module.css b/frontend/app/register/register.module.css
index 180a9df..5924417 100644
--- a/frontend/app/register/register.module.css
+++ b/frontend/app/register/register.module.css
@@ -14,5 +14,15 @@
.subtitle {
color: #a0a0aa;
+ margin-bottom: 1rem;
+}
+
+.cta {
+ color: var(--primary);
margin-bottom: 2rem;
}
+
+.cta a {
+ color: inherit;
+ text-decoration: underline;
+}
diff --git a/frontend/app/vault/page.tsx b/frontend/app/vault/page.tsx
index 5b9a8c8..57a66cb 100644
--- a/frontend/app/vault/page.tsx
+++ b/frontend/app/vault/page.tsx
@@ -8,7 +8,7 @@ export default function VaultPage() {
Vault Deposit
- Deposit STX to your S-pay vault (vault-deposit)
+ Deposit STX to your S-pay vault. Minimum 0.000001 STX.
diff --git a/frontend/components/Button/Button.tsx b/frontend/components/Button/Button.tsx
index 58b49b1..4791873 100644
--- a/frontend/components/Button/Button.tsx
+++ b/frontend/components/Button/Button.tsx
@@ -23,18 +23,3 @@ export default function Button({
);
}
-// Feature extension 36
-// Feature extension 37
-// Feature extension 38
-// Feature extension 39
-// Feature extension 40
-// Feature extension 41
-// Feature extension 42
-// Feature extension 43
-// Feature extension 44
-// Feature extension 45
-// Feature extension 46
-// Feature extension 47
-// Feature extension 48
-// Feature extension 49
-// Feature extension 50
diff --git a/frontend/components/ConnectWallet/ConnectWallet.test.tsx b/frontend/components/ConnectWallet/ConnectWallet.test.tsx
new file mode 100644
index 0000000..792d4b6
--- /dev/null
+++ b/frontend/components/ConnectWallet/ConnectWallet.test.tsx
@@ -0,0 +1,6 @@
+/**
+ * ConnectWallet component tests placeholder.
+ * Add tests with React Testing Library.
+ */
+
+export const placeholder = true;
diff --git a/frontend/components/ConnectWallet/ConnectWallet.tsx b/frontend/components/ConnectWallet/ConnectWallet.tsx
index 4b64896..ab66044 100644
--- a/frontend/components/ConnectWallet/ConnectWallet.tsx
+++ b/frontend/components/ConnectWallet/ConnectWallet.tsx
@@ -1,24 +1,45 @@
"use client";
+import { useState } from "react";
import { useStacks } from "@/context/StacksContext";
+import { shortenAddress } from "@/lib/utils";
import styles from "./ConnectWallet.module.css";
export function ConnectWallet() {
const { address, handleConnect, handleDisconnect } = useStacks();
+ const [connecting, setConnecting] = useState(false);
+
+ const onConnect = async () => {
+ setConnecting(true);
+ try {
+ await handleConnect();
+ } finally {
+ setConnecting(false);
+ }
+ };
return (
{address ? (
<>
-
{address.slice(0, 6)}…{address.slice(-4)}
-
+
{addr ? (
<>
-
{addr.slice(0, 6)}…{addr.slice(-4)}
+
copy(addr)}
+ title={addr}
+ >
+ {shortenAddress(addr)}{copied ? " ✓" : ""}
+
Disconnect
>
) : (
diff --git a/frontend/components/NetworkBadge/NetworkBadge.module.css b/frontend/components/NetworkBadge/NetworkBadge.module.css
new file mode 100644
index 0000000..2781a55
--- /dev/null
+++ b/frontend/components/NetworkBadge/NetworkBadge.module.css
@@ -0,0 +1,9 @@
+.badge {
+ font-size: 0.7rem;
+ font-weight: 600;
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
+ background: rgba(34, 197, 94, 0.2);
+ color: #22c55e;
+ border: 1px solid rgba(34, 197, 94, 0.3);
+}
diff --git a/frontend/components/NetworkBadge/NetworkBadge.tsx b/frontend/components/NetworkBadge/NetworkBadge.tsx
new file mode 100644
index 0000000..3856c30
--- /dev/null
+++ b/frontend/components/NetworkBadge/NetworkBadge.tsx
@@ -0,0 +1,11 @@
+"use client";
+
+import styles from "./NetworkBadge.module.css";
+
+export function NetworkBadge() {
+ return (
+
+ Mainnet
+
+ );
+}
diff --git a/frontend/components/PaymentForm/PaymentForm.module.css b/frontend/components/PaymentForm/PaymentForm.module.css
index 4716d90..3c4f16d 100644
--- a/frontend/components/PaymentForm/PaymentForm.module.css
+++ b/frontend/components/PaymentForm/PaymentForm.module.css
@@ -20,3 +20,9 @@
border-radius: 8px;
font-weight: 600;
}
+
+.error {
+ color: #ef4444;
+ font-size: 0.85rem;
+ margin: 0;
+}
diff --git a/frontend/components/PaymentForm/PaymentForm.tsx b/frontend/components/PaymentForm/PaymentForm.tsx
index 08c1459..957bd58 100644
--- a/frontend/components/PaymentForm/PaymentForm.tsx
+++ b/frontend/components/PaymentForm/PaymentForm.tsx
@@ -2,26 +2,40 @@
import { useState } from "react";
import { useProcessPayment } from "@/hooks/useProcessPayment";
+import { isValidPrincipal } from "@/lib/utils";
import styles from "./PaymentForm.module.css";
+const MIN_STX = 1;
+
export function PaymentForm() {
const [recipient, setRecipient] = useState("");
const [amountStx, setAmountStx] = useState("");
+ const [error, setError] = useState("");
const { processPayment } = useProcessPayment();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
+ setError("");
+ if (!isValidPrincipal(recipient.trim())) {
+ setError("Invalid Stacks address");
+ return;
+ }
const amount = BigInt(Math.floor(parseFloat(amountStx || "0") * 1e6));
- if (recipient && amount > BigInt(0)) processPayment(amount, recipient);
+ if (amount < BigInt(MIN_STX * 1e6)) {
+ setError(`Minimum ${MIN_STX} STX`);
+ return;
+ }
+ processPayment(amount, recipient.trim());
};
return (