mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #2336 from PR0M3TH3AN/decompose-profile-modal-controller-5857031998313460612
Decompose ProfileModalController: Extract Wallet and Storage Controllers
This commit is contained in:
5 files changed
+1505
-1356
No files matched your search
@@ -0,0 +1,702 @@
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
import { PROVIDERS } from "../../services/storageService.js";
|
||||
import {
|
||||
prepareS3Connection,
|
||||
getCorsOrigins,
|
||||
} from "../../services/s3Service.js";
|
||||
import { getActiveSigner } from "../../nostr/client.js";
|
||||
import { DEFAULT_NIP07_ENCRYPTION_METHODS } from "../../nostr/nip07Permissions.js";
|
||||
|
||||
export class ProfileStorageController {
|
||||
constructor(mainController) {
|
||||
this.mainController = mainController;
|
||||
|
||||
this.storageUnlockBtn = null;
|
||||
this.storageSaveBtn = null;
|
||||
this.storageTestBtn = null;
|
||||
this.storageClearBtn = null;
|
||||
this.storageUnlockSection = null;
|
||||
this.storageFormSection = null;
|
||||
this.storageStatusText = null;
|
||||
this.storageFormStatus = null;
|
||||
|
||||
this.storageProviderInput = null;
|
||||
this.storageEndpointInput = null;
|
||||
this.storageRegionInput = null;
|
||||
this.storageAccessKeyInput = null;
|
||||
this.storageSecretKeyInput = null;
|
||||
this.storageBucketInput = null;
|
||||
this.storagePrefixInput = null;
|
||||
this.storagePrefixWarning = null;
|
||||
this.storageDefaultInput = null;
|
||||
this.storageR2Helper = null;
|
||||
this.storageS3Helper = null;
|
||||
this.storageForcePathStyleInput = null;
|
||||
this.storageForcePathStyleLabel = null;
|
||||
|
||||
this.storageUnlockFailure = null;
|
||||
}
|
||||
|
||||
cacheDomReferences() {
|
||||
this.storageUnlockBtn = document.getElementById("profileStorageUnlockBtn") || null;
|
||||
this.storageSaveBtn = document.getElementById("storageSaveBtn") || null;
|
||||
this.storageTestBtn = document.getElementById("storageTestBtn") || null;
|
||||
this.storageClearBtn = document.getElementById("storageClearBtn") || null;
|
||||
this.storageUnlockSection = document.getElementById("profileStorageUnlock") || null;
|
||||
this.storageFormSection = document.getElementById("profileStorageForm") || null;
|
||||
this.storageStatusText = document.getElementById("profileStorageStatus") || null;
|
||||
this.storageFormStatus = document.getElementById("storageFormStatus") || null;
|
||||
|
||||
this.storageProviderInput = document.getElementById("storageProvider") || null;
|
||||
this.storageEndpointInput = document.getElementById("storageEndpoint") || null;
|
||||
this.storageRegionInput = document.getElementById("storageRegion") || null;
|
||||
this.storageAccessKeyInput = document.getElementById("storageAccessKey") || null;
|
||||
this.storageSecretKeyInput = document.getElementById("storageSecretKey") || null;
|
||||
this.storageBucketInput = document.getElementById("storageBucket") || null;
|
||||
this.storagePrefixInput = document.getElementById("storagePrefix") || null;
|
||||
this.storagePrefixWarning = document.getElementById("storagePrefixWarning") || null;
|
||||
this.storageDefaultInput = document.getElementById("storageDefault") || null;
|
||||
this.storageR2Helper = document.getElementById("storageR2Helper") || null;
|
||||
this.storageS3Helper = document.getElementById("storageS3Helper") || null;
|
||||
this.storageForcePathStyleInput = document.getElementById("storageForcePathStyle") || null;
|
||||
this.storageForcePathStyleLabel = document.getElementById("storageForcePathStyleLabel") || null;
|
||||
}
|
||||
|
||||
registerEventListeners() {
|
||||
if (this.storageUnlockBtn instanceof HTMLElement) {
|
||||
this.storageUnlockBtn.addEventListener("click", () => {
|
||||
void this.handleUnlockStorage();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.storageSaveBtn instanceof HTMLElement) {
|
||||
this.storageSaveBtn.addEventListener("click", () => {
|
||||
void this.handleSaveStorage();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.storageTestBtn instanceof HTMLElement) {
|
||||
this.storageTestBtn.addEventListener("click", () => {
|
||||
void this.handleTestStorage();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.storageClearBtn instanceof HTMLElement) {
|
||||
this.storageClearBtn.addEventListener("click", () => {
|
||||
void this.handleClearStorage();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.storageProviderInput instanceof HTMLElement) {
|
||||
this.storageProviderInput.addEventListener("change", () => {
|
||||
this.updateStorageFormVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.storagePrefixInput instanceof HTMLElement) {
|
||||
this.storagePrefixInput.addEventListener("input", () => {
|
||||
this.handlePublicUrlInput();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async populateStoragePane() {
|
||||
const storageService = this.mainController.services.storageService;
|
||||
const pubkey = this.mainController.normalizeHexPubkey(
|
||||
this.mainController.getActivePubkey()
|
||||
);
|
||||
|
||||
this.handleClearStorage();
|
||||
|
||||
if (!pubkey) {
|
||||
// Not logged in
|
||||
if (this.storageUnlockSection) this.storageUnlockSection.classList.add("hidden");
|
||||
if (this.storageFormSection) this.storageFormSection.classList.add("hidden");
|
||||
if (this.storageStatusText) {
|
||||
this.storageStatusText.textContent = "Please login to manage storage.";
|
||||
this.storageStatusText.className = "text-xs text-status-danger";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let isUnlocked = storageService && storageService.masterKeys.has(pubkey);
|
||||
|
||||
// Auto-unlock
|
||||
if (!isUnlocked && storageService && !this.storageUnlockFailure) {
|
||||
const signer = getActiveSigner();
|
||||
if (
|
||||
signer &&
|
||||
(typeof signer.nip44Decrypt === "function" ||
|
||||
typeof signer.nip04Decrypt === "function")
|
||||
) {
|
||||
try {
|
||||
await storageService.unlock(pubkey, { signer });
|
||||
isUnlocked = storageService.masterKeys.has(pubkey);
|
||||
} catch (autoUnlockError) {
|
||||
devLogger.log(
|
||||
"[ProfileModal] Auto-unlock storage skipped:",
|
||||
autoUnlockError?.message || autoUnlockError
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.storageStatusText) {
|
||||
if (isUnlocked) {
|
||||
this.storageStatusText.textContent = "Unlocked";
|
||||
this.storageStatusText.className = "text-xs text-status-success";
|
||||
} else if (this.storageUnlockFailure?.message) {
|
||||
this.storageStatusText.textContent = `Locked (${this.storageUnlockFailure.message})`;
|
||||
this.storageStatusText.className = "text-xs text-status-danger";
|
||||
} else {
|
||||
this.storageStatusText.textContent = "Locked";
|
||||
this.storageStatusText.className = "text-xs text-status-warning";
|
||||
}
|
||||
}
|
||||
|
||||
if (isUnlocked) {
|
||||
if (this.storageUnlockSection)
|
||||
this.storageUnlockSection.classList.add("hidden");
|
||||
if (this.storageFormSection)
|
||||
this.storageFormSection.classList.remove("hidden");
|
||||
|
||||
try {
|
||||
const connections = await storageService.listConnections(pubkey);
|
||||
const defaultConn = connections.find((c) => c.meta?.defaultForUploads);
|
||||
const targetConn = defaultConn || connections[0];
|
||||
|
||||
if (targetConn) {
|
||||
const conn = await storageService.getConnection(
|
||||
pubkey,
|
||||
targetConn.id
|
||||
);
|
||||
if (conn) {
|
||||
this.fillStorageForm(conn);
|
||||
if (this.storageStatusText) {
|
||||
const label = conn.meta?.label || conn.provider || "S3";
|
||||
this.storageStatusText.textContent = `Unlocked (${label})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.error("Failed to load storage connections:", error);
|
||||
}
|
||||
} else {
|
||||
if (this.storageUnlockSection)
|
||||
this.storageUnlockSection.classList.remove("hidden");
|
||||
if (this.storageFormSection)
|
||||
this.storageFormSection.classList.add("hidden");
|
||||
}
|
||||
|
||||
this.updateStorageFormVisibility();
|
||||
}
|
||||
|
||||
fillStorageForm(conn) {
|
||||
if (!conn) return;
|
||||
const {
|
||||
provider,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
accountId: payloadAccountId,
|
||||
endpoint: payloadEndpoint,
|
||||
forcePathStyle: payloadForcePathStyle,
|
||||
} = conn;
|
||||
const {
|
||||
endpoint,
|
||||
region,
|
||||
bucket,
|
||||
prefix,
|
||||
defaultForUploads,
|
||||
accountId,
|
||||
forcePathStyle: metaForcePathStyle,
|
||||
} = conn.meta || {};
|
||||
|
||||
if (this.storageProviderInput)
|
||||
this.storageProviderInput.value = provider || "cloudflare_r2";
|
||||
|
||||
const resolvedEndpoint =
|
||||
endpoint || accountId || payloadAccountId || payloadEndpoint || "";
|
||||
|
||||
if (this.storageEndpointInput)
|
||||
this.storageEndpointInput.value = resolvedEndpoint;
|
||||
if (this.storageRegionInput)
|
||||
this.storageRegionInput.value = region || "auto";
|
||||
if (this.storageAccessKeyInput)
|
||||
this.storageAccessKeyInput.value = accessKeyId || "";
|
||||
if (this.storageSecretKeyInput)
|
||||
this.storageSecretKeyInput.value = secretAccessKey || "";
|
||||
if (this.storageBucketInput) this.storageBucketInput.value = bucket || "";
|
||||
if (this.storagePrefixInput) this.storagePrefixInput.value = prefix || "";
|
||||
if (this.storageDefaultInput)
|
||||
this.storageDefaultInput.checked = !!defaultForUploads;
|
||||
|
||||
if (this.storageForcePathStyleInput) {
|
||||
if (typeof payloadForcePathStyle === "boolean") {
|
||||
this.storageForcePathStyleInput.checked = payloadForcePathStyle;
|
||||
} else if (typeof metaForcePathStyle === "boolean") {
|
||||
this.storageForcePathStyleInput.checked = metaForcePathStyle;
|
||||
} else {
|
||||
this.storageForcePathStyleInput.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateStorageFormVisibility();
|
||||
this.handlePublicUrlInput();
|
||||
}
|
||||
|
||||
handlePublicUrlInput() {
|
||||
if (!this.storagePrefixInput || !this.storagePrefixWarning) return;
|
||||
this.storagePrefixWarning.textContent = "";
|
||||
this.storagePrefixWarning.classList.add("hidden");
|
||||
}
|
||||
|
||||
updateStorageFormVisibility() {
|
||||
const provider = this.storageProviderInput?.value || "cloudflare_r2";
|
||||
const isR2 = provider === "cloudflare_r2";
|
||||
|
||||
if (this.storageEndpointInput) {
|
||||
const label =
|
||||
this.storageEndpointInput.parentElement.querySelector("span");
|
||||
if (isR2) {
|
||||
if (label) label.textContent = "Cloudflare Account ID";
|
||||
this.storageEndpointInput.placeholder =
|
||||
"Account ID from Cloudflare dashboard";
|
||||
this.storageEndpointInput.parentElement.classList.remove("hidden");
|
||||
this.storageEndpointInput.type = "text";
|
||||
if (this.storagePrefixInput) {
|
||||
this.storagePrefixInput.placeholder = "https://pub-xxx.r2.dev";
|
||||
}
|
||||
} else {
|
||||
if (label) label.textContent = "Endpoint URL";
|
||||
this.storageEndpointInput.placeholder = "https://s3.example.com";
|
||||
this.storageEndpointInput.parentElement.classList.remove("hidden");
|
||||
this.storageEndpointInput.type = "url";
|
||||
if (this.storagePrefixInput) {
|
||||
this.storagePrefixInput.placeholder = "https://cdn.example.com";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.storageR2Helper) {
|
||||
if (isR2) this.storageR2Helper.classList.remove("hidden");
|
||||
else this.storageR2Helper.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (this.storageS3Helper) {
|
||||
if (!isR2) this.storageS3Helper.classList.remove("hidden");
|
||||
else this.storageS3Helper.classList.add("hidden");
|
||||
}
|
||||
|
||||
if (this.storageForcePathStyleLabel) {
|
||||
if (!isR2)
|
||||
this.storageForcePathStyleLabel.classList.remove("hidden", "flex");
|
||||
else this.storageForcePathStyleLabel.classList.add("hidden");
|
||||
|
||||
if (!isR2) {
|
||||
this.storageForcePathStyleLabel.classList.add("flex");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getStorageUnlockFailureMessage(error) {
|
||||
const code = typeof error?.code === "string" ? error.code : "";
|
||||
|
||||
switch (code) {
|
||||
case "storage-unlock-permission-denied":
|
||||
return "Storage unlock requires extension encryption permission. Approve the prompt, then retry unlock.";
|
||||
case "storage-unlock-no-decryptor":
|
||||
return "Your signer cannot decrypt storage keys. Use a signer with NIP-44 or NIP-04 decrypt support.";
|
||||
case "storage-unlock-decrypt-failed":
|
||||
return "Unable to decrypt your saved storage key. Retry unlock and confirm the active account matches.";
|
||||
default:
|
||||
return typeof error?.message === "string" && error.message.trim()
|
||||
? error.message
|
||||
: "Failed to unlock storage. Ensure your signer supports NIP-04/44.";
|
||||
}
|
||||
}
|
||||
|
||||
setStorageUnlockFailureState(error) {
|
||||
const code =
|
||||
typeof error?.code === "string"
|
||||
? error.code
|
||||
: "storage-unlock-decrypt-failed";
|
||||
const message = this.getStorageUnlockFailureMessage(error);
|
||||
|
||||
this.storageUnlockFailure = { code, message };
|
||||
|
||||
if (this.storageStatusText) {
|
||||
this.storageStatusText.textContent = `Locked (${message})`;
|
||||
this.storageStatusText.className = "text-xs text-status-danger";
|
||||
}
|
||||
|
||||
this.setStorageFormStatus(message, "error");
|
||||
}
|
||||
|
||||
clearStorageUnlockFailureState() {
|
||||
this.storageUnlockFailure = null;
|
||||
}
|
||||
|
||||
async requestStorageUnlockPermissions() {
|
||||
const client = this.mainController.services?.nostrClient;
|
||||
if (!client || typeof client.ensureExtensionPermissions !== "function") {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
return client.ensureExtensionPermissions(DEFAULT_NIP07_ENCRYPTION_METHODS, {
|
||||
context: "storage-unlock",
|
||||
statusMessage:
|
||||
"Approve the extension prompt to allow storage encryption/decryption.",
|
||||
showSpinner: true,
|
||||
});
|
||||
}
|
||||
|
||||
async handleUnlockStorage() {
|
||||
const pubkey = this.mainController.normalizeHexPubkey(
|
||||
this.mainController.getActivePubkey()
|
||||
);
|
||||
if (!pubkey) return;
|
||||
|
||||
let signer = getActiveSigner();
|
||||
if (
|
||||
!signer &&
|
||||
this.mainController.services.nostrClient &&
|
||||
typeof this.mainController.services.nostrClient
|
||||
.ensureActiveSignerForPubkey === "function"
|
||||
) {
|
||||
try {
|
||||
signer = await this.mainController.services.nostrClient.ensureActiveSignerForPubkey(
|
||||
pubkey
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[ProfileModal] Failed to resolve signer for storage unlock:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!signer) {
|
||||
this.mainController.showError("No active signer found. Please login.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
signer.pubkey &&
|
||||
this.mainController.normalizeHexPubkey(signer.pubkey) !== pubkey
|
||||
) {
|
||||
this.mainController.showError(
|
||||
`Signer account (${signer.pubkey.slice(
|
||||
0,
|
||||
8
|
||||
)}...) does not match profile (${pubkey.slice(
|
||||
0,
|
||||
8
|
||||
)}...). Please switch accounts in your extension.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const storageService = this.mainController.services.storageService;
|
||||
if (!storageService) {
|
||||
this.mainController.showError("Storage service unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
const signerType =
|
||||
typeof signer?.type === "string"
|
||||
? signer.type.trim().toLowerCase()
|
||||
: "";
|
||||
const isExtensionSigner =
|
||||
signerType === "extension" || signerType === "nip07";
|
||||
|
||||
const shouldForcePermissionRetry =
|
||||
this.storageUnlockBtn?.dataset?.retryAction === "request-permissions";
|
||||
|
||||
if (shouldForcePermissionRetry) {
|
||||
const extensionPermissionCache = this.mainController.services?.nostrClient
|
||||
?.extensionPermissionCache;
|
||||
if (extensionPermissionCache instanceof Set) {
|
||||
for (const method of DEFAULT_NIP07_ENCRYPTION_METHODS) {
|
||||
extensionPermissionCache.delete(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isExtensionSigner) {
|
||||
const permissionResult = await this.requestStorageUnlockPermissions();
|
||||
if (!permissionResult?.ok) {
|
||||
const permissionError =
|
||||
permissionResult?.error ||
|
||||
new Error("Extension permissions denied.");
|
||||
permissionError.code = "storage-unlock-permission-denied";
|
||||
this.setStorageUnlockFailureState(permissionError);
|
||||
this.mainController.showError(
|
||||
this.getStorageUnlockFailureMessage(permissionError)
|
||||
);
|
||||
if (this.storageUnlockBtn) {
|
||||
this.storageUnlockBtn.dataset.retryAction = "request-permissions";
|
||||
this.storageUnlockBtn.textContent = "Retry Permissions + Unlock";
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const hasNip44Decrypt = typeof signer?.nip44Decrypt === "function";
|
||||
const hasNip04Decrypt =
|
||||
typeof signer?.nip04Decrypt === "function" ||
|
||||
typeof signer?.decrypt === "function";
|
||||
|
||||
if (!hasNip44Decrypt && !hasNip04Decrypt) {
|
||||
const missingDecryptError = new Error(
|
||||
"This signer cannot decrypt storage keys (NIP-44/NIP-04 missing)."
|
||||
);
|
||||
missingDecryptError.code = "storage-unlock-no-decryptor";
|
||||
this.setStorageUnlockFailureState(missingDecryptError);
|
||||
this.mainController.showError(
|
||||
this.getStorageUnlockFailureMessage(missingDecryptError)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.storageUnlockBtn) {
|
||||
this.storageUnlockBtn.disabled = true;
|
||||
this.storageUnlockBtn.textContent = "Unlocking...";
|
||||
}
|
||||
|
||||
try {
|
||||
await storageService.unlock(pubkey, { signer });
|
||||
this.clearStorageUnlockFailureState();
|
||||
this.mainController.showSuccess("Storage unlocked.");
|
||||
this.populateStoragePane();
|
||||
} catch (error) {
|
||||
devLogger.error("Failed to unlock storage:", error);
|
||||
this.setStorageUnlockFailureState(error);
|
||||
this.mainController.showError(this.getStorageUnlockFailureMessage(error));
|
||||
if (this.storageUnlockBtn) {
|
||||
this.storageUnlockBtn.dataset.retryAction = "request-permissions";
|
||||
}
|
||||
} finally {
|
||||
if (this.storageUnlockBtn) {
|
||||
this.storageUnlockBtn.disabled = false;
|
||||
const shouldShowRetry =
|
||||
this.storageUnlockFailure?.code === "storage-unlock-permission-denied";
|
||||
if (shouldShowRetry) {
|
||||
this.storageUnlockBtn.textContent = "Retry Permissions + Unlock";
|
||||
this.storageUnlockBtn.dataset.retryAction = "request-permissions";
|
||||
} else {
|
||||
this.storageUnlockBtn.textContent = "Unlock Storage";
|
||||
delete this.storageUnlockBtn.dataset.retryAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleSaveStorage() {
|
||||
const pubkey = this.mainController.normalizeHexPubkey(
|
||||
this.mainController.getActivePubkey()
|
||||
);
|
||||
if (!pubkey) return;
|
||||
|
||||
const storageService = this.mainController.services.storageService;
|
||||
if (!storageService) return;
|
||||
|
||||
const provider = this.storageProviderInput?.value || "cloudflare_r2";
|
||||
let endpointOrAccount = this.storageEndpointInput?.value?.trim() || "";
|
||||
const region = this.storageRegionInput?.value?.trim() || "auto";
|
||||
const accessKeyId = this.storageAccessKeyInput?.value?.trim() || "";
|
||||
const secretAccessKey = this.storageSecretKeyInput?.value?.trim() || "";
|
||||
const bucket = this.storageBucketInput?.value?.trim() || "";
|
||||
const prefix = this.storagePrefixInput?.value?.trim() || "";
|
||||
const isDefault = this.storageDefaultInput?.checked || false;
|
||||
|
||||
if (!accessKeyId || !secretAccessKey || !bucket || !endpointOrAccount) {
|
||||
this.setStorageFormStatus("Please fill in all required fields.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
provider === "cloudflare_r2" &&
|
||||
(prefix.includes(".r2.cloudflarestorage.com") ||
|
||||
prefix.includes(".s3.") ||
|
||||
prefix.includes(".amazonaws.com"))
|
||||
) {
|
||||
this.setStorageFormStatus(
|
||||
"Invalid Public URL. Please use your R2.dev or custom domain.",
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let publicBaseUrl = "";
|
||||
let forcePathStyle = false;
|
||||
if (provider === PROVIDERS.GENERIC) {
|
||||
forcePathStyle = this.storageForcePathStyleInput?.checked ?? true;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
provider,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
};
|
||||
|
||||
if (provider === "cloudflare_r2") {
|
||||
payload.accountId = endpointOrAccount;
|
||||
} else {
|
||||
try {
|
||||
const normalized = await prepareS3Connection({
|
||||
endpoint: endpointOrAccount,
|
||||
region,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
bucket,
|
||||
forcePathStyle,
|
||||
origins: getCorsOrigins(),
|
||||
});
|
||||
endpointOrAccount = normalized.endpoint;
|
||||
publicBaseUrl = normalized.publicBaseUrl;
|
||||
forcePathStyle = normalized.forcePathStyle;
|
||||
} catch (error) {
|
||||
devLogger.error("Failed to validate S3 connection:", error);
|
||||
this.setStorageFormStatus(
|
||||
error?.message || "Invalid S3 configuration.",
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
payload.endpoint = endpointOrAccount;
|
||||
payload.forcePathStyle = forcePathStyle;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
provider,
|
||||
region,
|
||||
bucket,
|
||||
prefix,
|
||||
defaultForUploads: isDefault,
|
||||
label: `${provider} - ${bucket}`,
|
||||
endpoint: provider === "cloudflare_r2" ? undefined : endpointOrAccount,
|
||||
};
|
||||
|
||||
if (provider === "cloudflare_r2") {
|
||||
meta.accountId = endpointOrAccount;
|
||||
meta.publicBaseUrl = prefix;
|
||||
meta.baseDomain = prefix;
|
||||
} else {
|
||||
meta.publicBaseUrl = publicBaseUrl;
|
||||
meta.baseDomain = publicBaseUrl;
|
||||
meta.forcePathStyle = forcePathStyle;
|
||||
}
|
||||
|
||||
this.setStorageFormStatus("Saving...", "info");
|
||||
|
||||
try {
|
||||
await storageService.saveConnection(pubkey, "default", payload, meta);
|
||||
this.setStorageFormStatus("Connection saved.", "success");
|
||||
this.mainController.showSuccess("Storage connection saved.");
|
||||
} catch (error) {
|
||||
devLogger.error("Failed to save connection:", error);
|
||||
this.setStorageFormStatus("Failed to save connection.", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async handleTestStorage() {
|
||||
const pubkey = this.mainController.normalizeHexPubkey(
|
||||
this.mainController.getActivePubkey()
|
||||
);
|
||||
if (!pubkey) return;
|
||||
|
||||
const storageService = this.mainController.services.storageService;
|
||||
if (!storageService) {
|
||||
this.setStorageFormStatus("Storage service unavailable.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = this.storageProviderInput?.value || "cloudflare_r2";
|
||||
const endpointOrAccount = this.storageEndpointInput?.value?.trim() || "";
|
||||
const region = this.storageRegionInput?.value?.trim() || "auto";
|
||||
const accessKeyId = this.storageAccessKeyInput?.value?.trim() || "";
|
||||
const secretAccessKey = this.storageSecretKeyInput?.value?.trim() || "";
|
||||
const bucket = this.storageBucketInput?.value?.trim() || "";
|
||||
|
||||
const forcePathStyle =
|
||||
provider === PROVIDERS.GENERIC
|
||||
? this.storageForcePathStyleInput?.checked ?? true
|
||||
: false;
|
||||
|
||||
const publicBaseUrl = this.storagePrefixInput?.value?.trim() || "";
|
||||
|
||||
if (!accessKeyId || !secretAccessKey || !endpointOrAccount) {
|
||||
this.setStorageFormStatus("Missing credentials for test.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
provider === "cloudflare_r2" &&
|
||||
(publicBaseUrl.includes(".r2.cloudflarestorage.com") ||
|
||||
publicBaseUrl.includes(".s3.") ||
|
||||
publicBaseUrl.includes(".amazonaws.com"))
|
||||
) {
|
||||
this.setStorageFormStatus(
|
||||
"Invalid Public URL. Please use your R2.dev or custom domain.",
|
||||
"error"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setStorageFormStatus("Testing connection...", "info");
|
||||
|
||||
const config = {
|
||||
provider,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
region,
|
||||
bucket,
|
||||
};
|
||||
|
||||
if (provider === "cloudflare_r2") {
|
||||
config.accountId = endpointOrAccount;
|
||||
config.publicBaseUrl = publicBaseUrl;
|
||||
config.baseDomain = publicBaseUrl;
|
||||
} else {
|
||||
config.endpoint = endpointOrAccount;
|
||||
config.forcePathStyle = forcePathStyle;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await storageService.testAccess(provider, config);
|
||||
if (result.success) {
|
||||
this.setStorageFormStatus(
|
||||
result.message || "Connection Verified!",
|
||||
"success"
|
||||
);
|
||||
} else {
|
||||
this.setStorageFormStatus(`Test Failed: ${result.error}`, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
this.setStorageFormStatus(`Test Error: ${error.message}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
handleClearStorage() {
|
||||
this.storageEndpointInput.value = "";
|
||||
this.storageRegionInput.value = "auto";
|
||||
this.storageAccessKeyInput.value = "";
|
||||
this.storageSecretKeyInput.value = "";
|
||||
this.storageBucketInput.value = "";
|
||||
this.storagePrefixInput.value = "";
|
||||
this.storageDefaultInput.checked = false;
|
||||
this.storageProviderInput.value = "cloudflare_r2";
|
||||
if (this.storageForcePathStyleInput) {
|
||||
this.storageForcePathStyleInput.checked = true;
|
||||
}
|
||||
this.updateStorageFormVisibility();
|
||||
this.setStorageFormStatus("", "info");
|
||||
}
|
||||
|
||||
setStorageFormStatus(message, variant = "info") {
|
||||
if (!this.storageFormStatus) return;
|
||||
this.storageFormStatus.textContent = message;
|
||||
this.storageFormStatus.className = `text-sm text-status-${variant}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,758 @@
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
import { NWC_URI_SCHEME } from "../profileModalContract.js";
|
||||
|
||||
const noop = () => {};
|
||||
const SECRET_PLACEHOLDER = "*****";
|
||||
|
||||
export class ProfileWalletController {
|
||||
constructor(mainController) {
|
||||
this.mainController = mainController;
|
||||
|
||||
this.walletUriInput = null;
|
||||
this.walletDefaultZapInput = null;
|
||||
this.walletSaveButton = null;
|
||||
this.walletTestButton = null;
|
||||
this.walletDisconnectButton = null;
|
||||
this.walletStatusText = null;
|
||||
this.walletPane = null;
|
||||
}
|
||||
|
||||
cacheDomReferences() {
|
||||
this.walletUriInput = document.getElementById("profileWalletUri") || null;
|
||||
this.walletDefaultZapInput = document.getElementById("profileWalletDefaultZap") || null;
|
||||
this.walletSaveButton = document.getElementById("profileWalletSave") || null;
|
||||
this.walletTestButton = document.getElementById("profileWalletTest") || null;
|
||||
this.walletDisconnectButton = document.getElementById("profileWalletDisconnect") || null;
|
||||
this.walletStatusText = document.getElementById("profileWalletStatus") || null;
|
||||
this.walletPane = document.getElementById("profilePaneWallet") || null;
|
||||
|
||||
// Backwards compatibility alias if needed by tests/external code
|
||||
this.mainController.profileWalletStatusText = this.walletStatusText;
|
||||
}
|
||||
|
||||
registerEventListeners() {
|
||||
if (this.walletUriInput instanceof HTMLElement) {
|
||||
this.walletUriInput.addEventListener("focus", () => {
|
||||
this.revealSecretInputValue(this.walletUriInput);
|
||||
});
|
||||
this.walletUriInput.addEventListener("blur", () => {
|
||||
this.handleSecretInputBlur(this.walletUriInput);
|
||||
this.applyWalletControlState();
|
||||
});
|
||||
this.walletUriInput.addEventListener("input", () => {
|
||||
this.handleSecretInputChange(this.walletUriInput);
|
||||
this.applyWalletControlState();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.walletDefaultZapInput instanceof HTMLElement) {
|
||||
this.walletDefaultZapInput.addEventListener("input", () => {
|
||||
this.applyWalletControlState();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.walletSaveButton instanceof HTMLElement) {
|
||||
this.walletSaveButton.addEventListener("click", () => {
|
||||
void this.handleWalletSave();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.walletTestButton instanceof HTMLElement) {
|
||||
this.walletTestButton.addEventListener("click", () => {
|
||||
void this.handleWalletTest();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.walletDisconnectButton instanceof HTMLElement) {
|
||||
this.walletDisconnectButton.addEventListener("click", () => {
|
||||
void this.handleWalletDisconnect();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
applyWalletControlState() {
|
||||
const hasActive = Boolean(
|
||||
this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey())
|
||||
);
|
||||
const busy = this.isWalletBusy();
|
||||
const uriValue = this.getSecretInputValue(this.walletUriInput);
|
||||
const hasUri = uriValue.length > 0;
|
||||
|
||||
const applyDisabledState = (element, disabled) => {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
if ("disabled" in element) {
|
||||
element.disabled = disabled;
|
||||
}
|
||||
if (disabled) {
|
||||
element.setAttribute("aria-disabled", "true");
|
||||
} else {
|
||||
element.removeAttribute("aria-disabled");
|
||||
}
|
||||
};
|
||||
|
||||
applyDisabledState(this.walletUriInput, busy || !hasActive);
|
||||
applyDisabledState(this.walletDefaultZapInput, busy || !hasActive);
|
||||
applyDisabledState(this.walletSaveButton, busy || !hasActive);
|
||||
|
||||
const testDisabled = busy || !hasActive || !hasUri;
|
||||
applyDisabledState(this.walletTestButton, testDisabled);
|
||||
|
||||
const disconnectDisabled = busy || !hasActive || !hasUri;
|
||||
applyDisabledState(this.walletDisconnectButton, disconnectDisabled);
|
||||
if (this.walletDisconnectButton instanceof HTMLElement) {
|
||||
this.walletDisconnectButton.classList.toggle("hidden", !hasUri);
|
||||
if (!hasUri) {
|
||||
this.walletDisconnectButton.setAttribute("aria-hidden", "true");
|
||||
} else {
|
||||
this.walletDisconnectButton.removeAttribute("aria-hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateWalletStatus(message, variant = "info") {
|
||||
if (!(this.walletStatusText instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = this.walletStatusText;
|
||||
const variants = {
|
||||
success: "text-status-success",
|
||||
error: "text-status-danger",
|
||||
info: "text-status-info",
|
||||
neutral: "text-status-neutral",
|
||||
};
|
||||
|
||||
element.classList.remove(
|
||||
"text-status-info",
|
||||
"text-status-success",
|
||||
"text-status-danger",
|
||||
"text-status-neutral",
|
||||
);
|
||||
const variantClass = variants[variant] || variants.neutral;
|
||||
element.classList.add(variantClass);
|
||||
element.textContent = message || "";
|
||||
}
|
||||
|
||||
refreshWalletPaneState() {
|
||||
const hasActive = Boolean(
|
||||
this.mainController.normalizeHexPubkey(this.mainController.getActivePubkey())
|
||||
);
|
||||
if (!hasActive) {
|
||||
this.setSecretInputValue(this.walletUriInput, "");
|
||||
if (this.walletDefaultZapInput && "value" in this.walletDefaultZapInput) {
|
||||
try {
|
||||
this.walletDefaultZapInput.value = "";
|
||||
} catch (error) {
|
||||
if (this.walletDefaultZapInput instanceof HTMLElement) {
|
||||
this.walletDefaultZapInput.setAttribute("data-value", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
this.updateWalletStatus("Sign in to connect a wallet.", "info");
|
||||
this.applyWalletControlState();
|
||||
return;
|
||||
}
|
||||
|
||||
let settings = this.mainController.services.nwcSettings.getActiveNwcSettings();
|
||||
if (!settings || typeof settings !== "object") {
|
||||
settings = this.mainController.services.nwcSettings.createDefaultNwcSettings();
|
||||
}
|
||||
this.setSecretInputValue(this.walletUriInput, settings.nwcUri || "");
|
||||
if (this.walletDefaultZapInput && "value" in this.walletDefaultZapInput) {
|
||||
const defaultZapValue =
|
||||
settings.defaultZap === null || settings.defaultZap === undefined
|
||||
? ""
|
||||
: String(settings.defaultZap);
|
||||
try {
|
||||
this.walletDefaultZapInput.value = defaultZapValue;
|
||||
} catch (error) {
|
||||
if (this.walletDefaultZapInput instanceof HTMLElement) {
|
||||
this.walletDefaultZapInput.setAttribute("data-value", defaultZapValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.nwcUri) {
|
||||
this.updateWalletStatus(
|
||||
"Wallet connected via Nostr Wallet Connect.",
|
||||
"success",
|
||||
);
|
||||
} else {
|
||||
this.updateWalletStatus("No wallet connected yet.", "info");
|
||||
}
|
||||
|
||||
this.applyWalletControlState();
|
||||
}
|
||||
|
||||
isSecretInputElement(element) {
|
||||
if (!element || typeof element !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
typeof HTMLInputElement !== "undefined" &&
|
||||
element instanceof HTMLInputElement
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return typeof element.value === "string";
|
||||
}
|
||||
|
||||
sanitizeSecretValue(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
getSecretInputValue(element) {
|
||||
if (!this.isSecretInputElement(element)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const placeholder =
|
||||
typeof element.dataset?.secretPlaceholder === "string"
|
||||
? element.dataset.secretPlaceholder
|
||||
: SECRET_PLACEHOLDER;
|
||||
const stored = this.sanitizeSecretValue(
|
||||
typeof element.dataset?.secretValue === "string"
|
||||
? element.dataset.secretValue
|
||||
: ""
|
||||
);
|
||||
const raw = this.sanitizeSecretValue(element.value);
|
||||
const isMasked = element.dataset?.secretMasked === "true";
|
||||
|
||||
if (isMasked && placeholder && raw === placeholder) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
if (!raw && isMasked) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
setSecretInputValue(element, value) {
|
||||
if (!this.isSecretInputElement(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitized = this.sanitizeSecretValue(value);
|
||||
if (!sanitized) {
|
||||
if (element.dataset) {
|
||||
delete element.dataset.secretValue;
|
||||
delete element.dataset.secretMasked;
|
||||
delete element.dataset.secretPlaceholder;
|
||||
}
|
||||
element.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const placeholder = SECRET_PLACEHOLDER;
|
||||
if (element.dataset) {
|
||||
element.dataset.secretValue = sanitized;
|
||||
element.dataset.secretPlaceholder = placeholder;
|
||||
element.dataset.secretMasked = "true";
|
||||
}
|
||||
element.value = placeholder;
|
||||
}
|
||||
|
||||
revealSecretInputValue(element) {
|
||||
if (!this.isSecretInputElement(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stored = this.sanitizeSecretValue(
|
||||
typeof element.dataset?.secretValue === "string"
|
||||
? element.dataset.secretValue
|
||||
: ""
|
||||
);
|
||||
|
||||
if (!stored) {
|
||||
if (element.dataset) {
|
||||
delete element.dataset.secretMasked;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.dataset) {
|
||||
element.dataset.secretMasked = "false";
|
||||
}
|
||||
element.value = stored;
|
||||
try {
|
||||
if (typeof element.setSelectionRange === "function") {
|
||||
const length = stored.length;
|
||||
element.setSelectionRange(length, length);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore selection errors on unsupported input types.
|
||||
}
|
||||
}
|
||||
|
||||
handleSecretInputChange(element) {
|
||||
if (!this.isSecretInputElement(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this.sanitizeSecretValue(element.value);
|
||||
if (!value) {
|
||||
if (element.dataset) {
|
||||
element.dataset.secretMasked = "false";
|
||||
delete element.dataset.secretValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.dataset) {
|
||||
element.dataset.secretValue = value;
|
||||
element.dataset.secretMasked = "false";
|
||||
if (!element.dataset.secretPlaceholder) {
|
||||
element.dataset.secretPlaceholder = SECRET_PLACEHOLDER;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSecretInputBlur(element) {
|
||||
if (!this.isSecretInputElement(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this.sanitizeSecretValue(this.getSecretInputValue(element));
|
||||
if (!value) {
|
||||
if (element.dataset) {
|
||||
delete element.dataset.secretValue;
|
||||
delete element.dataset.secretMasked;
|
||||
delete element.dataset.secretPlaceholder;
|
||||
}
|
||||
element.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.dataset) {
|
||||
element.dataset.secretValue = value;
|
||||
element.dataset.secretPlaceholder =
|
||||
element.dataset.secretPlaceholder || SECRET_PLACEHOLDER;
|
||||
element.dataset.secretMasked = "true";
|
||||
}
|
||||
element.value = element.dataset?.secretPlaceholder || SECRET_PLACEHOLDER;
|
||||
}
|
||||
|
||||
getWalletFormValues() {
|
||||
const uri = this.getSecretInputValue(this.walletUriInput);
|
||||
const defaultZapRaw =
|
||||
typeof this.walletDefaultZapInput?.value === "string"
|
||||
? this.walletDefaultZapInput.value.trim()
|
||||
: "";
|
||||
|
||||
if (defaultZapRaw) {
|
||||
const numeric = Number(defaultZapRaw);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return { uri, error: "Default zap amount must be a number." };
|
||||
}
|
||||
const rounded = Math.round(numeric);
|
||||
if (!Number.isFinite(rounded) || rounded < 0) {
|
||||
return {
|
||||
uri,
|
||||
error: "Default zap amount must be a positive whole number.",
|
||||
};
|
||||
}
|
||||
const clamped = Math.min(this.mainController.maxWalletDefaultZap, rounded);
|
||||
return { uri, defaultZap: clamped };
|
||||
}
|
||||
|
||||
return { uri, defaultZap: null };
|
||||
}
|
||||
|
||||
validateWalletUri(uri, { requireValue = false } = {}) {
|
||||
const value = typeof uri === "string" ? uri.trim() : "";
|
||||
if (!value) {
|
||||
if (requireValue) {
|
||||
return {
|
||||
valid: false,
|
||||
sanitized: "",
|
||||
message: "Enter a wallet connect URI before continuing.",
|
||||
};
|
||||
}
|
||||
return { valid: true, sanitized: "" };
|
||||
}
|
||||
|
||||
if (!value.toLowerCase().startsWith(NWC_URI_SCHEME)) {
|
||||
return {
|
||||
valid: false,
|
||||
sanitized: value,
|
||||
message: `Wallet URI must start with ${NWC_URI_SCHEME}.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true, sanitized: value };
|
||||
}
|
||||
|
||||
async handleWalletSave() {
|
||||
const { uri, defaultZap, error } = this.getWalletFormValues();
|
||||
const context = {
|
||||
uri,
|
||||
defaultZap: defaultZap ?? null,
|
||||
sanitizedUri: null,
|
||||
success: false,
|
||||
reason: null,
|
||||
error: error || null,
|
||||
status: null,
|
||||
variant: null,
|
||||
};
|
||||
|
||||
if (this.isWalletBusy()) {
|
||||
context.reason = "busy";
|
||||
this.mainController.callbacks.onWalletSave(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.updateWalletStatus(error, "error");
|
||||
this.mainController.showError(error);
|
||||
context.reason = "invalid-default-zap";
|
||||
if (this.walletDefaultZapInput instanceof HTMLElement) {
|
||||
this.walletDefaultZapInput.focus();
|
||||
}
|
||||
this.mainController.callbacks.onWalletSave(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
|
||||
const { valid, sanitized, message } = this.validateWalletUri(uri);
|
||||
context.sanitizedUri = sanitized;
|
||||
if (!valid) {
|
||||
this.updateWalletStatus(message, "error");
|
||||
this.mainController.showError(message);
|
||||
context.reason = "invalid-uri";
|
||||
context.error = message;
|
||||
if (this.walletUriInput instanceof HTMLElement) {
|
||||
this.walletUriInput.focus();
|
||||
}
|
||||
this.mainController.callbacks.onWalletSave(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
|
||||
const normalizedActive = this.mainController.normalizeHexPubkey(
|
||||
this.mainController.getActivePubkey()
|
||||
);
|
||||
if (!normalizedActive) {
|
||||
const loginMessage = "Sign in to save wallet settings.";
|
||||
this.updateWalletStatus(loginMessage, "error");
|
||||
this.mainController.showError(loginMessage);
|
||||
context.reason = "no-active-pubkey";
|
||||
context.error = loginMessage;
|
||||
this.mainController.callbacks.onWalletSave(context, this.mainController);
|
||||
return context;
|
||||
}
|
||||
|
||||
this.setWalletPaneBusy(true);
|
||||
let finalStatus = null;
|
||||
let finalVariant = "info";
|
||||
try {
|
||||
const persistResult = await this.persistWalletSettings({
|
||||
nwcUri: sanitized,
|
||||
defaultZap,
|
||||
activePubkey: normalizedActive,
|
||||
});
|
||||
context.persistResult = persistResult || null;
|
||||
|
||||
if (sanitized) {
|
||||
finalStatus = "Wallet settings saved.";
|
||||
finalVariant = "success";
|
||||
this.mainController.showSuccess("Wallet settings saved.");
|
||||
context.reason = "saved";
|
||||
} else {
|
||||
finalStatus = "Wallet connection removed.";
|
||||
finalVariant = "info";
|
||||
this.mainController.showStatus("Wallet connection removed.");
|
||||
context.reason = "cleared";
|
||||
}
|
||||
context.success = true;
|
||||
} catch (error) {
|
||||
const fallbackMessage = "Failed to save wallet settings.";
|
||||
const detail =
|
||||
error && typeof error.message === "string" && error.message.trim()
|
||||
? error.message.trim()
|
||||
: fallbackMessage;
|
||||
finalStatus = detail;
|
||||
finalVariant = "error";
|
||||
context.error = detail;
|
||||
context.reason = error?.code || "service-error";
|
||||
this.mainController.showError(detail);
|
||||
} finally {
|
||||
this.setWalletPaneBusy(false);
|
||||
this.refreshWalletPaneState();
|
||||
if (finalStatus) {
|
||||
this.updateWalletStatus(finalStatus, finalVariant);
|
||||
}
|
||||
context.status = finalStatus;
|
||||
context.variant = finalVariant;
|
||||
this.mainController.callbacks.onWalletSave(context, this.mainController);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
async handleWalletTest() {
|
||||
const { uri, defaultZap, error } = this.getWalletFormValues();
|
||||
const context = {
|
||||
uri,
|
||||
defaultZap: defaultZap ?? null,
|
||||
sanitizedUri: null,
|
||||
success: false,
|
||||
reason: null,
|
||||
error: error || null,
|
||||
status: null,
|
||||
variant: null,
|
||||
result: null,
|
||||
};
|
||||
|
||||
if (this.isWalletBusy()) {
|
||||
context.reason = "busy";
|
||||
this.mainController.callbacks.onWalletTest(context, this.mainController);
|
||||
return context.result;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.updateWalletStatus(error, "error");
|
||||
this.mainController.showError(error);
|
||||
context.reason = "invalid-default-zap";
|
||||
if (this.walletDefaultZapInput instanceof HTMLElement) {
|
||||
this.walletDefaultZapInput.focus();
|
||||
}
|
||||
this.mainController.callbacks.onWalletTest(context, this.mainController);
|
||||
return context.result;
|
||||
}
|
||||
|
||||
const { valid, sanitized, message } = this.validateWalletUri(uri, {
|
||||
requireValue: true,
|
||||
});
|
||||
context.sanitizedUri = sanitized;
|
||||
if (!valid) {
|
||||
this.updateWalletStatus(message, "error");
|
||||
this.mainController.showError(message);
|
||||
context.reason = "invalid-uri";
|
||||
context.error = message;
|
||||
if (this.walletUriInput instanceof HTMLElement) {
|
||||
this.walletUriInput.focus();
|
||||
}
|
||||
this.mainController.callbacks.onWalletTest(context, this.mainController);
|
||||
return context.result;
|
||||
}
|
||||
|
||||
const normalizedActive = this.mainController.normalizeHexPubkey(
|
||||
this.mainController.getActivePubkey()
|
||||
);
|
||||
if (!normalizedActive) {
|
||||
const loginMessage = "Sign in to test your wallet connection.";
|
||||
this.updateWalletStatus(loginMessage, "error");
|
||||
this.mainController.showError(loginMessage);
|
||||
context.reason = "no-active-pubkey";
|
||||
context.error = loginMessage;
|
||||
this.mainController.callbacks.onWalletTest(context, this.mainController);
|
||||
return context.result;
|
||||
}
|
||||
|
||||
this.setWalletPaneBusy(true);
|
||||
let finalStatus = null;
|
||||
let finalVariant = "info";
|
||||
try {
|
||||
const result = await this.testWalletConnection({
|
||||
nwcUri: sanitized,
|
||||
defaultZap,
|
||||
activePubkey: normalizedActive,
|
||||
});
|
||||
finalStatus = "Wallet connection confirmed.";
|
||||
finalVariant = "success";
|
||||
this.mainController.showSuccess("Wallet connection confirmed.");
|
||||
context.result = result;
|
||||
context.success = true;
|
||||
context.reason = "tested";
|
||||
|
||||
let currentSettings = this.mainController.services.nwcSettings.getActiveNwcSettings();
|
||||
if (!currentSettings || typeof currentSettings !== "object") {
|
||||
currentSettings = this.mainController.services.nwcSettings.createDefaultNwcSettings();
|
||||
}
|
||||
if (currentSettings.nwcUri === sanitized) {
|
||||
await this.persistWalletSettings({
|
||||
lastChecked: Date.now(),
|
||||
activePubkey: normalizedActive,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const fallbackMessage = "Failed to reach wallet.";
|
||||
const detail =
|
||||
error && typeof error.message === "string" && error.message.trim()
|
||||
? error.message.trim()
|
||||
: fallbackMessage;
|
||||
finalStatus = detail;
|
||||
finalVariant = "error";
|
||||
context.error = detail;
|
||||
context.reason = error?.code || "service-error";
|
||||
this.mainController.showError(detail);
|
||||
} finally {
|
||||
this.setWalletPaneBusy(false);
|
||||
this.refreshWalletPaneState();
|
||||
if (finalStatus) {
|
||||
this.updateWalletStatus(finalStatus, finalVariant);
|
||||
}
|
||||
context.status = finalStatus;
|
||||
context.variant = finalVariant;
|
||||
this.mainController.callbacks.onWalletTest(context, this.mainController);
|
||||
}
|
||||
|
||||
return context.result;
|
||||
}
|
||||
|
||||
async handleWalletDisconnect() {
|
||||
const context = {
|
||||
success: false,
|
||||
reason: null,
|
||||
error: null,
|
||||
status: null,
|
||||
variant: null,
|
||||
};
|
||||
|
||||
if (this.isWalletBusy()) {
|
||||
context.reason = "busy";
|
||||
this.mainController.callbacks.onWalletDisconnect(
|
||||
context,
|
||||
this.mainController
|
||||
);
|
||||
return context;
|
||||
}
|
||||
|
||||
const normalizedActive = this.mainController.normalizeHexPubkey(
|
||||
this.mainController.getActivePubkey()
|
||||
);
|
||||
if (!normalizedActive) {
|
||||
const loginMessage = "Sign in to disconnect your wallet.";
|
||||
this.updateWalletStatus(loginMessage, "error");
|
||||
this.mainController.showError(loginMessage);
|
||||
context.reason = "no-active-pubkey";
|
||||
context.error = loginMessage;
|
||||
this.mainController.callbacks.onWalletDisconnect(
|
||||
context,
|
||||
this.mainController
|
||||
);
|
||||
return context;
|
||||
}
|
||||
|
||||
this.setWalletPaneBusy(true);
|
||||
let finalStatus = null;
|
||||
let finalVariant = "info";
|
||||
try {
|
||||
const disconnectResult = await this.disconnectWallet({
|
||||
activePubkey: normalizedActive,
|
||||
});
|
||||
context.result = disconnectResult || null;
|
||||
finalStatus = "Wallet disconnected.";
|
||||
this.mainController.showStatus("Wallet disconnected.");
|
||||
context.success = true;
|
||||
context.reason = "disconnected";
|
||||
} catch (error) {
|
||||
const fallbackMessage = "Failed to disconnect wallet.";
|
||||
const detail =
|
||||
error && typeof error.message === "string" && error.message.trim()
|
||||
? error.message.trim()
|
||||
: fallbackMessage;
|
||||
finalStatus = detail;
|
||||
finalVariant = "error";
|
||||
context.error = detail;
|
||||
context.reason = error?.code || "service-error";
|
||||
this.mainController.showError(detail);
|
||||
} finally {
|
||||
this.setWalletPaneBusy(false);
|
||||
this.refreshWalletPaneState();
|
||||
if (finalStatus) {
|
||||
this.updateWalletStatus(finalStatus, finalVariant);
|
||||
}
|
||||
context.status = finalStatus;
|
||||
context.variant = finalVariant;
|
||||
this.mainController.callbacks.onWalletDisconnect(
|
||||
context,
|
||||
this.mainController
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
isWalletBusy() {
|
||||
return Boolean(this.mainController.state.getWalletBusy());
|
||||
}
|
||||
|
||||
setWalletPaneBusy(isBusy) {
|
||||
const result = this.mainController.state.setWalletBusy(Boolean(isBusy));
|
||||
if (this.walletPane instanceof HTMLElement) {
|
||||
this.walletPane.setAttribute(
|
||||
"aria-busy",
|
||||
this.isWalletBusy() ? "true" : "false"
|
||||
);
|
||||
}
|
||||
this.applyWalletControlState();
|
||||
return result;
|
||||
}
|
||||
|
||||
async persistWalletSettings({
|
||||
nwcUri,
|
||||
defaultZap,
|
||||
lastChecked,
|
||||
activePubkey,
|
||||
} = {}) {
|
||||
const callback = this.mainController.callbacks.onWalletPersist;
|
||||
if (callback && callback !== noop) {
|
||||
const result = await callback({
|
||||
controller: this.mainController,
|
||||
nwcUri,
|
||||
defaultZap,
|
||||
lastChecked,
|
||||
activePubkey,
|
||||
});
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return this.mainController.services.nwcSettings.handleProfileWalletPersist({
|
||||
nwcUri,
|
||||
defaultZap,
|
||||
lastChecked,
|
||||
});
|
||||
}
|
||||
|
||||
async testWalletConnection({ nwcUri, defaultZap, activePubkey } = {}) {
|
||||
const callback = this.mainController.callbacks.onWalletTestRequest;
|
||||
if (callback && callback !== noop) {
|
||||
const result = await callback({
|
||||
controller: this.mainController,
|
||||
nwcUri,
|
||||
defaultZap,
|
||||
activePubkey,
|
||||
});
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return this.mainController.services.nwcSettings.ensureWallet({
|
||||
nwcUri,
|
||||
defaultZap,
|
||||
});
|
||||
}
|
||||
|
||||
async disconnectWallet({ activePubkey } = {}) {
|
||||
const callback = this.mainController.callbacks.onWalletDisconnectRequest;
|
||||
if (callback && callback !== noop) {
|
||||
const result = await callback({
|
||||
controller: this.mainController,
|
||||
activePubkey,
|
||||
});
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return this.mainController.services.nwcSettings.updateActiveNwcSettings(
|
||||
this.mainController.services.nwcSettings.createDefaultNwcSettings()
|
||||
);
|
||||
}
|
||||
}
|
||||
+20
-1331
File diff suppressed because it is too large.
Load diff
@@ -26,7 +26,7 @@ const GROWTH_MARGIN = 50; // lines — allowlisted files may grow by this much f
|
||||
// These files are known to be oversized and are decomposition targets.
|
||||
// They must NOT grow beyond (recorded + GROWTH_MARGIN).
|
||||
const GRANDFATHERED = {
|
||||
"js/ui/profileModalController.js": 13275,
|
||||
"js/ui/profileModalController.js": 11963,
|
||||
"js/ui/components/VideoModal.js": 6284,
|
||||
"js/app.js": 5602,
|
||||
"js/channelProfile.js": 5529,
|
||||
|
||||
@@ -634,46 +634,46 @@ for (const _ of [0]) {
|
||||
defaultZap: 21,
|
||||
});
|
||||
|
||||
controller.refreshWalletPaneState();
|
||||
controller.walletController.refreshWalletPaneState();
|
||||
|
||||
assert.ok(
|
||||
controller.walletUriInput instanceof window.HTMLElement,
|
||||
controller.walletController.walletUriInput instanceof window.HTMLElement,
|
||||
'wallet URI input should exist',
|
||||
);
|
||||
assert.equal(controller.walletUriInput.value, '*****');
|
||||
assert.equal(controller.walletUriInput.dataset.secretValue, sampleUri);
|
||||
assert.equal(controller.walletController.walletUriInput.value, '*****');
|
||||
assert.equal(controller.walletController.walletUriInput.dataset.secretValue, sampleUri);
|
||||
assert.equal(
|
||||
controller.walletDisconnectButton?.classList.contains('hidden'),
|
||||
controller.walletController.walletDisconnectButton?.classList.contains('hidden'),
|
||||
false,
|
||||
'disconnect button should remain visible when a URI exists',
|
||||
);
|
||||
|
||||
const formValues = controller.getWalletFormValues();
|
||||
const formValues = controller.walletController.getWalletFormValues();
|
||||
assert.equal(formValues.uri, sampleUri);
|
||||
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('focus'));
|
||||
assert.equal(controller.walletUriInput.value, sampleUri);
|
||||
controller.walletController.walletUriInput.dispatchEvent(new window.Event('focus'));
|
||||
assert.equal(controller.walletController.walletUriInput.value, sampleUri);
|
||||
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('blur'));
|
||||
assert.equal(controller.walletUriInput.value, '*****');
|
||||
controller.walletController.walletUriInput.dispatchEvent(new window.Event('blur'));
|
||||
assert.equal(controller.walletController.walletUriInput.value, '*****');
|
||||
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('focus'));
|
||||
controller.walletController.walletUriInput.dispatchEvent(new window.Event('focus'));
|
||||
const updatedUri = `${sampleUri}&name=bitvid`;
|
||||
controller.walletUriInput.value = updatedUri;
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('input'));
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('blur'));
|
||||
controller.walletController.walletUriInput.value = updatedUri;
|
||||
controller.walletController.walletUriInput.dispatchEvent(new window.Event('input'));
|
||||
controller.walletController.walletUriInput.dispatchEvent(new window.Event('blur'));
|
||||
|
||||
const updatedValues = controller.getWalletFormValues();
|
||||
const updatedValues = controller.walletController.getWalletFormValues();
|
||||
assert.equal(updatedValues.uri, updatedUri);
|
||||
assert.equal(controller.walletUriInput.value, '*****');
|
||||
assert.equal(controller.walletController.walletUriInput.value, '*****');
|
||||
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('focus'));
|
||||
controller.walletUriInput.value = '';
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('input'));
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('blur'));
|
||||
controller.walletController.walletUriInput.dispatchEvent(new window.Event('focus'));
|
||||
controller.walletController.walletUriInput.value = '';
|
||||
controller.walletController.walletUriInput.dispatchEvent(new window.Event('input'));
|
||||
controller.walletController.walletUriInput.dispatchEvent(new window.Event('blur'));
|
||||
|
||||
assert.equal(controller.walletUriInput.value, '');
|
||||
assert.equal(controller.walletUriInput.dataset.secretValue, undefined);
|
||||
assert.equal(controller.walletController.walletUriInput.value, '');
|
||||
assert.equal(controller.walletController.walletUriInput.dataset.secretValue, undefined);
|
||||
|
||||
t.after(() => {
|
||||
try {
|
||||
@@ -996,7 +996,7 @@ test('load() injects markup and caches expected elements', async () => {
|
||||
assert.ok(controller.panes.account instanceof window.HTMLElement);
|
||||
assert.ok(controller.relayList instanceof window.HTMLElement);
|
||||
assert.ok(controller.blockList instanceof window.HTMLElement);
|
||||
assert.ok(controller.walletStatusText instanceof window.HTMLElement);
|
||||
assert.ok(controller.walletController.walletStatusText instanceof window.HTMLElement);
|
||||
});
|
||||
|
||||
test('show()/hide() toggle panes, trap focus, and refresh the wallet pane', async () => {
|
||||
@@ -1009,7 +1009,7 @@ test('show()/hide() toggle panes, trap focus, and refresh the wallet pane', asyn
|
||||
};
|
||||
|
||||
let walletRefreshes = 0;
|
||||
controller.refreshWalletPaneState = () => {
|
||||
controller.walletController.refreshWalletPaneState = () => {
|
||||
walletRefreshes += 1;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user