([]);
const [loading, setLoading] = useState(true);
@@ -84,7 +84,7 @@ export function WithdrawRoutesForm({
const remainingDisplay = Math.max(0, 100 - totalRoutedChain / 100);
const submitRoute = async (toAccount: string, chainPercent: number, vest: boolean) => {
- if (!loggedIn || !privateKey || loggedIn !== accountUsername) {
+ if (!loggedIn || !signingKey || loggedIn !== accountUsername) {
setError(t('mustBeAccountOwner'));
return;
}
@@ -96,7 +96,7 @@ export function WithdrawRoutesForm({
toAccount,
chainPercent,
vest,
- privateKey
+ signingKey
);
const res = await apiClient.broadcastSetWithdrawVestingRoute(signedTx, loggedIn);
if (!res.success) {
@@ -135,7 +135,7 @@ export function WithdrawRoutesForm({
void submitRoute(toAccount, 0, false);
};
- const canEdit = isMyAccount && !!loggedIn && loggedIn === accountUsername && !!privateKey;
+ const canEdit = isMyAccount && !!loggedIn && loggedIn === accountUsername && !!signingKey;
const inner = (
diff --git a/src/components/wallet/witness-vote-form.tsx b/src/components/wallet/witness-vote-form.tsx
index d3178339..34b88c36 100644
--- a/src/components/wallet/witness-vote-form.tsx
+++ b/src/components/wallet/witness-vote-form.tsx
@@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useSelector } from 'react-redux';
import type { RootState } from '@/lib/store';
-import { usePrivateKey } from '@/hooks/use-auth';
+import { useActiveSigningKey } from '@/hooks/use-auth';
import { useAccountData } from '@/hooks/use-account-data';
import { SteemSigner, apiClient } from '@/lib/steem/client';
import type { Witness } from '@/lib/steem/types';
@@ -21,7 +21,7 @@ export function WitnessVoteForm() {
const tCommon = useTranslations('common');
const router = useRouter();
const username = useSelector((state: RootState) => state.auth.username);
- const privateKey = usePrivateKey();
+ const signingKey = useActiveSigningKey();
const [isPending, startTransition] = useTransition();
const { data: account } = useAccountData();
@@ -80,7 +80,7 @@ export function WitnessVoteForm() {
const handleVote = async (witness: Witness, approve: boolean) => {
setError('');
- if (!username || !privateKey) {
+ if (!username || !signingKey) {
setError('Not authenticated');
return;
}
@@ -88,7 +88,7 @@ export function WitnessVoteForm() {
setIsLoading(true);
try {
// Sign transaction
- const signedTx = SteemSigner.signWitnessVote(username, witness.owner, approve, privateKey);
+ const signedTx = SteemSigner.signWitnessVote(username, witness.owner, approve, signingKey);
// Broadcast
const response = await apiClient.broadcastWitnessVote(signedTx, username);
diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts
index e077beba..05209af0 100644
--- a/src/hooks/use-auth.ts
+++ b/src/hooks/use-auth.ts
@@ -179,3 +179,11 @@ export function useRequireAuth(): { username: string | null; isAuthenticated: bo
export function usePrivateKey(): string | null {
return useSelector((state: RootState) => state.auth.activeKey || state.auth.privateKey);
}
+
+/**
+ * Private key that can sign operations requiring active (or owner) authority.
+ * Do not use posting/memo keys for transfers / power / delegate / etc.
+ */
+export function useActiveSigningKey(): string | null {
+ return useSelector((state: RootState) => state.auth.activeKey || state.auth.ownerKey || null);
+}
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 9f15f8f0..75cdd149 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -53,6 +53,10 @@
"transfer": "Transfer",
"powerDown": "Power Down",
"delegations": "Delegations",
+ "keysAndPermissions": "Keys & Permissions",
+ "changePassword": "Change Password",
+ "communities": "Communities",
+ "sectionPlaceholderHint": "This page is a placeholder. Full functionality will be added in a future update.",
"witnesses": "Witnesses",
"witnessVoting": "Witness Voting",
"history": "History",
@@ -72,8 +76,10 @@
"effectiveSP": "Effective Steem Power",
"receivedVests": "Received VESTS",
"delegatedVests": "Delegated VESTS",
- "buySteem": "Buy Steem or Steem Power",
+ "buySteem": "Buy STEEM",
"claimRewards": "Redeem Rewards",
+ "reauthTitle": "Sign in to continue",
+ "reauthDescription": "This action requires your active key. Enter your password or private key.",
"liquidBalanceDesc": "Liquid token, transferable at any time.",
"powerDesc": "Influence token that gives you more influence.",
"sbdDesc": "Tradeable tokens that can be transferred to savings.",
diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json
index 663dd2d0..f1af7e9a 100644
--- a/src/i18n/messages/es.json
+++ b/src/i18n/messages/es.json
@@ -53,6 +53,10 @@
"transfer": "Transferir",
"powerDown": "Bajar Energía",
"delegations": "Delegaciones",
+ "keysAndPermissions": "Claves y permisos",
+ "changePassword": "Cambiar contraseña",
+ "communities": "Comunidades",
+ "sectionPlaceholderHint": "Esta página es provisional. La función completa se añadirá más adelante.",
"witnesses": "Testigos",
"witnessVoting": "Votación de Testigos",
"history": "Historial",
@@ -76,9 +80,12 @@
"navDiscover": "Descubrir",
"navAccess": "Acceso",
"blog": "Blog",
+ "buySteem": "Comprar STEEM",
"proposals": "Propuestas",
"market": "Mercado",
"profileJoined": "Miembro desde",
+ "reauthTitle": "Inicia sesión para continuar",
+ "reauthDescription": "Esta acción requiere tu clave activa. Introduce tu contraseña o clave privada.",
"withdrawRoutes": {
"title": "Rutas de retiro de Power down",
"dialogDescription": "Administra a dónde se envía el STEEM del power down.",
diff --git a/src/i18n/messages/zh.json b/src/i18n/messages/zh.json
index c311256c..174cf549 100644
--- a/src/i18n/messages/zh.json
+++ b/src/i18n/messages/zh.json
@@ -53,6 +53,10 @@
"transfer": "转账",
"powerDown": "能量下调",
"delegations": "委托",
+ "keysAndPermissions": "密钥与权限",
+ "changePassword": "修改密码",
+ "communities": "社区",
+ "sectionPlaceholderHint": "本页为占位界面,完整功能将在后续版本提供。",
"witnesses": "见证人",
"witnessVoting": "见证人投票",
"history": "历史",
@@ -72,8 +76,10 @@
"effectiveSP": "有效 Steem Power",
"receivedVests": "收到的 VESTS",
"delegatedVests": "委托的 VESTS",
- "buySteem": "购买 STEEM 或 STEEM POWER",
+ "buySteem": "购买 STEEM",
"claimRewards": "领取奖励",
+ "reauthTitle": "请登录后继续",
+ "reauthDescription": "此操作需要 active 权限。请输入主密码或私钥。",
"liquidBalanceDesc": "流动代币,可随时转账。",
"powerDesc": "影响力代币,给予您更多影响力。",
"sbdDesc": "可交易的代币,可转入储蓄。",
diff --git a/src/lib/auth/browser-storage.ts b/src/lib/auth/browser-storage.ts
index cbade7c7..30171eea 100644
--- a/src/lib/auth/browser-storage.ts
+++ b/src/lib/auth/browser-storage.ts
@@ -4,3 +4,38 @@
*/
export const REMEMBERED_USERNAME_KEY = 'wallet:rememberedUsername';
export const REMEMBERED_POSTING_KEY_KEY = 'wallet:rememberedPostingKey';
+
+/** Normalize Steem account names for comparisons (matches LoginForm handling). */
+export function normalizeSteemUsername(raw: string): string {
+ return raw.trim().toLowerCase().replace(/^@+/, '');
+}
+
+/** Read remembered username from localStorage (client only). */
+export function getRememberedDeviceUsername(): string | null {
+ if (typeof window === 'undefined') return null;
+ try {
+ const saved = localStorage.getItem(REMEMBERED_USERNAME_KEY);
+ if (!saved?.trim()) return null;
+ return normalizeSteemUsername(saved);
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Whether this device/session may use balance actions on the given profile URL.
+ * True if the URL account is remembered on this device or the Redux session user matches.
+ */
+export function canManageBalanceForPageUrl(params: {
+ urlUsername: string;
+ loggedInUser: string | null;
+ isAuthenticated: boolean;
+}): boolean {
+ const { urlUsername, loggedInUser, isAuthenticated } = params;
+ if (!urlUsername?.trim()) return false;
+ const nu = normalizeSteemUsername(urlUsername);
+ const remembered = getRememberedDeviceUsername();
+ if (remembered && remembered === nu) return true;
+ if (isAuthenticated && loggedInUser && normalizeSteemUsername(loggedInUser) === nu) return true;
+ return false;
+}
diff --git a/src/lib/steem/normalize-profile.ts b/src/lib/steem/normalize-profile.ts
index 3c356570..e324a727 100644
--- a/src/lib/steem/normalize-profile.ts
+++ b/src/lib/steem/normalize-profile.ts
@@ -87,7 +87,7 @@ export function normalizeProfile(account: AccountWithMeta | null | undefined): N
let name = pickString(profile.name);
let about = pickString(profile.about);
let location = pickString(profile.location);
- let website = normalizeWebsite(pickString(profile.website));
+ const website = normalizeWebsite(pickString(profile.website));
const profile_image = normalizeHttpsUrl(pickString(profile.profile_image));
const cover_image = normalizeHttpsUrl(pickString(profile.cover_image));
diff --git a/tests/e2e/smoke.spec.ts b/tests/e2e/smoke.spec.ts
index 52f616c2..b92f3745 100644
--- a/tests/e2e/smoke.spec.ts
+++ b/tests/e2e/smoke.spec.ts
@@ -3,12 +3,18 @@ import { test, expect } from '@playwright/test';
test.describe('Smoke', () => {
test('home page loads and shows login form', async ({ page }) => {
await page.goto('/');
- await expect(page.getByRole('button', { name: /sign in|login/i }).first()).toBeVisible({ timeout: 15_000 });
+ // LoginForm submit (locale-aware); not "password" — secret field is labeled Private Key.
+ await expect(
+ page.getByRole('button', { name: /sign in|log in|login|登录|iniciar sesión/i }).first()
+ ).toBeVisible({ timeout: 15_000 });
});
- test('login page has username and password inputs', async ({ page }) => {
+ test('login page has username and private key inputs', async ({ page }) => {
await page.goto('/login');
- await expect(page.getByRole('textbox', { name: /username/i }).first()).toBeVisible({ timeout: 15_000 });
- await expect(page.getByLabel(/password/i).first()).toBeVisible({ timeout: 5_000 });
+ await expect(
+ page.getByRole('textbox', { name: /username|用户|usuario/i }).first()
+ ).toBeVisible({ timeout: 15_000 });
+ // Label comes from auth.privateKey ("Private Key" / "私钥" / "Clave privada"), not "password".
+ await expect(page.locator('#password')).toBeVisible({ timeout: 5_000 });
});
});