feat: show provider metadata in saved profiles

This commit is contained in:
thePR0M3TH3AN committed 2025-10-21 14:44:14 -04:00
1 parent 3532c212f3
commit 6d84d6be4e
8 files changed
+362 -29

No files matched your search

+17 -2
View File
@@ -2085,6 +2085,20 @@ class Application {
return "nip07";
})();
const resolvedProviderId = (() => {
const candidates = [detailProviderId, providerId, resolvedAuthType];
for (const candidate of candidates) {
if (typeof candidate !== "string") {
continue;
}
const trimmed = candidate.trim();
if (trimmed) {
return trimmed;
}
}
return resolvedAuthType;
})();
const normalizedPubkey = this.normalizeHexPubkey(pubkey);
if (!normalizedPubkey) {
throw new Error(
@@ -2119,6 +2133,7 @@ class Application {
name,
picture,
authType: resolvedAuthType,
providerId: resolvedProviderId,
});
persistSavedProfiles({ persistActive: false });
@@ -2955,12 +2970,12 @@ class Application {
}
}
async handleProfileSwitchRequest({ pubkey } = {}) {
async handleProfileSwitchRequest({ pubkey, providerId } = {}) {
if (!pubkey) {
throw new Error("Missing pubkey for profile switch request.");
}
const result = await this.authService.switchProfile(pubkey);
const result = await this.authService.switchProfile(pubkey, { providerId });
if (result?.switched) {
try {
+121 -6
View File
@@ -1,14 +1,129 @@
import nip07 from "./nip07.js";
const map = {
[(nip07 && typeof nip07.id === "string" && nip07.id.trim()) || "nip07"]: nip07,
};
function normalizeProviderId(provider, fallbackId) {
if (provider && typeof provider.id === "string") {
const trimmed = provider.id.trim();
if (trimmed) {
return trimmed;
}
}
if (!Object.prototype.hasOwnProperty.call(map, "nip07")) {
map.nip07 = nip07;
if (typeof fallbackId === "string") {
const trimmed = fallbackId.trim();
if (trimmed) {
return trimmed;
}
}
return "provider";
}
export const providers = Object.freeze({ ...map });
function normalizeProviderMetadata(provider, fallbackId) {
const id = normalizeProviderId(provider, fallbackId);
const label =
provider && typeof provider.label === "string" && provider.label.trim()
? provider.label.trim()
: "Saved profile";
const description =
provider && typeof provider.description === "string" && provider.description.trim()
? provider.description.trim()
: "";
const badgeVariant =
provider && typeof provider.badgeVariant === "string" && provider.badgeVariant.trim()
? provider.badgeVariant.trim()
: "neutral";
return Object.freeze({ id, label, description, badgeVariant });
}
const providerList = [nip07].filter(Boolean);
const providerMap = {};
const metadataMap = {};
providerList.forEach((provider, index) => {
const fallbackId = `provider-${index}`;
const id = normalizeProviderId(provider, fallbackId);
providerMap[id] = provider;
metadataMap[id] = normalizeProviderMetadata(provider, id);
});
if (!Object.prototype.hasOwnProperty.call(providerMap, "nip07")) {
providerMap.nip07 = nip07;
metadataMap.nip07 = normalizeProviderMetadata(nip07, "nip07");
} else if (!Object.prototype.hasOwnProperty.call(metadataMap, "nip07")) {
metadataMap.nip07 = normalizeProviderMetadata(providerMap.nip07, "nip07");
}
const builtInMetadata = [
{
id: "nsec",
label: "Direct key",
description: "Use a saved nsec key stored locally.",
badgeVariant: "warning",
},
];
builtInMetadata.forEach((entry) => {
if (!entry || typeof entry !== "object") {
return;
}
const id =
typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : null;
if (!id || Object.prototype.hasOwnProperty.call(metadataMap, id)) {
return;
}
metadataMap[id] = Object.freeze({
id,
label:
typeof entry.label === "string" && entry.label.trim()
? entry.label.trim()
: "Saved profile",
description:
typeof entry.description === "string" && entry.description.trim()
? entry.description.trim()
: "",
badgeVariant:
typeof entry.badgeVariant === "string" && entry.badgeVariant.trim()
? entry.badgeVariant.trim()
: "neutral",
});
});
export const providers = Object.freeze({ ...providerMap });
export const providerMetadata = Object.freeze({ ...metadataMap });
const unknownMetadataCache = new Map();
function getFallbackMetadata(providerId) {
const id =
typeof providerId === "string" && providerId.trim() ? providerId.trim() : "unknown";
if (!unknownMetadataCache.has(id)) {
unknownMetadataCache.set(
id,
Object.freeze({
id,
label: "Saved profile",
description: "",
badgeVariant: "neutral",
}),
);
}
return unknownMetadataCache.get(id);
}
export function getProviderMetadata(providerId) {
if (typeof providerId === "string" && providerId.trim()) {
const normalizedId = providerId.trim();
if (Object.prototype.hasOwnProperty.call(providerMetadata, normalizedId)) {
return providerMetadata[normalizedId];
}
return getFallbackMetadata(normalizedId);
}
return getFallbackMetadata("saved-profile");
}
export function getProvider(providerId = "nip07") {
const id =
+1
View File
@@ -47,6 +47,7 @@ export default {
id: PROVIDER_ID,
label: PROVIDER_LABEL,
description: PROVIDER_DESCRIPTION,
badgeVariant: "info",
capabilities: PROVIDER_CAPABILITIES,
button: PROVIDER_BUTTON,
messages: PROVIDER_MESSAGES,
+15 -3
View File
@@ -521,6 +521,8 @@ export default class AuthService {
providerResult,
);
const entryProviderId = this.normalizeProviderId(providerId) || entryAuthType;
mutateSavedProfiles((profiles) => {
const draft = Array.isArray(profiles) ? profiles.slice() : [];
const existingIndex = draft.findIndex((entry) => {
@@ -537,6 +539,7 @@ export default class AuthService {
name: cachedProfile.name || draft[existingIndex]?.name || "",
picture: cachedProfile.picture || draft[existingIndex]?.picture || "",
authType: entryAuthType,
providerId: entryProviderId,
};
if (existingIndex >= 0) {
@@ -545,7 +548,8 @@ export default class AuthService {
currentEntry.npub !== nextEntry.npub ||
currentEntry.name !== nextEntry.name ||
currentEntry.picture !== nextEntry.picture ||
currentEntry.authType !== nextEntry.authType;
currentEntry.authType !== nextEntry.authType ||
currentEntry.providerId !== nextEntry.providerId;
if (changed) {
draft[existingIndex] = nextEntry;
savedProfilesMutated = true;
@@ -791,12 +795,16 @@ export default class AuthService {
return { removed: changed };
}
async switchProfile(pubkey) {
async switchProfile(pubkey, options = {}) {
const normalizedTarget = this.normalizeHexPubkey(pubkey);
if (!normalizedTarget) {
throw new Error("Unable to switch profiles: invalid account.");
}
const normalizedOptions =
options && typeof options === "object" ? { ...options } : {};
const providerId = this.normalizeProviderId(normalizedOptions.providerId);
const normalizedActive = this.normalizeHexPubkey(getActiveProfilePubkey());
if (normalizedActive && normalizedActive === normalizedTarget) {
return { switched: false, reason: "already-active" };
@@ -806,6 +814,7 @@ export default class AuthService {
allowAccountSelection: true,
expectPubkey: normalizedTarget,
persistActive: true,
...(providerId ? { providerId } : {}),
};
const autoApply = requestOptions.autoApply !== false;
@@ -827,7 +836,10 @@ export default class AuthService {
if (!detail && !autoApply) {
const fallbackTarget =
this.normalizeHexPubkey(requestedPubkey) || requestedPubkey || normalizedTarget;
detail = await this.login(fallbackTarget, { persistActive: true });
detail = await this.login(fallbackTarget, {
persistActive: true,
providerId,
});
}
mutateSavedProfiles((profiles) => {
+23
View File
@@ -364,6 +364,10 @@ export function persistSavedProfiles({ persistActive = true } = {}) {
typeof entry.authType === "string" && entry.authType.trim()
? entry.authType.trim()
: null,
providerId:
typeof entry.providerId === "string" && entry.providerId.trim()
? entry.providerId.trim()
: null,
})),
activePubkey: activePubkeyToPersist,
};
@@ -430,6 +434,24 @@ export function loadSavedProfilesFromStorage() {
needsRewrite = true;
}
}
const rawProviderId =
typeof candidate.providerId === "string" ? candidate.providerId : null;
let storedProviderId = null;
if (rawProviderId) {
const trimmedProviderId = rawProviderId.trim();
if (trimmedProviderId) {
storedProviderId = trimmedProviderId;
if (trimmedProviderId !== rawProviderId) {
needsRewrite = true;
}
}
}
if (!storedProviderId && storedAuthType) {
storedProviderId = storedAuthType;
if (rawProviderId !== storedProviderId) {
needsRewrite = true;
}
}
const npub =
typeof candidate.npub === "string" && candidate.npub.trim()
? candidate.npub.trim()
@@ -441,6 +463,7 @@ export function loadSavedProfilesFromStorage() {
name: typeof candidate.name === "string" ? candidate.name : "",
picture: typeof candidate.picture === "string" ? candidate.picture : "",
authType: storedAuthType,
providerId: storedProviderId,
};
if (
+115 -18
View File
@@ -11,6 +11,7 @@ import {
getTrustedSpamHideThreshold,
RUNTIME_FLAGS,
} from "../constants.js";
import { getProviderMetadata } from "../services/authProviders/index.js";
import { devLogger, userLogger } from "../utils/logger.js";
const noop = () => {};
@@ -21,6 +22,30 @@ const DEFAULT_ADMIN_DM_IMAGE_URL =
const DEFAULT_BITVID_WEBSITE_URL = "https://bitvid.network/";
const NWC_URI_SCHEME = "nostr+walletconnect://";
const DEFAULT_MAX_WALLET_DEFAULT_ZAP = 100000000;
const DEFAULT_SAVED_PROFILE_LABEL = "Saved profile";
const PROVIDER_BADGE_BASE_CLASS =
"text-3xs font-semibold uppercase tracking-extra-wide";
const PROVIDER_BADGE_VARIANT_CLASS_MAP = Object.freeze({
info: "text-status-info",
success: "text-status-success",
warning: "text-status-warning",
danger: "text-status-danger",
neutral: "text-muted",
accent: "text-accent",
primary: "text-accent",
});
function resolveProviderBadgeClass(variant) {
if (typeof variant === "string" && variant.trim()) {
const normalized = variant.trim().toLowerCase();
if (Object.prototype.hasOwnProperty.call(PROVIDER_BADGE_VARIANT_CLASS_MAP, normalized)) {
return PROVIDER_BADGE_VARIANT_CLASS_MAP[normalized];
}
}
return PROVIDER_BADGE_VARIANT_CLASS_MAP.neutral;
}
const DEFAULT_INTERNAL_NWC_SETTINGS = Object.freeze({
nwcUri: "",
@@ -1419,8 +1444,8 @@ export class ProfileModalController {
? (value) => this.formatShortNpub(value)
: (value) => (typeof value === "string" ? value : "");
const activeNameFallback = activeMeta?.npub
? formatNpub(activeMeta.npub) || "Saved profile"
: "Saved profile";
? formatNpub(activeMeta.npub) || DEFAULT_SAVED_PROFILE_LABEL
: DEFAULT_SAVED_PROFILE_LABEL;
const activeDisplayName = hasActiveProfile
? activeMeta?.name?.trim() || activeNameFallback
: "No active profile";
@@ -1545,7 +1570,9 @@ export class ProfileModalController {
avatarImg.src = meta.picture || FALLBACK_PROFILE_AVATAR;
const cardDisplayName =
meta.name?.trim() ||
(meta.npub ? formatNpub(meta.npub) || "Saved profile" : "Saved profile");
(meta.npub
? formatNpub(meta.npub) || DEFAULT_SAVED_PROFILE_LABEL
: DEFAULT_SAVED_PROFILE_LABEL);
avatarImg.alt = `${cardDisplayName} avatar`;
avatarSpan.appendChild(avatarImg);
@@ -1555,11 +1582,24 @@ export class ProfileModalController {
const topLine = document.createElement("div");
topLine.className = "flex flex-wrap items-center gap-3";
const providerId = this.getEntryProviderId(entry);
const providerInfo = this.resolveEntryProviderMetadata(entry);
const providerLabel =
(providerInfo && providerInfo.label) || DEFAULT_SAVED_PROFILE_LABEL;
const badgeVariant = resolveProviderBadgeClass(
providerInfo && providerInfo.badgeVariant,
);
const label = document.createElement("span");
label.className =
"text-3xs font-semibold uppercase tracking-extra-wide text-status-info";
label.textContent =
normalizedAuthType === "nsec" ? "Direct key" : "Saved profile";
label.className = `${PROVIDER_BADGE_BASE_CLASS} ${badgeVariant}`;
label.textContent = providerLabel;
label.dataset.providerVariant =
(providerInfo && providerInfo.badgeVariant) || "neutral";
if (providerId) {
label.dataset.providerId = providerId;
} else if (providerInfo && providerInfo.id) {
label.dataset.providerId = providerInfo.id;
}
const action = document.createElement("span");
action.className = "text-xs font-medium text-muted";
@@ -1589,6 +1629,15 @@ export class ProfileModalController {
: `Switch to ${cardDisplayName}`;
button.setAttribute("aria-label", ariaLabel);
const datasetProviderId =
providerId || (providerInfo && providerInfo.id) || "";
if (datasetProviderId) {
button.dataset.providerId = datasetProviderId;
} else {
delete button.dataset.providerId;
}
const activateProfile = async (event) => {
if (event) {
event.preventDefault();
@@ -1602,13 +1651,22 @@ export class ProfileModalController {
button.dataset.loading = "true";
button.setAttribute("aria-busy", "true");
try {
await this.switchProfile(entry.pubkey, { entry });
} catch (error) {
userLogger.error("Failed to switch profile:", error);
} finally {
button.dataset.loading = "false";
button.setAttribute("aria-busy", "false");
const logProviderId =
providerId || (providerInfo && providerInfo.id) || normalizedAuthType || "unknown";
try {
await this.switchProfile(entry.pubkey, {
entry,
providerId: providerId || null,
});
} catch (error) {
userLogger.error(
`[ProfileModalController] Failed to switch profile for provider ${logProviderId}:`,
error,
);
} finally {
button.dataset.loading = "false";
button.setAttribute("aria-busy", "false");
}
};
@@ -4708,16 +4766,20 @@ export class ProfileModalController {
);
}
async requestSwitchProfile({ pubkey, entry } = {}) {
async requestSwitchProfile({ pubkey, entry, providerId } = {}) {
const callback = this.callbacks.onRequestSwitchProfile;
if (callback && callback !== noop) {
return callback({ controller: this, pubkey, entry });
return callback({ controller: this, pubkey, entry, providerId });
}
if (!pubkey) {
throw new Error("Missing target pubkey for switch request.");
}
if (providerId) {
return this.services.switchProfile(pubkey, { providerId });
}
return this.services.switchProfile(pubkey);
}
@@ -5618,14 +5680,14 @@ export class ProfileModalController {
}
}
async switchProfile(pubkey, { entry } = {}) {
async switchProfile(pubkey, { entry, providerId } = {}) {
if (!pubkey) {
return { switched: false, reason: "missing-pubkey" };
}
let result;
try {
result = await this.requestSwitchProfile({ pubkey, entry });
result = await this.requestSwitchProfile({ pubkey, entry, providerId });
} catch (error) {
const message =
error && typeof error.message === "string" && error.message.trim()
@@ -5661,6 +5723,41 @@ export class ProfileModalController {
return this.state.setSavedProfiles(...args);
}
normalizeProviderId(providerId) {
if (typeof providerId !== "string") {
return null;
}
const trimmed = providerId.trim();
if (!trimmed) {
return null;
}
return trimmed;
}
getEntryProviderId(entry) {
if (!entry || typeof entry !== "object") {
return null;
}
const explicit = this.normalizeProviderId(entry.providerId);
if (explicit) {
return explicit;
}
return this.normalizeProviderId(entry.authType);
}
resolveEntryProviderMetadata(entry) {
const providerId = this.getEntryProviderId(entry);
if (providerId) {
return getProviderMetadata(providerId);
}
return getProviderMetadata();
}
persistSavedProfiles(...args) {
if (typeof this.services.persistSavedProfiles === "function") {
try {
+60
View File
@@ -440,12 +440,16 @@ for (const _ of [0]) {
npub: 'npub1abcdefghijkmnopqrstuvwxyz1234567890example',
name: '',
picture: '',
providerId: 'nip07',
authType: 'nip07',
},
{
pubkey: 'b'.repeat(64),
npub: 'npub1zyxwvutsrqponmlkjihgfedcba1234567890sample',
name: '',
picture: '',
providerId: 'nsec',
authType: 'nsec',
},
];
@@ -470,6 +474,62 @@ for (const _ of [0]) {
const expectedSwitcher = formatShortNpub(sampleProfiles[1].npub);
assert.equal(switcherNpubEl.textContent, expectedSwitcher);
});
test('renderSavedProfiles applies provider metadata', async () => {
const sampleProfiles = [
{
pubkey: defaultActorHex,
npub: 'npub1abcdefghijkmnopqrstuvwxyz1234567890example',
name: 'Primary Account',
picture: '',
providerId: 'nip07',
authType: 'nip07',
},
{
pubkey: 'b'.repeat(64),
npub: 'npub1zyxwvutsrqponmlkjihgfedcba1234567890sample',
name: 'Backup',
picture: '',
providerId: 'nsec',
authType: 'nsec',
},
];
const controller = createController({
services: {
formatShortNpub,
},
});
await controller.load();
controller.state.setSavedProfiles(sampleProfiles);
controller.state.setActivePubkey(null);
controller.renderSavedProfiles();
const buttons = Array.from(
controller.switcherList.querySelectorAll('button[data-provider-id]'),
);
assert.equal(buttons.length, 2, 'expected two provider entries in the switcher');
const [extensionButton, directKeyButton] = buttons;
assert.equal(extensionButton.dataset.providerId, 'nip07');
const extensionLabel = extensionButton.querySelector('[data-provider-variant]');
assert.ok(extensionLabel, 'extension entry should include a provider badge');
assert.equal(extensionLabel.textContent, 'Login with Extension');
assert.equal(extensionLabel.dataset.providerVariant, 'info');
assert.equal(extensionButton.getAttribute('aria-pressed'), 'false');
assert.equal(directKeyButton.dataset.providerId, 'nsec');
const directKeyLabel = directKeyButton.querySelector('[data-provider-variant]');
assert.ok(directKeyLabel, 'direct key entry should include a provider badge');
assert.equal(directKeyLabel.textContent, 'Direct key');
assert.equal(directKeyLabel.dataset.providerVariant, 'warning');
assert.equal(directKeyButton.getAttribute('aria-pressed'), 'false');
});
}
test('load() injects markup and caches expected elements', async () => {
+10
View File
@@ -42,6 +42,7 @@ test("persistSavedProfiles preserves custom authType strings", () => {
name: "",
picture: "",
authType: "custom-wallet",
providerId: "custom-wallet",
},
],
{ persist: false, persistActive: false },
@@ -55,6 +56,7 @@ test("persistSavedProfiles preserves custom authType strings", () => {
const stored = JSON.parse(storedRaw);
assert.equal(stored.entries.length, 1);
assert.equal(stored.entries[0].authType, "custom-wallet");
assert.equal(stored.entries[0].providerId, "custom-wallet");
});
test("loadSavedProfilesFromStorage retains custom provider authType", () => {
@@ -71,6 +73,7 @@ test("loadSavedProfilesFromStorage retains custom provider authType", () => {
name: "",
picture: "",
authType: "nostr-wallet-connect",
providerId: "nostr-wallet-connect",
},
],
activePubkey: null,
@@ -80,9 +83,11 @@ test("loadSavedProfilesFromStorage retains custom provider authType", () => {
const { profiles } = loadSavedProfilesFromStorage();
assert.equal(profiles.length, 1);
assert.equal(profiles[0].authType, "nostr-wallet-connect");
assert.equal(profiles[0].providerId, "nostr-wallet-connect");
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY));
assert.equal(stored.entries[0].authType, "nostr-wallet-connect");
assert.equal(stored.entries[0].providerId, "nostr-wallet-connect");
});
test("loadSavedProfilesFromStorage migrates missing authType to nip07", () => {
@@ -107,9 +112,11 @@ test("loadSavedProfilesFromStorage migrates missing authType to nip07", () => {
const { profiles } = loadSavedProfilesFromStorage();
assert.equal(profiles.length, 1);
assert.equal(profiles[0].authType, "nip07");
assert.equal(profiles[0].providerId, "nip07");
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY));
assert.equal(stored.entries[0].authType, "nip07");
assert.equal(stored.entries[0].providerId, "nip07");
});
test("loadSavedProfilesFromStorage trims stored authType values", () => {
@@ -126,6 +133,7 @@ test("loadSavedProfilesFromStorage trims stored authType values", () => {
name: "",
picture: "",
authType: " extension-provider ",
providerId: " extension-provider ",
},
],
activePubkey: null,
@@ -135,7 +143,9 @@ test("loadSavedProfilesFromStorage trims stored authType values", () => {
const { profiles } = loadSavedProfilesFromStorage();
assert.equal(profiles.length, 1);
assert.equal(profiles[0].authType, "extension-provider");
assert.equal(profiles[0].providerId, "extension-provider");
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY));
assert.equal(stored.entries[0].authType, "extension-provider");
assert.equal(stored.entries[0].providerId, "extension-provider");
});