package main import ( "smesh.lol/web/common/helpers" "smesh.lol/web/common/jsbridge/dom" "smesh.lol/web/common/jsbridge/localstorage" "smesh.lol/web/common/jsbridge/signer" ) // Signer modal overlay inside the sm3sh app. // Communicates with the extension background via window.nostr.smesh. var ( signerModal dom.Element signerContent dom.Element signerOpen bool signerStatus string // "none", "locked", "unlocked" pendingK0Name string // nickname to publish as kind 0 after login ) // initSigner checks for the extension and sets up the signer button. func initSigner() { signer.IsInstalled(func(ok bool) { if !ok { dom.ConsoleLog("signer extension not detected") return } dom.ConsoleLog("signer extension detected") refreshVaultStatus() }) } func refreshVaultStatus() { signer.GetVaultStatus(func(status string) { signerStatus = status }) } // showSignerModal creates and shows the signer modal overlay. func showSignerModal() { if signerOpen { return } signerOpen = true // Backdrop. signerModal = dom.CreateElement("div") dom.SetAttribute(signerModal, "class", "signer-backdrop") // Modal card. card := dom.CreateElement("div") dom.SetAttribute(card, "class", "signer-card") // Header. header := dom.CreateElement("div") dom.SetAttribute(header, "class", "signer-header") title := dom.CreateElement("h2") dom.SetTextContent(title, t("signer")) dom.AppendChild(header, title) closeBtn := dom.CreateElement("button") dom.SetAttribute(closeBtn, "class", "signer-close") dom.SetTextContent(closeBtn, "\u00d7") closeCB := dom.RegisterCallback(func() { hideSignerModal() }) dom.AddEventListener(closeBtn, "click", closeCB) dom.AppendChild(header, closeBtn) dom.AppendChild(card, header) // Content area. signerContent = dom.CreateElement("div") dom.SetAttribute(signerContent, "class", "signer-content") dom.AppendChild(card, signerContent) dom.AppendChild(signerModal, card) // Click backdrop (not card children) to close. backdropCB := dom.RegisterCallback(func() { hideSignerModal() }) dom.AddSelfEventListener(signerModal, "click", backdropCB) dom.AppendChild(dom.Body(), signerModal) // Render based on vault status. renderSignerUI() } func hideSignerModal() { if !signerOpen { return } signerOpen = false dom.RemoveChild(dom.Body(), signerModal) } func renderSignerUI() { dom.SetTextContent(signerContent, "") signer.GetVaultStatus(func(status string) { signerStatus = status dom.SetTextContent(signerContent, "") switch status { case "none": renderCreateVault() case "locked": renderUnlockVault() case "unlocked": renderIdentityList() } }) } func renderCreateVault() { p := dom.CreateElement("p") dom.SetTextContent(p, t("no_vault")) dom.AppendChild(signerContent, p) // --- HD Vault (default) --- h3 := dom.CreateElement("h3") dom.SetTextContent(h3, t("hd_keychain")) dom.AppendChild(signerContent, h3) pwInput := dom.CreateElement("input") dom.SetAttribute(pwInput, "type", "password") dom.SetAttribute(pwInput, "placeholder", t("vault_password")) dom.SetAttribute(pwInput, "class", "signer-input") dom.AppendChild(signerContent, pwInput) createBtn := dom.CreateElement("button") dom.SetAttribute(createBtn, "class", "signer-btn") dom.SetTextContent(createBtn, t("generate_keychain")) createCB := dom.RegisterCallback(func() { pw := dom.GetProperty(pwInput, "value") if pw == "" { return } name := "Identity 0" dom.SetTextContent(createBtn, t("generating")) dom.SetAttribute(createBtn, "disabled", "true") signer.CreateHDVault(pw, name, func(mnemonic string) { if mnemonic == "" { dom.SetTextContent(createBtn, t("create_failed")) dom.SetAttribute(createBtn, "disabled", "") return } // Derive Identity 1 so the user has a visible identity after seed reveal. signer.DeriveIdentity("Identity 1", func(pk string) { showMnemonicReveal(mnemonic) }) }) }) dom.AddEventListener(createBtn, "click", createCB) dom.AppendChild(signerContent, createBtn) // --- Restore from mnemonic --- sep := dom.CreateElement("hr") dom.SetAttribute(sep, "class", "signer-sep") dom.AppendChild(signerContent, sep) h3r := dom.CreateElement("h3") dom.SetTextContent(h3r, t("restore_seed")) dom.AppendChild(signerContent, h3r) mnInput := dom.CreateElement("textarea") dom.SetAttribute(mnInput, "placeholder", t("seed_placeholder")) dom.SetAttribute(mnInput, "class", "signer-input signer-textarea") dom.SetAttribute(mnInput, "rows", "3") dom.AppendChild(signerContent, mnInput) rpwInput := dom.CreateElement("input") dom.SetAttribute(rpwInput, "type", "password") dom.SetAttribute(rpwInput, "placeholder", t("vault_password")) dom.SetAttribute(rpwInput, "class", "signer-input") dom.AppendChild(signerContent, rpwInput) idxInput := dom.CreateElement("input") dom.SetAttribute(idxInput, "type", "number") dom.SetAttribute(idxInput, "placeholder", t("account_placeholder")) dom.SetAttribute(idxInput, "class", "signer-input") dom.SetProperty(idxInput, "value", "1") dom.AppendChild(signerContent, idxInput) restoreBtn := dom.CreateElement("button") dom.SetAttribute(restoreBtn, "class", "signer-btn") dom.SetTextContent(restoreBtn, t("restore_keychain")) restoreCB := dom.RegisterCallback(func() { mn := dom.GetProperty(mnInput, "value") pw := dom.GetProperty(rpwInput, "value") if mn == "" || pw == "" { return } idxStr := dom.GetProperty(idxInput, "value") targetIdx := parseAccountIdx(idxStr) if targetIdx < 1 { targetIdx = 1 } dom.SetTextContent(restoreBtn, t("restoring")) dom.SetAttribute(restoreBtn, "disabled", "true") signer.RestoreHDVault(pw, mn, "Identity 0", func(ok bool) { if !ok { dom.SetTextContent(restoreBtn, t("restore_failed")) dom.SetAttribute(restoreBtn, "disabled", "") return } // Derive accounts 1..targetIdx so the requested identity is available. dom.SetTextContent(restoreBtn, t("deriving_n")+" "+itoa(targetIdx)+"...") restoreDeriveUpTo(targetIdx, 1, func() { renderSignerUI() }) }) }) dom.AddEventListener(restoreBtn, "click", restoreCB) dom.AppendChild(signerContent, restoreBtn) // --- Import vault file --- sep2 := dom.CreateElement("hr") dom.SetAttribute(sep2, "class", "signer-sep") dom.AppendChild(signerContent, sep2) h3b := dom.CreateElement("h3") dom.SetTextContent(h3b, t("import_vault")) dom.AppendChild(signerContent, h3b) importBtn := dom.CreateElement("button") dom.SetAttribute(importBtn, "class", "signer-btn signer-btn-secondary") dom.SetTextContent(importBtn, t("choose_vault")) importCB := dom.RegisterCallback(func() { dom.PickFileText(".json", func(data string) { if data == "" { return } dom.SetTextContent(importBtn, t("restoring")) dom.SetAttribute(importBtn, "disabled", "true") signer.ImportVault(data, func(ok bool) { if ok { renderSignerUI() } else { dom.SetTextContent(importBtn, t("invalid_vault")) dom.SetAttribute(importBtn, "disabled", "") } }) }) }) dom.AddEventListener(importBtn, "click", importCB) dom.AppendChild(signerContent, importBtn) } // showMnemonicReveal displays the generated mnemonic for the user to write down. func showMnemonicReveal(mnemonic string) { dom.SetTextContent(signerContent, "") h3 := dom.CreateElement("h3") dom.SetTextContent(h3, t("write_seed")) dom.AppendChild(signerContent, h3) warn := dom.CreateElement("p") dom.SetAttribute(warn, "class", "signer-warn") dom.SetTextContent(warn, t("seed_warning")) dom.AppendChild(signerContent, warn) mnBox := dom.CreateElement("div") dom.SetAttribute(mnBox, "class", "signer-mnemonic") words := splitMnemonicWords(mnemonic) for i, w := range words { span := dom.CreateElement("span") dom.SetAttribute(span, "class", "signer-word") dom.SetTextContent(span, itoa(i+1)+". "+w) dom.AppendChild(mnBox, span) } dom.AppendChild(signerContent, mnBox) copyBtn := dom.CreateElement("button") dom.SetAttribute(copyBtn, "class", "signer-btn signer-btn-secondary") dom.SetAttribute(copyBtn, "data-mn", mnemonic) dom.SetTextContent(copyBtn, t("copy_clipboard")) dom.SetAttribute(copyBtn, "data-label", t("copy_clipboard")) dom.SetAttribute(copyBtn, "data-copied", t("copied")) dom.SetAttribute(copyBtn, "onclick", "var b=this;navigator.clipboard.writeText(b.dataset.mn).then(function(){b.textContent=b.dataset.copied});setTimeout(function(){b.textContent=b.dataset.label},1500)") dom.AppendChild(signerContent, copyBtn) doneBtn := dom.CreateElement("button") dom.SetAttribute(doneBtn, "class", "signer-btn") dom.SetTextContent(doneBtn, t("saved_it")) doneCB := dom.RegisterCallback(func() { renderSignerUI() }) dom.AddEventListener(doneBtn, "click", doneCB) dom.AppendChild(signerContent, doneBtn) } func splitMnemonicWords(s string) []string { var words []string start := -1 for i := 0; i < len(s); i++ { if s[i] == ' ' { if start >= 0 { words = append(words, s[start:i]) start = -1 } } else if start < 0 { start = i } } if start >= 0 { words = append(words, s[start:]) } return words } func renderUnlockVault() { p := dom.CreateElement("p") dom.SetTextContent(p, t("vault_locked")) dom.AppendChild(signerContent, p) input := dom.CreateElement("input") dom.SetAttribute(input, "type", "password") dom.SetAttribute(input, "placeholder", t("password")) dom.SetAttribute(input, "class", "signer-input") dom.AppendChild(signerContent, input) dom.Focus(input) btn := dom.CreateElement("button") dom.SetAttribute(btn, "class", "signer-btn") dom.SetTextContent(btn, t("unlock")) cb := dom.RegisterCallback(func() { pw := dom.GetProperty(input, "value") if pw == "" { return } dom.SetTextContent(p, t("deriving_key")) dom.SetAttribute(btn, "disabled", "true") signer.UnlockVault(pw, func(ok bool) { if ok { renderSignerUI() } else { dom.SetTextContent(p, t("wrong_password")) dom.SetAttribute(btn, "disabled", "") } }) }) dom.AddEventListener(btn, "click", cb) dom.AddEnterKeyListener(input, cb) dom.AppendChild(signerContent, btn) } func renderIdentityList() { signer.ListIdentities(func(list string) { signer.IsHD(func(hd bool) { dom.SetTextContent(signerContent, "") h := dom.CreateElement("h3") dom.SetTextContent(h, t("identities")) dom.AppendChild(signerContent, h) renderIdentitiesFromJSON(list, hd) if hd { renderHDControls() } else { renderLegacyAddIdentity() } renderBottomActions(hd) }) }) } func renderHDControls() { addDiv := dom.CreateElement("div") dom.SetAttribute(addDiv, "class", "signer-add") nameInput := dom.CreateElement("input") dom.SetAttribute(nameInput, "type", "text") dom.SetAttribute(nameInput, "placeholder", t("nickname_placeholder")) dom.SetAttribute(nameInput, "class", "signer-input") dom.AppendChild(addDiv, nameInput) deriveBtn := dom.CreateElement("button") dom.SetAttribute(deriveBtn, "class", "signer-btn") dom.SetTextContent(deriveBtn, t("derive_new")) deriveCB := dom.RegisterCallback(func() { name := dom.GetProperty(nameInput, "value") dom.SetAttribute(deriveBtn, "disabled", "true") dom.SetTextContent(deriveBtn, t("deriving")) signer.DeriveIdentity(name, func(pk string) { if pk == "" { dom.SetAttribute(deriveBtn, "disabled", "") dom.SetTextContent(deriveBtn, t("derive_new")) return } // Stash name — kind 0 will be published after login when relays are live. pendingK0Name = name renderSignerUI() }) }) dom.AddEventListener(deriveBtn, "click", deriveCB) dom.AppendChild(addDiv, deriveBtn) dom.AppendChild(signerContent, addDiv) } func parseAccountIdx(s string) int { n := 0 for i := 0; i < len(s); i++ { c := s[i] if c >= '0' && c <= '9' { n = n*10 + int(c-'0') } else { break } } return n } // restoreDeriveUpTo derives accounts from cur up to target sequentially. func restoreDeriveUpTo(target, cur int, done func()) { if cur > target { done() return } signer.DeriveIdentity("Identity "+itoa(cur), func(pk string) { restoreDeriveUpTo(target, cur+1, done) }) } // flushPendingK0 publishes a kind 0 if one was queued during identity derivation. // Called from showApp() after relays are live. func flushPendingK0() { name := pendingK0Name if name == "" || pubhex == "" { return } pendingK0Name = "" publishKind0ForIdentity(pubhex, name, func() {}) } // publishKind0ForIdentity switches to the given identity, signs a kind 0 event // with the nickname, publishes it to all relays, then calls done. func publishKind0ForIdentity(pk, name string, done func()) { signer.SwitchIdentity(pk, func(ok bool) { if !ok { done() return } content := "{\"name\":" + helpers.JsonString(name) + ",\"display_name\":" + helpers.JsonString(name) + "}" ts := dom.NowSeconds() unsigned := "{\"kind\":0,\"content\":" + helpers.JsonString(content) + ",\"tags\":[],\"created_at\":" + i64toa(ts) + ",\"pubkey\":\"" + pk + "\"}" signer.SignEvent(unsigned, func(signed string) { if signed != "" { dom.PostToSW("[\"EVENT\"," + signed + "]") dom.ConsoleLog("published kind 0 for " + pk[:8] + " name=" + name) } done() }) }) } func renderLegacyAddIdentity() { addDiv := dom.CreateElement("div") dom.SetAttribute(addDiv, "class", "signer-add") addInput := dom.CreateElement("input") dom.SetAttribute(addInput, "type", "password") dom.SetAttribute(addInput, "placeholder", "nsec1...") dom.SetAttribute(addInput, "class", "signer-input") dom.AppendChild(addDiv, addInput) addBtn := dom.CreateElement("button") dom.SetAttribute(addBtn, "class", "signer-btn") dom.SetTextContent(addBtn, t("add")) addCB := dom.RegisterCallback(func() { nsec := dom.GetProperty(addInput, "value") if nsec == "" { return } signer.AddIdentity(nsec, func(ok bool) { if ok { dom.SetProperty(addInput, "value", "") renderSignerUI() } }) }) dom.AddEventListener(addBtn, "click", addCB) dom.AppendChild(addDiv, addBtn) dom.AppendChild(signerContent, addDiv) } func renderBottomActions(hd bool) { actions := dom.CreateElement("div") dom.SetAttribute(actions, "class", "signer-actions") // Show seed phrase button (HD only). if hd { seedBtn := dom.CreateElement("button") dom.SetAttribute(seedBtn, "class", "signer-btn signer-btn-secondary") dom.SetTextContent(seedBtn, t("show_seed")) seedCB := dom.RegisterCallback(func() { signer.GetMnemonic(func(m string) { if m != "" { showMnemonicReveal(m) } }) }) dom.AddEventListener(seedBtn, "click", seedCB) dom.AppendChild(actions, seedBtn) } // Export vault button. exportBtn := dom.CreateElement("button") dom.SetAttribute(exportBtn, "class", "signer-btn signer-btn-secondary") dom.SetTextContent(exportBtn, t("export_vault")) exportCB := dom.RegisterCallback(func() { signer.ExportVault(func(data string) { if data == "" { return } dom.DownloadText("smesh-vault.json", data, "application/json") }) }) dom.AddEventListener(exportBtn, "click", exportCB) dom.AppendChild(actions, exportBtn) // Lock vault button. lockBtn := dom.CreateElement("button") dom.SetAttribute(lockBtn, "class", "signer-btn signer-btn-secondary") dom.SetTextContent(lockBtn, t("lock_vault")) lockCB := dom.RegisterCallback(func() { signer.LockVault(func() { renderSignerUI() }) }) dom.AddEventListener(lockBtn, "click", lockCB) dom.AppendChild(actions, lockBtn) // Reset extension button. resetBtn := dom.CreateElement("button") dom.SetAttribute(resetBtn, "class", "signer-btn signer-btn-danger") dom.SetTextContent(resetBtn, t("reset_extension")) resetCB := dom.RegisterCallback(func() { if !dom.Confirm(t("reset_confirm")) { return } signer.ResetExtension(func(ok bool) { if !ok { return } // Verify the vault is actually gone before clearing app state. signer.GetVaultStatus(func(status string) { if status == "none" { localstorage.RemoveItem(lsKeyPubkey) dom.LocationReload() } }) }) }) dom.AddEventListener(resetBtn, "click", resetCB) dom.AppendChild(actions, resetBtn) dom.AppendChild(signerContent, actions) } func renderIdentitiesFromJSON(listJSON string, hd bool) { // Parse: [{"pubkey":"...","name":"...","active":true}, ...] i := 0 idxNum := 0 for i < len(listJSON) && listJSON[i] != '[' { i++ } i++ for i < len(listJSON) { for i < len(listJSON) && listJSON[i] != '{' && listJSON[i] != ']' { i++ } if i >= len(listJSON) || listJSON[i] == ']' { break } end := i + 1 depth := 1 for end < len(listJSON) && depth > 0 { if listJSON[end] == '{' { depth++ } else if listJSON[end] == '}' { depth-- } else if listJSON[end] == '"' { end++ for end < len(listJSON) && listJSON[end] != '"' { if listJSON[end] == '\\' { end++ } end++ } } end++ } obj := listJSON[i:end] pk := helpers.JsonGetString(obj, "pubkey") name := helpers.JsonGetString(obj, "name") if pk == "" { i = end idxNum++ continue } row := dom.CreateElement("div") dom.SetAttribute(row, "class", "signer-identity") label := dom.CreateElement("span") display := pk[:8] + "..." if name != "" { display = name + " (" + pk[:8] + "...)" } if hd { display = "#" + itoa(idxNum) + " " + display } dom.SetTextContent(label, display) dom.AppendChild(row, label) btns := dom.CreateElement("span") // HD identity 0 is the seed — skip it entirely. if hd && idxNum == 0 { i = end idxNum++ continue } // Switch button. switchBtn := dom.CreateElement("button") dom.SetAttribute(switchBtn, "class", "signer-btn-sm") dom.SetTextContent(switchBtn, t("use")) switchPK := pk switchCB := dom.RegisterCallback(func() { signer.SwitchIdentity(switchPK, func(ok bool) { if ok { hideSignerModal() if pubkey == nil { pubhex = switchPK pubkey = helpers.HexDecode(switchPK) localstorage.SetItem(lsKeyPubkey, pubhex) clearChildren(root) showApp() } } }) }) dom.AddEventListener(switchBtn, "click", switchCB) dom.AppendChild(btns, switchBtn) // Publish kind 0 button. pubBtn := dom.CreateElement("button") dom.SetAttribute(pubBtn, "class", "signer-btn-sm") dom.SetTextContent(pubBtn, t("publish")) pubPK := pk pubName := name pubCB := dom.RegisterCallback(func() { dom.SetTextContent(pubBtn, "...") dom.SetAttribute(pubBtn, "disabled", "true") publishKind0ForIdentity(pubPK, pubName, func() { dom.SetTextContent(pubBtn, t("publish")) dom.SetAttribute(pubBtn, "disabled", "") }) }) dom.AddEventListener(pubBtn, "click", pubCB) dom.AppendChild(btns, pubBtn) // Remove button. rmBtn := dom.CreateElement("button") dom.SetAttribute(rmBtn, "class", "signer-btn-sm signer-btn-danger") dom.SetTextContent(rmBtn, "\u00d7") rmPK := pk rmCB := dom.RegisterCallback(func() { signer.RemoveIdentity(rmPK, func(ok bool) { if ok { renderSignerUI() } }) }) dom.AddEventListener(rmBtn, "click", rmCB) dom.AppendChild(btns, rmBtn) dom.AppendChild(row, btns) dom.AppendChild(signerContent, row) i = end idxNum++ } }