Skip to content
Draft
26 changes: 15 additions & 11 deletions aastar-frontend/app/address-book/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Layout from "@/components/Layout";
import { addressBookAPI } from "@/lib/api";
import { getAddressBook, setAddressName, removeAddress } from "@/lib/address-book-store";
import { useDashboard } from "@/contexts/DashboardContext";
import SwipeableListItem from "@/components/SwipeableListItem";
import toast from "react-hot-toast";
import { PencilIcon, PlusIcon, BookOpenIcon } from "@heroicons/react/24/outline";
Expand All @@ -19,6 +20,8 @@ interface AddressBookEntry {

export default function AddressBookPage() {
const router = useRouter();
const { data } = useDashboard();
const accountAddress = data?.account?.address ?? null;
const [addressBook, setAddressBook] = useState<AddressBookEntry[]>([]);
const [loading, setLoading] = useState(true);
const [editingAddress, setEditingAddress] = useState<string | null>(null);
Expand All @@ -27,15 +30,16 @@ export default function AddressBookPage() {
const [newAddress, setNewAddress] = useState("");
const [newName, setNewName] = useState("");

// Re-load when the (async) account resolves so the list is scoped to this account.
useEffect(() => {
loadAddressBook();
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accountAddress]);

const loadAddressBook = async () => {
const loadAddressBook = () => {
setLoading(true);
try {
const response = await addressBookAPI.getAddressBook();
setAddressBook(response.data);
setAddressBook(getAddressBook(accountAddress));
} catch (error) {
console.error("Failed to load address book:", error);
toast.error("Failed to load address book");
Expand All @@ -57,8 +61,8 @@ export default function AddressBookPage() {
}

try {
await addressBookAPI.setAddressName(newAddress, newName || "");
await loadAddressBook();
setAddressName(accountAddress, newAddress, newName || "");
loadAddressBook();
setShowAddForm(false);
setNewAddress("");
setNewName("");
Expand All @@ -73,8 +77,8 @@ export default function AddressBookPage() {

const handleUpdateName = async (address: string) => {
try {
await addressBookAPI.setAddressName(address, editingName);
await loadAddressBook();
setAddressName(accountAddress, address, editingName);
loadAddressBook();
setEditingAddress(null);
setEditingName("");
toast.success("Name updated successfully!");
Expand All @@ -92,8 +96,8 @@ export default function AddressBookPage() {
}

try {
await addressBookAPI.removeAddress(address);
await loadAddressBook();
removeAddress(accountAddress, address);
loadAddressBook();
toast.success("Address deleted successfully!");
} catch (error) {
const message =
Expand Down
25 changes: 14 additions & 11 deletions aastar-frontend/app/paymaster/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import Layout from "@/components/Layout";
import { paymasterAPI } from "@/lib/api";
import {
getAvailablePaymasters,
getPaymasterPresets,
addCustomPaymaster,
removeCustomPaymaster,
} from "@/lib/paymaster-store";
import { useDashboard } from "@/contexts/DashboardContext";
import SwipeableListItem from "@/components/SwipeableListItem";
import toast from "react-hot-toast";
Expand Down Expand Up @@ -55,7 +60,9 @@ export default function PaymasterPage() {

useEffect(() => {
loadPaymasters();
}, []);
// saved paymasters are account-scoped (localStorage) — reload when the account resolves
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accountAddress]);

// Re-read the account-scoped default whenever the (async-loaded) account resolves,
// so the ☆ / "Default" state reflects THIS account's preference.
Expand All @@ -81,12 +88,8 @@ export default function PaymasterPage() {
const loadPaymasters = async () => {
setLoading(true);
try {
const [available, presetList] = await Promise.all([
paymasterAPI.getAvailable(),
paymasterAPI.getPresets().catch(() => ({ data: [] })),
]);
setPaymasters(available.data);
setPresets(presetList.data);
setPaymasters(getAvailablePaymasters(accountAddress));
setPresets(getPaymasterPresets());
} catch (error) {
console.error("Failed to load paymasters:", error);
toast.error("Failed to load paymasters");
Expand All @@ -99,7 +102,7 @@ export default function PaymasterPage() {
const handleAddPreset = async (preset: PaymasterPreset) => {
setActionLoading(`preset-${preset.name}`);
try {
await paymasterAPI.addCustom({
addCustomPaymaster(accountAddress, {
name: preset.name,
address: preset.address,
type: preset.type,
Expand Down Expand Up @@ -130,7 +133,7 @@ export default function PaymasterPage() {

setActionLoading("add");
try {
await paymasterAPI.addCustom({
addCustomPaymaster(accountAddress, {
name: newPaymaster.name,
address: newPaymaster.address,
type: newPaymaster.type,
Expand Down Expand Up @@ -166,7 +169,7 @@ export default function PaymasterPage() {
setActionLoading(`remove-${name}`);
try {
const removed = paymasters.find(p => p.name === name);
await paymasterAPI.remove(name);
removeCustomPaymaster(accountAddress, name);
// If the removed paymaster was the persisted default, drop the stale preference.
if (removed && defaultPaymaster === removed.address.toLowerCase()) {
clearDefaultPaymaster(accountAddress);
Expand Down
180 changes: 180 additions & 0 deletions aastar-frontend/app/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"use client";

import { useEffect, useState } from "react";
import Layout from "@/components/Layout";
import toast from "react-hot-toast";
import {
getApiKey,
setApiKey,
clearApiKey,
getKmsUrl,
setKmsUrl,
getBundlerUrl,
setBundlerUrl,
} from "@/lib/api-key-store";
import { kmsBaseUrl, isDirectKmsReady, pingKms } from "@/lib/kms-client";

export default function SettingsPage() {
const [apiKey, setApiKeyInput] = useState("");
const [kmsUrl, setKmsUrlInput] = useState("");
const [bundlerUrl, setBundlerUrlInput] = useState("");
const [ready, setReady] = useState(false);
const [resolvedKms, setResolvedKms] = useState("");
const [pinging, setPinging] = useState(false);
const [pingResult, setPingResult] = useState<string | null>(null);

useEffect(() => {
setApiKeyInput(getApiKey() ?? "");
setKmsUrlInput(getKmsUrl() ?? "");
setBundlerUrlInput(getBundlerUrl() ?? "");
setReady(isDirectKmsReady());
setResolvedKms(kmsBaseUrl());
}, []);

const handleSave = () => {
if (apiKey.trim()) setApiKey(apiKey);
else clearApiKey();
setKmsUrl(kmsUrl);
setBundlerUrl(bundlerUrl);
setReady(isDirectKmsReady());
setResolvedKms(kmsBaseUrl());
toast.success("Settings saved (this device)");
};

const handleClear = () => {
clearApiKey();
setKmsUrl("");
setBundlerUrl("");
setApiKeyInput("");
setKmsUrlInput("");
setBundlerUrlInput("");
setReady(false);
setResolvedKms(kmsBaseUrl());
setPingResult(null);
toast.success("Cleared");
};

const handlePing = async () => {
setPinging(true);
setPingResult(null);
const r = await pingKms();
setPingResult(r.ok ? `OK (${r.status})` : `Failed: ${r.error ?? r.status}`);
setPinging(false);
};

const inputClass =
"block w-full px-4 py-3 border-2 border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-xl shadow-sm focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-sm placeholder-gray-400 dark:placeholder-gray-500 transition-all";

return (
<Layout requireAuth={true}>
<div className="max-w-2xl px-3 py-4 sm:px-4 sm:py-6 mx-auto lg:px-8">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white mb-1">
Settings
</h1>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
Your AAStar API key authorizes KMS + gas (bundler) access. Stored on this device only. A
free tier covers normal use; more usage is paid / aPoints.
</p>

<div className="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-5 sm:p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
AAStar API Key
</label>
<input
type="password"
value={apiKey}
onChange={e => setApiKeyInput(e.target.value)}
placeholder="Paste your API key"
autoComplete="off"
className={inputClass}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Authorizes both KMS and the bundler. Leave blank to keep using the hosted proxy.
</p>
</div>

<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
KMS Endpoint (optional override)
</label>
<input
type="text"
value={kmsUrl}
onChange={e => setKmsUrlInput(e.target.value)}
placeholder={kmsBaseUrl()}
autoComplete="off"
spellCheck={false}
className={`${inputClass} font-mono`}
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Bundler Endpoint (optional override)
</label>
<input
type="text"
value={bundlerUrl}
onChange={e => setBundlerUrlInput(e.target.value)}
placeholder="Default bundler"
autoComplete="off"
spellCheck={false}
className={`${inputClass} font-mono`}
/>
</div>

<div className="flex flex-col sm:flex-row gap-3 pt-2">
<button
onClick={handleSave}
className="inline-flex items-center justify-center px-4 py-3 sm:py-2 text-sm font-medium text-white bg-slate-900 hover:bg-slate-800 dark:bg-emerald-600 dark:hover:bg-emerald-500 rounded-xl shadow transition active:scale-95"
>
Save
</button>
<button
onClick={handlePing}
disabled={pinging}
className="inline-flex items-center justify-center px-4 py-3 sm:py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-xl transition active:scale-95 disabled:opacity-60"
>
{pinging ? "Testing…" : "Test KMS connection"}
</button>
<button
onClick={handleClear}
className="inline-flex items-center justify-center px-4 py-3 sm:py-2 text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-xl transition active:scale-95"
>
Clear
</button>
</div>
</div>

{/* Status */}
<div className="mt-6 rounded-xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 p-4 text-sm">
<div className="flex items-center justify-between py-1">
<span className="text-slate-600 dark:text-slate-400">Direct-KMS ready</span>
<span className={ready ? "text-emerald-600 dark:text-emerald-400" : "text-gray-500"}>
{ready ? "Yes (API key set)" : "No (using hosted proxy)"}
</span>
</div>
<div className="flex items-center justify-between py-1">
<span className="text-slate-600 dark:text-slate-400">Resolved KMS endpoint</span>
<span className="font-mono text-xs text-slate-700 dark:text-slate-300 break-all">
{resolvedKms}
</span>
</div>
{pingResult && (
<div className="flex items-center justify-between py-1">
<span className="text-slate-600 dark:text-slate-400">Last test</span>
<span className="text-xs text-slate-700 dark:text-slate-300">{pingResult}</span>
</div>
)}
</div>

<p className="mt-4 text-xs text-gray-500 dark:text-gray-400">
Note: transfer signing still runs through the hosted service today. Direct browser→KMS +
direct bundler (using this key) turn on once the KMS Origin/API-key path is live — this
screen configures it ahead of that switch.
</p>
</div>
</Layout>
);
}
Loading
Loading