fix(auth): sign in against the v0.3 backend

v0.3 wallets register with the v0.3 backend, so a site built against the
legacy backend answered every v0.3 sign-in with "User not found". The
v0.3 backend resolves website sign-in by each coin's own key and adopts a
legacy identity on first proof, so it serves both generations.

The v0.3 backend has no social linking. Settings now loads linked accounts
separately and hides that section when they are unavailable, instead of
failing the whole page and taking username editing with it. The public user
count and stats may come from a separate backend via
NEXT_PUBLIC_STATS_API_URL; unset keeps today's behavior.
This commit is contained in:
jwinterm
2026-10-05 19:33:04 -04:00
parent afd1c231db
commit eaf4c90cab
3 changed files with 152 additions and 126 deletions
+4
View File
@@ -2,3 +2,7 @@
# For local development: http://localhost:8080
# For production: https://api.smirk.cash (or your backend URL)
NEXT_PUBLIC_API_URL=http://localhost:8080
# Optional: backend serving the public user count and stats, when it differs
# from the sign-in backend above. Unset means the same backend.
# NEXT_PUBLIC_STATS_API_URL=https://backend.smirk.cash
+22 -3
View File
@@ -21,6 +21,7 @@ type LinkStep = 'idle' | 'linking' | 'waiting';
export default function SettingsPage() {
const [user, setUser] = useState<MeResponse | null>(null);
const [socials, setSocials] = useState<LinkedSocial[]>([]);
const [socialsAvailable, setSocialsAvailable] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -81,10 +82,16 @@ export default function SettingsPage() {
return;
}
Promise.all([getMe(token), getLinkedSocials(token), getMyUsername(token)])
.then(([userData, socialsData, username]) => {
// Linked accounts are a legacy-backend feature; the v0.3 backend has no
// social linking. Load them separately so their absence hides that section
// instead of failing the whole page.
getLinkedSocials(token)
.then((socialsData) => setSocials(socialsData.socials))
.catch(() => setSocialsAvailable(false));
Promise.all([getMe(token), getMyUsername(token)])
.then(([userData, username]) => {
setUser(userData);
setSocials(socialsData.socials);
setSmirkUsername(username || '');
setUsernameInput(username || '');
})
@@ -320,6 +327,8 @@ export default function SettingsPage() {
<section>
<h2 className="text-lg font-semibold mb-4 text-zinc-400">Linked Accounts</h2>
{socialsAvailable ? (
<>
{/* Telegram */}
<div className="bg-zinc-900 rounded-xl p-4 mb-4">
<div className="flex items-center justify-between">
@@ -454,6 +463,16 @@ export default function SettingsPage() {
)}
</div>
</>
) : (
<div className="bg-zinc-900 rounded-xl p-4 mb-4">
<p className="text-sm text-zinc-500">
Linking Telegram and Discord isn&apos;t available for this wallet. Your Smirk
username above is how others find and tip you.
</p>
</div>
)}
{/* Future platforms */}
<div className="space-y-3">
{['Signal', 'Matrix'].map((platform) => (
+5 -2
View File
@@ -1,6 +1,9 @@
// API client for Smirk backend
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
// Public counters (user count, stats) may live on a different backend than
// sign-in. Unset means the same backend.
const STATS_API_URL = process.env.NEXT_PUBLIC_STATS_API_URL || API_URL;
export interface ChallengeResponse {
challenge: string;
@@ -398,7 +401,7 @@ export interface UserCountResponse {
}
export async function getUserCount(): Promise<UserCountResponse> {
const res = await fetch(`${API_URL}/api/v1/users/count`);
const res = await fetch(`${STATS_API_URL}/api/v1/users/count`);
if (!res.ok) {
throw new Error('Failed to get user count');
@@ -419,7 +422,7 @@ export interface PublicStats {
}
export async function getPublicStats(): Promise<PublicStats> {
const res = await fetch(`${API_URL}/api/v1/stats/public`);
const res = await fetch(`${STATS_API_URL}/api/v1/stats/public`);
if (!res.ok) {
throw new Error('Failed to get stats');