hd-wallet-ui 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/app.js +372 -254
- package/src/template.js +159 -132
- package/styles/main.css +428 -26
package/src/app.js
CHANGED
|
@@ -20,7 +20,7 @@ import { Buffer } from 'buffer';
|
|
|
20
20
|
import { createV3 } from 'vcard-cryptoperson';
|
|
21
21
|
|
|
22
22
|
// SpaceDataStandards EME (Encrypted Message Envelope)
|
|
23
|
-
import { EME, EMET } from '@sds/lib/js/
|
|
23
|
+
import { EME, EMET } from '@sds/lib/js/EME/EME.js';
|
|
24
24
|
import * as flatbuffers from 'flatbuffers';
|
|
25
25
|
|
|
26
26
|
// Make Buffer available globally for various crypto libraries
|
|
@@ -908,9 +908,6 @@ function login(keys) {
|
|
|
908
908
|
// Update wallet addresses and balances
|
|
909
909
|
updateAdversarialSecurity();
|
|
910
910
|
|
|
911
|
-
// Populate vCard keys display
|
|
912
|
-
populateVCardKeysDisplay();
|
|
913
|
-
|
|
914
911
|
// Open Account modal so user can see the wallet they just loaded
|
|
915
912
|
$('keys-modal')?.classList.add('active');
|
|
916
913
|
deriveAndDisplayAddress();
|
|
@@ -1000,7 +997,7 @@ async function exportWallet(format) {
|
|
|
1000
997
|
break;
|
|
1001
998
|
|
|
1002
999
|
case 'xpub':
|
|
1003
|
-
if (!state.hdRoot?.
|
|
1000
|
+
if (!state.hdRoot?.toXpub) {
|
|
1004
1001
|
alert('Extended public key not available.');
|
|
1005
1002
|
return;
|
|
1006
1003
|
}
|
|
@@ -1010,7 +1007,7 @@ async function exportWallet(format) {
|
|
|
1010
1007
|
break;
|
|
1011
1008
|
|
|
1012
1009
|
case 'xprv':
|
|
1013
|
-
if (!state.hdRoot?.
|
|
1010
|
+
if (!state.hdRoot?.toXprv) {
|
|
1014
1011
|
alert('Extended private key not available.');
|
|
1015
1012
|
return;
|
|
1016
1013
|
}
|
|
@@ -1060,104 +1057,26 @@ function downloadData(data, filename, mimeType) {
|
|
|
1060
1057
|
// Wallet Address Population & Balance Fetching
|
|
1061
1058
|
// =============================================================================
|
|
1062
1059
|
|
|
1063
|
-
// Account
|
|
1064
|
-
let _accountAddressData = {}; // { xpub: { addr, value }, btc: { addr, value }, ... }
|
|
1065
|
-
|
|
1060
|
+
// Account header — show xpub only
|
|
1066
1061
|
function populateAccountAddressDropdown() {
|
|
1067
|
-
const
|
|
1068
|
-
if (!
|
|
1062
|
+
const addrEl = $('account-address-display');
|
|
1063
|
+
if (!addrEl) return;
|
|
1069
1064
|
|
|
1070
1065
|
const xpubStr = state.hdRoot ? state.hdRoot.toXpub() : '';
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
const networks = [
|
|
1074
|
-
{ key: 'xpub', label: 'xpub', addr: xpubStr },
|
|
1075
|
-
{ key: 'btc', label: 'Bitcoin', addr: addrs.btc || '' },
|
|
1076
|
-
{ key: 'eth', label: 'Ethereum', addr: addrs.eth || '' },
|
|
1077
|
-
{ key: 'sol', label: 'Solana', addr: addrs.sol || '' },
|
|
1078
|
-
// { key: 'xrp', label: 'Ripple', addr: addrs.xrp || '' },
|
|
1079
|
-
];
|
|
1080
|
-
|
|
1081
|
-
// // Add SUI/Monad/ADA if we can derive them
|
|
1082
|
-
// if (state.hdRoot) {
|
|
1083
|
-
// try {
|
|
1084
|
-
// const suiPath = buildSigningPath(784, 0, 0);
|
|
1085
|
-
// const suiDerived = state.hdRoot.derivePath(suiPath);
|
|
1086
|
-
// const suiPubKey = ed25519.getPublicKey(suiDerived.privateKey());
|
|
1087
|
-
// networks.push({ key: 'sui', label: 'SUI', addr: deriveSuiAddress(suiPubKey, 'ed25519') });
|
|
1088
|
-
// } catch (_) {}
|
|
1089
|
-
// networks.push({ key: 'monad', label: 'Monad', addr: addrs.eth || '' });
|
|
1090
|
-
// try {
|
|
1091
|
-
// const adaPath = buildSigningPath(1815, 0, 0);
|
|
1092
|
-
// const adaDerived = state.hdRoot.derivePath(adaPath);
|
|
1093
|
-
// const adaPubKey = ed25519.getPublicKey(adaDerived.privateKey());
|
|
1094
|
-
// networks.push({ key: 'ada', label: 'Cardano', addr: deriveCardanoAddress(adaPubKey) });
|
|
1095
|
-
// } catch (_) {}
|
|
1096
|
-
// }
|
|
1097
|
-
|
|
1098
|
-
_accountAddressData = {};
|
|
1099
|
-
sel.innerHTML = '';
|
|
1100
|
-
for (const n of networks) {
|
|
1101
|
-
if (!n.addr) continue;
|
|
1102
|
-
_accountAddressData[n.key] = { addr: n.addr, value: '' };
|
|
1103
|
-
const opt = document.createElement('option');
|
|
1104
|
-
opt.value = n.key;
|
|
1105
|
-
opt.textContent = n.label;
|
|
1106
|
-
sel.appendChild(opt);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
sel.removeEventListener('change', updateAccountAddressDisplay);
|
|
1110
|
-
sel.addEventListener('change', updateAccountAddressDisplay);
|
|
1066
|
+
addrEl.textContent = xpubStr;
|
|
1067
|
+
addrEl.title = xpubStr;
|
|
1111
1068
|
|
|
1112
1069
|
const copyBtn = $('account-address-copy');
|
|
1113
1070
|
if (copyBtn) {
|
|
1114
1071
|
copyBtn.onclick = () => {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if (data?.addr) {
|
|
1118
|
-
navigator.clipboard.writeText(data.addr).then(() => {
|
|
1072
|
+
if (xpubStr) {
|
|
1073
|
+
navigator.clipboard.writeText(xpubStr).then(() => {
|
|
1119
1074
|
copyBtn.title = 'Copied!';
|
|
1120
|
-
setTimeout(() => { copyBtn.title = 'Copy
|
|
1075
|
+
setTimeout(() => { copyBtn.title = 'Copy xpub'; }, 1500);
|
|
1121
1076
|
});
|
|
1122
1077
|
}
|
|
1123
1078
|
};
|
|
1124
1079
|
}
|
|
1125
|
-
|
|
1126
|
-
updateAccountAddressDisplay();
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
function updateAccountAddressDisplay() {
|
|
1130
|
-
const sel = $('account-address-select');
|
|
1131
|
-
const addrEl = $('account-address-display');
|
|
1132
|
-
const valEl = $('account-address-value');
|
|
1133
|
-
if (!sel || !addrEl) return;
|
|
1134
|
-
|
|
1135
|
-
const key = sel.value;
|
|
1136
|
-
const data = _accountAddressData[key];
|
|
1137
|
-
if (!data) return;
|
|
1138
|
-
|
|
1139
|
-
const addr = data.addr;
|
|
1140
|
-
addrEl.textContent = addr;
|
|
1141
|
-
addrEl.title = addr;
|
|
1142
|
-
if (valEl) valEl.textContent = data.value || (key !== 'xpub' ? '$0.00' : '');
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
function updateAccountAddressValues(bondBalances, prices, currency) {
|
|
1146
|
-
const symbol = CURRENCY_SYMBOLS[currency] || currency;
|
|
1147
|
-
const keyToSymbol = { btc: 'BTC', eth: 'ETH', sol: 'SOL' };
|
|
1148
|
-
|
|
1149
|
-
for (const [key, data] of Object.entries(_accountAddressData)) {
|
|
1150
|
-
if (key === 'xpub') {
|
|
1151
|
-
data.value = '';
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
const sym = keyToSymbol[key];
|
|
1155
|
-
const bal = parseFloat(bondBalances[key]) || 0;
|
|
1156
|
-
const price = (prices && sym) ? (prices[sym] || 0) : 0;
|
|
1157
|
-
const converted = bal * price;
|
|
1158
|
-
data.value = converted > 0 ? symbol + converted.toFixed(2) : bal > 0 ? bal.toFixed(6) + ' ' + (sym || '') : '';
|
|
1159
|
-
}
|
|
1160
|
-
updateAccountAddressDisplay();
|
|
1161
1080
|
}
|
|
1162
1081
|
|
|
1163
1082
|
function populateWalletAddresses() {
|
|
@@ -1629,8 +1548,6 @@ async function updateAdversarialSecurity() {
|
|
|
1629
1548
|
accountTotalEl.textContent = 'Bond: ' + formatCurrencyValue(totalConverted, currency);
|
|
1630
1549
|
}
|
|
1631
1550
|
|
|
1632
|
-
// Update account address dropdown values
|
|
1633
|
-
updateAccountAddressValues(bondBalances, cryptoPrices, currency);
|
|
1634
1551
|
}
|
|
1635
1552
|
|
|
1636
1553
|
// =============================================================================
|
|
@@ -1638,7 +1555,7 @@ async function updateAdversarialSecurity() {
|
|
|
1638
1555
|
// =============================================================================
|
|
1639
1556
|
|
|
1640
1557
|
function generateVCard(info, { skipPhoto = false } = {}) {
|
|
1641
|
-
const person = {};
|
|
1558
|
+
const person = { KEY: [] };
|
|
1642
1559
|
|
|
1643
1560
|
if (info.firstName || info.lastName) {
|
|
1644
1561
|
if (info.lastName) person.FAMILY_NAME = info.lastName;
|
|
@@ -1648,8 +1565,15 @@ function generateVCard(info, { skipPhoto = false } = {}) {
|
|
|
1648
1565
|
if (info.suffix) person.HONORIFIC_SUFFIX = info.suffix;
|
|
1649
1566
|
}
|
|
1650
1567
|
|
|
1568
|
+
const contacts = [];
|
|
1651
1569
|
if (info.email) {
|
|
1652
|
-
|
|
1570
|
+
contacts.push({ EMAIL: info.email, CONTACT_TYPE: 'HOME' });
|
|
1571
|
+
}
|
|
1572
|
+
if (info.phone) {
|
|
1573
|
+
contacts.push({ TELEPHONE: info.phone, CONTACT_TYPE: 'CELL' });
|
|
1574
|
+
}
|
|
1575
|
+
if (contacts.length) {
|
|
1576
|
+
person.CONTACT_POINT = contacts;
|
|
1653
1577
|
}
|
|
1654
1578
|
|
|
1655
1579
|
if (info.org) {
|
|
@@ -1664,34 +1588,42 @@ function generateVCard(info, { skipPhoto = false } = {}) {
|
|
|
1664
1588
|
person.IMAGE = state.vcardPhoto;
|
|
1665
1589
|
}
|
|
1666
1590
|
|
|
1667
|
-
if (info.includeKeys && state.wallet
|
|
1591
|
+
if (info.includeKeys && state.wallet?.x25519) {
|
|
1668
1592
|
person.KEY = [
|
|
1669
|
-
...(state.hdRoot?.
|
|
1670
|
-
|
|
1671
|
-
|
|
1593
|
+
...(state.hdRoot?.toXpub ? [{
|
|
1594
|
+
XPUB: state.hdRoot.toXpub(),
|
|
1595
|
+
LABEL: '',
|
|
1672
1596
|
}] : []),
|
|
1673
1597
|
{
|
|
1674
|
-
KEY_TYPE: 'X25519',
|
|
1675
1598
|
PUBLIC_KEY: toBase64(state.wallet.x25519.publicKey),
|
|
1599
|
+
LABEL: 'X25519',
|
|
1676
1600
|
},
|
|
1677
1601
|
{
|
|
1678
|
-
KEY_TYPE: 'Ed25519',
|
|
1679
1602
|
PUBLIC_KEY: toBase64(state.wallet.ed25519.publicKey),
|
|
1603
|
+
LABEL: "Ed25519 m/44'/501'/0'/0/0",
|
|
1680
1604
|
},
|
|
1681
1605
|
{
|
|
1682
|
-
KEY_TYPE: 'secp256k1',
|
|
1683
1606
|
PUBLIC_KEY: toBase64(state.wallet.secp256k1.publicKey),
|
|
1684
|
-
|
|
1607
|
+
KEY_ADDRESS: state.addresses.btc || undefined,
|
|
1608
|
+
LABEL: "secp256k1 m/44'/0'/0'/0/0",
|
|
1685
1609
|
},
|
|
1686
1610
|
];
|
|
1611
|
+
} else if (info.xpubOnly && state.hdRoot?.toXpub) {
|
|
1612
|
+
person.KEY = [{ XPUB: state.hdRoot.toXpub(), LABEL: '' }];
|
|
1687
1613
|
}
|
|
1688
1614
|
|
|
1689
1615
|
const note = info.includeKeys
|
|
1690
|
-
? 'Generated by
|
|
1616
|
+
? 'Generated by Space Data Network'
|
|
1691
1617
|
: undefined;
|
|
1692
1618
|
|
|
1693
1619
|
let vcard = createV3(person, note);
|
|
1694
1620
|
|
|
1621
|
+
// Add NICKNAME field (not supported by createV3)
|
|
1622
|
+
if (info.nickname) {
|
|
1623
|
+
vcard = vcard.replace('END:VCARD', `NICKNAME:${info.nickname}\nEND:VCARD`);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
|
|
1695
1627
|
// Convert PHOTO from data URI format to iOS-compatible inline base64 format
|
|
1696
1628
|
vcard = vcard.replace(
|
|
1697
1629
|
/PHOTO;VALUE=URI:data:image\/(\w+);base64,([^\n]+)\n/,
|
|
@@ -1709,127 +1641,126 @@ function generateVCard(info, { skipPhoto = false } = {}) {
|
|
|
1709
1641
|
}
|
|
1710
1642
|
|
|
1711
1643
|
// =============================================================================
|
|
1712
|
-
// vCard
|
|
1644
|
+
// vCard Digital Signature (Ed25519)
|
|
1713
1645
|
// =============================================================================
|
|
1714
1646
|
|
|
1715
|
-
function
|
|
1716
|
-
const
|
|
1717
|
-
|
|
1647
|
+
function getSignableBody(vcardText) {
|
|
1648
|
+
const lines = vcardText.split('\n');
|
|
1649
|
+
const sigItems = new Set();
|
|
1650
|
+
for (const line of lines) {
|
|
1651
|
+
const m = line.match(/^item(\d+)\.X-ABLabel:Digital Signature/);
|
|
1652
|
+
if (m) sigItems.add(m[1]);
|
|
1653
|
+
}
|
|
1654
|
+
return lines.filter(line => {
|
|
1655
|
+
if (line.trim() === 'END:VCARD') return false;
|
|
1656
|
+
for (const n of sigItems) {
|
|
1657
|
+
if (line.startsWith(`item${n}.`)) return false;
|
|
1658
|
+
}
|
|
1659
|
+
return true;
|
|
1660
|
+
}).join('\n') + '\n';
|
|
1661
|
+
}
|
|
1718
1662
|
|
|
1719
|
-
|
|
1663
|
+
function signVCard(vcardText) {
|
|
1664
|
+
if (!state.wallet?.ed25519?.privateKey) return vcardText;
|
|
1720
1665
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1666
|
+
const body = getSignableBody(vcardText);
|
|
1667
|
+
const messageBytes = new TextEncoder().encode(body);
|
|
1668
|
+
const signature = ed25519.sign(messageBytes, state.wallet.ed25519.privateKey);
|
|
1669
|
+
const sigB64 = toBase64(signature);
|
|
1670
|
+
|
|
1671
|
+
// Encode signature + derivation path (coinType=501, account=0, index=0)
|
|
1672
|
+
const sigValue = `${sigB64}:501:0:0`;
|
|
1673
|
+
|
|
1674
|
+
// Find highest itemN and key index
|
|
1675
|
+
let maxItem = 0;
|
|
1676
|
+
let maxKeyIdx = 0;
|
|
1677
|
+
const itemRe = /item(\d+)\./g;
|
|
1678
|
+
const keyIdxRe = /#(\d+)/g;
|
|
1679
|
+
let match;
|
|
1680
|
+
while ((match = itemRe.exec(vcardText)) !== null) {
|
|
1681
|
+
maxItem = Math.max(maxItem, parseInt(match[1], 10));
|
|
1682
|
+
}
|
|
1683
|
+
while ((match = keyIdxRe.exec(vcardText)) !== null) {
|
|
1684
|
+
maxKeyIdx = Math.max(maxKeyIdx, parseInt(match[1], 10));
|
|
1732
1685
|
}
|
|
1733
1686
|
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1687
|
+
const sigLines =
|
|
1688
|
+
`item${maxItem + 1}.X-ABLabel:Digital Signature #${maxKeyIdx + 1}\n` +
|
|
1689
|
+
`item${maxItem + 1}.X-ABRELATEDNAMES:${sigValue}\n`;
|
|
1690
|
+
|
|
1691
|
+
return body + sigLines + 'END:VCARD';
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function verifyVCardSignature(vcardText) {
|
|
1695
|
+
// Parse all itemN label/value pairs
|
|
1696
|
+
const lines = vcardText.split('\n');
|
|
1697
|
+
const items = {};
|
|
1698
|
+
for (const line of lines) {
|
|
1699
|
+
const labelMatch = line.match(/^item(\d+)\.X-ABLabel:(.+)/);
|
|
1700
|
+
if (labelMatch) {
|
|
1701
|
+
items[labelMatch[1]] = items[labelMatch[1]] || {};
|
|
1702
|
+
items[labelMatch[1]].label = labelMatch[2].trim();
|
|
1703
|
+
}
|
|
1704
|
+
const valueMatch = line.match(/^item(\d+)\.X-ABRELATEDNAMES:(.+)/);
|
|
1705
|
+
if (valueMatch) {
|
|
1706
|
+
items[valueMatch[1]] = items[valueMatch[1]] || {};
|
|
1707
|
+
items[valueMatch[1]].value = valueMatch[2].trim();
|
|
1708
|
+
}
|
|
1745
1709
|
}
|
|
1746
1710
|
|
|
1747
|
-
//
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
pubkey: state.wallet.ed25519 ? toHex(state.wallet.ed25519.publicKey) : '—',
|
|
1754
|
-
path: buildSigningPath(501, 0, 0), // m/44'/501'/0'/0'
|
|
1755
|
-
role: 'signing',
|
|
1756
|
-
explorer: `https://explorer.solana.com/address/${state.addresses.sol}`,
|
|
1757
|
-
});
|
|
1711
|
+
// Find Digital Signature entry
|
|
1712
|
+
let sigValue = null;
|
|
1713
|
+
for (const item of Object.values(items)) {
|
|
1714
|
+
if (item.label?.startsWith('Digital Signature') && item.value) {
|
|
1715
|
+
sigValue = item.value;
|
|
1716
|
+
}
|
|
1758
1717
|
}
|
|
1759
1718
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
keys.push({
|
|
1763
|
-
label: 'Encryption Key',
|
|
1764
|
-
curve: 'P-256 (NIST)',
|
|
1765
|
-
address: '—',
|
|
1766
|
-
pubkey: toHex(state.wallet.p256.publicKey),
|
|
1767
|
-
path: buildEncryptionPath(0, 0, 0), // m/44'/0'/0'/1'/0'
|
|
1768
|
-
role: 'encryption',
|
|
1769
|
-
explorer: null,
|
|
1770
|
-
});
|
|
1719
|
+
if (!sigValue) {
|
|
1720
|
+
return { verified: false, path: null, publicKey: null, error: 'unsigned' };
|
|
1771
1721
|
}
|
|
1772
1722
|
|
|
1773
|
-
//
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
<div class="key-display-row">
|
|
1789
|
-
<span class="key-display-field">Public Key</span>
|
|
1790
|
-
<code class="key-display-value truncate" title="${key.pubkey}">${truncateAddress(key.pubkey, 16)}</code>
|
|
1791
|
-
<button class="copy-btn-small" data-copy-text="${key.pubkey}" title="Copy">
|
|
1792
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1793
|
-
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
1794
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
1795
|
-
</svg>
|
|
1796
|
-
</button>
|
|
1797
|
-
</div>
|
|
1798
|
-
${key.address !== '—' ? `
|
|
1799
|
-
<div class="key-display-row">
|
|
1800
|
-
<span class="key-display-field">Address</span>
|
|
1801
|
-
<code class="key-display-value truncate" title="${key.address}">${truncateAddress(key.address, 16)}</code>
|
|
1802
|
-
${key.explorer ? `<a href="${key.explorer}" target="_blank" rel="noopener" class="explorer-link-small" title="View on Explorer">
|
|
1803
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1804
|
-
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
|
1805
|
-
<polyline points="15 3 21 3 21 9"/>
|
|
1806
|
-
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
1807
|
-
</svg>
|
|
1808
|
-
</a>` : ''}
|
|
1809
|
-
</div>
|
|
1810
|
-
` : ''}
|
|
1811
|
-
<div class="key-display-row">
|
|
1812
|
-
<span class="key-display-field">Derivation Path</span>
|
|
1813
|
-
<code class="key-display-value">${key.path}</code>
|
|
1814
|
-
</div>
|
|
1815
|
-
`;
|
|
1816
|
-
keysDisplay.appendChild(keyCard);
|
|
1817
|
-
});
|
|
1818
|
-
|
|
1819
|
-
// Add copy button event listeners
|
|
1820
|
-
keysDisplay.querySelectorAll('.copy-btn-small').forEach(btn => {
|
|
1821
|
-
btn.addEventListener('click', async () => {
|
|
1822
|
-
const text = btn.getAttribute('data-copy-text');
|
|
1723
|
+
// Parse signature value: base64sig:coinType:account:index
|
|
1724
|
+
const parts = sigValue.split(':');
|
|
1725
|
+
if (parts.length < 4) {
|
|
1726
|
+
return { verified: false, path: null, publicKey: null, error: 'Malformed signature' };
|
|
1727
|
+
}
|
|
1728
|
+
const sigB64 = parts[0];
|
|
1729
|
+
const coinType = parts[1];
|
|
1730
|
+
const account = parts[2];
|
|
1731
|
+
const index = parts[3];
|
|
1732
|
+
const path = `m/44'/${coinType}'/${account}'/0/${index}`;
|
|
1733
|
+
|
|
1734
|
+
// Find Ed25519 public key — look for "Public Key" entries with 32-byte (44-char base64) values
|
|
1735
|
+
let ed25519PubB64 = null;
|
|
1736
|
+
for (const item of Object.values(items)) {
|
|
1737
|
+
if (item.label?.startsWith('Public Key') && item.value) {
|
|
1823
1738
|
try {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1739
|
+
const decoded = Uint8Array.from(atob(item.value), c => c.charCodeAt(0));
|
|
1740
|
+
if (decoded.length === 32) {
|
|
1741
|
+
ed25519PubB64 = item.value;
|
|
1742
|
+
break;
|
|
1743
|
+
}
|
|
1744
|
+
} catch { /* not base64 */ }
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
if (!ed25519PubB64) {
|
|
1749
|
+
return { verified: false, path, publicKey: null, error: 'No Ed25519 public key found' };
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Reconstruct signable body and verify
|
|
1753
|
+
const body = getSignableBody(vcardText);
|
|
1754
|
+
const messageBytes = new TextEncoder().encode(body);
|
|
1755
|
+
const sigBytes = Uint8Array.from(atob(sigB64), c => c.charCodeAt(0));
|
|
1756
|
+
const pubKeyBytes = Uint8Array.from(atob(ed25519PubB64), c => c.charCodeAt(0));
|
|
1757
|
+
|
|
1758
|
+
try {
|
|
1759
|
+
const valid = ed25519.verify(sigBytes, messageBytes, pubKeyBytes);
|
|
1760
|
+
return { verified: valid, path, publicKey: ed25519PubB64, error: valid ? null : 'Signature invalid' };
|
|
1761
|
+
} catch (e) {
|
|
1762
|
+
return { verified: false, path, publicKey: ed25519PubB64, error: e.message };
|
|
1763
|
+
}
|
|
1833
1764
|
}
|
|
1834
1765
|
|
|
1835
1766
|
function parseAndDisplayVCF(vcfText) {
|
|
@@ -1913,6 +1844,23 @@ function parseAndDisplayVCF(vcfText) {
|
|
|
1913
1844
|
html += '</div>';
|
|
1914
1845
|
}
|
|
1915
1846
|
|
|
1847
|
+
// Verify digital signature
|
|
1848
|
+
const sigStatus = $('vcf-import-sig-status');
|
|
1849
|
+
if (sigStatus) {
|
|
1850
|
+
const result = verifyVCardSignature(vcfText);
|
|
1851
|
+
if (result.error === 'unsigned') {
|
|
1852
|
+
sigStatus.className = 'vcard-sig-badge sig-unsigned';
|
|
1853
|
+
sigStatus.innerHTML = 'No signature';
|
|
1854
|
+
} else if (result.verified) {
|
|
1855
|
+
sigStatus.className = 'vcard-sig-badge sig-verified';
|
|
1856
|
+
sigStatus.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg> Verified (${result.path})`;
|
|
1857
|
+
} else {
|
|
1858
|
+
sigStatus.className = 'vcard-sig-badge sig-invalid';
|
|
1859
|
+
sigStatus.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg> Invalid signature`;
|
|
1860
|
+
}
|
|
1861
|
+
sigStatus.style.display = 'flex';
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1916
1864
|
fieldsEl.innerHTML = html;
|
|
1917
1865
|
resultEl.style.display = 'block';
|
|
1918
1866
|
}
|
|
@@ -2431,11 +2379,55 @@ function setupMainAppHandlers() {
|
|
|
2431
2379
|
});
|
|
2432
2380
|
});
|
|
2433
2381
|
|
|
2382
|
+
// Messaging sub-tab switching
|
|
2383
|
+
$qa('.messaging-sub-tab').forEach(tab => {
|
|
2384
|
+
tab.addEventListener('click', () => {
|
|
2385
|
+
$qa('.messaging-sub-tab').forEach(t => t.classList.remove('active'));
|
|
2386
|
+
$qa('.messaging-sub-content').forEach(c => c.classList.remove('active'));
|
|
2387
|
+
tab.classList.add('active');
|
|
2388
|
+
const target = $(tab.dataset.messagingSub);
|
|
2389
|
+
if (target) target.classList.add('active');
|
|
2390
|
+
});
|
|
2391
|
+
});
|
|
2392
|
+
|
|
2393
|
+
// Identity card summary — updates the read-only card display from form fields
|
|
2394
|
+
function updateIdentityCardSummary() {
|
|
2395
|
+
const prefix = $('vcard-prefix')?.value || '';
|
|
2396
|
+
const first = $('vcard-firstname')?.value || '';
|
|
2397
|
+
const middle = $('vcard-middlename')?.value || '';
|
|
2398
|
+
const last = $('vcard-lastname')?.value || '';
|
|
2399
|
+
const suffix = $('vcard-suffix')?.value || '';
|
|
2400
|
+
const nick = $('vcard-nickname')?.value || '';
|
|
2401
|
+
const parts = [prefix, first, middle, last, suffix].filter(Boolean);
|
|
2402
|
+
const nameEl = $('identity-card-name');
|
|
2403
|
+
if (nameEl) {
|
|
2404
|
+
const namePart = parts.length > 0 ? parts.join(' ') : '--';
|
|
2405
|
+
if (nick) {
|
|
2406
|
+
nameEl.innerHTML = `${namePart} <span class="nickname">(${nick})</span>`;
|
|
2407
|
+
} else {
|
|
2408
|
+
nameEl.textContent = namePart;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
const titleEl = $('identity-card-title');
|
|
2413
|
+
if (titleEl) titleEl.textContent = $('vcard-title')?.value || '';
|
|
2414
|
+
|
|
2415
|
+
const orgEl = $('identity-card-org');
|
|
2416
|
+
if (orgEl) orgEl.textContent = $('vcard-org')?.value || '';
|
|
2417
|
+
|
|
2418
|
+
const emailEl = $('identity-card-email');
|
|
2419
|
+
if (emailEl) emailEl.textContent = $('vcard-email')?.value || '';
|
|
2420
|
+
|
|
2421
|
+
const phoneEl = $('identity-card-phone');
|
|
2422
|
+
if (phoneEl) phoneEl.textContent = $('vcard-phone')?.value || '';
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
|
|
2434
2426
|
// vCard identity auto-save
|
|
2435
2427
|
const VCARD_STORAGE_KEY = 'hd-wallet-vcard-identity';
|
|
2436
2428
|
const vcardFieldIds = [
|
|
2437
2429
|
'vcard-prefix', 'vcard-firstname', 'vcard-middlename', 'vcard-lastname',
|
|
2438
|
-
'vcard-suffix', 'vcard-email', 'vcard-phone', 'vcard-org', 'vcard-title',
|
|
2430
|
+
'vcard-suffix', 'vcard-nickname', 'vcard-email', 'vcard-phone', 'vcard-org', 'vcard-title',
|
|
2439
2431
|
'vcard-street', 'vcard-city', 'vcard-region', 'vcard-postal', 'vcard-country'
|
|
2440
2432
|
];
|
|
2441
2433
|
|
|
@@ -2466,17 +2458,96 @@ function setupMainAppHandlers() {
|
|
|
2466
2458
|
}
|
|
2467
2459
|
|
|
2468
2460
|
restoreVcardIdentity();
|
|
2461
|
+
updateIdentityCardSummary();
|
|
2469
2462
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2463
|
+
// Snapshot of field values before editing (for Back/cancel)
|
|
2464
|
+
let _vcardEditSnapshot = {};
|
|
2465
|
+
|
|
2466
|
+
function snapshotVcardFields() {
|
|
2467
|
+
_vcardEditSnapshot = {};
|
|
2468
|
+
for (const id of vcardFieldIds) {
|
|
2469
|
+
const el = $(id);
|
|
2470
|
+
if (el) _vcardEditSnapshot[id] = el.value;
|
|
2471
|
+
}
|
|
2474
2472
|
}
|
|
2475
2473
|
|
|
2476
|
-
|
|
2477
|
-
|
|
2474
|
+
function restoreVcardSnapshot() {
|
|
2475
|
+
for (const id of vcardFieldIds) {
|
|
2476
|
+
const el = $(id);
|
|
2477
|
+
if (el && _vcardEditSnapshot[id] !== undefined) el.value = _vcardEditSnapshot[id];
|
|
2478
|
+
}
|
|
2478
2479
|
}
|
|
2479
2480
|
|
|
2481
|
+
// Edit button — switch to edit view
|
|
2482
|
+
$('identity-edit-btn')?.addEventListener('click', () => {
|
|
2483
|
+
setPhotoActionsVisible(false);
|
|
2484
|
+
stopCamera();
|
|
2485
|
+
snapshotVcardFields();
|
|
2486
|
+
$('vcard-form-view').style.display = 'none';
|
|
2487
|
+
$('vcard-edit-view').style.display = 'flex';
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2490
|
+
// Save button — persist and return to card view
|
|
2491
|
+
$('identity-save-btn')?.addEventListener('click', () => {
|
|
2492
|
+
saveVcardIdentity();
|
|
2493
|
+
updateIdentityCardSummary();
|
|
2494
|
+
setPhotoActionsVisible(false);
|
|
2495
|
+
$('vcard-edit-view').style.display = 'none';
|
|
2496
|
+
$('vcard-form-view').style.display = '';
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
// Back button — discard changes and return to card view
|
|
2500
|
+
$('identity-back-btn')?.addEventListener('click', () => {
|
|
2501
|
+
restoreVcardSnapshot();
|
|
2502
|
+
setPhotoActionsVisible(false);
|
|
2503
|
+
stopCamera();
|
|
2504
|
+
$('vcard-edit-view').style.display = 'none';
|
|
2505
|
+
$('vcard-form-view').style.display = '';
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
const photoActions = $('vcard-photo-actions');
|
|
2509
|
+
const photoEditBtn = $('vcard-photo-edit-btn');
|
|
2510
|
+
function setPhotoActionsVisible(visible) {
|
|
2511
|
+
if (photoActions) photoActions.classList.toggle('visible', visible);
|
|
2512
|
+
if (photoEditBtn) {
|
|
2513
|
+
photoEditBtn.classList.toggle('is-open', visible);
|
|
2514
|
+
photoEditBtn.title = visible ? 'Close Photo Menu' : 'Edit Photo';
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
function arePhotoActionsVisible() {
|
|
2518
|
+
return !!photoActions?.classList.contains('visible');
|
|
2519
|
+
}
|
|
2520
|
+
setPhotoActionsVisible(false);
|
|
2521
|
+
|
|
2522
|
+
function encodeVcardPhoto(source, sourceWidth, sourceHeight) {
|
|
2523
|
+
if (!source || !sourceWidth || !sourceHeight) return null;
|
|
2524
|
+
const maxDimension = 1024;
|
|
2525
|
+
const quality = 0.9;
|
|
2526
|
+
const scale = Math.min(1, maxDimension / Math.max(sourceWidth, sourceHeight));
|
|
2527
|
+
const outputWidth = Math.max(1, Math.round(sourceWidth * scale));
|
|
2528
|
+
const outputHeight = Math.max(1, Math.round(sourceHeight * scale));
|
|
2529
|
+
const canvas = document.createElement('canvas');
|
|
2530
|
+
canvas.width = outputWidth;
|
|
2531
|
+
canvas.height = outputHeight;
|
|
2532
|
+
const ctx = canvas.getContext('2d');
|
|
2533
|
+
if (!ctx) return null;
|
|
2534
|
+
ctx.imageSmoothingEnabled = true;
|
|
2535
|
+
ctx.imageSmoothingQuality = 'high';
|
|
2536
|
+
ctx.drawImage(source, 0, 0, sourceWidth, sourceHeight, 0, 0, outputWidth, outputHeight);
|
|
2537
|
+
return canvas.toDataURL('image/jpeg', quality);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
photoEditBtn?.addEventListener('click', (e) => {
|
|
2541
|
+
e.preventDefault();
|
|
2542
|
+
e.stopPropagation();
|
|
2543
|
+
if (arePhotoActionsVisible()) {
|
|
2544
|
+
setPhotoActionsVisible(false);
|
|
2545
|
+
stopCamera();
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
setPhotoActionsVisible(true);
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2480
2551
|
// Photo upload handler
|
|
2481
2552
|
$('vcard-photo-input')?.addEventListener('change', (e) => {
|
|
2482
2553
|
const file = e.target.files[0];
|
|
@@ -2485,19 +2556,12 @@ function setupMainAppHandlers() {
|
|
|
2485
2556
|
reader.onload = (ev) => {
|
|
2486
2557
|
const img = new Image();
|
|
2487
2558
|
img.onload = () => {
|
|
2488
|
-
const
|
|
2489
|
-
|
|
2490
|
-
canvas.width = size;
|
|
2491
|
-
canvas.height = size;
|
|
2492
|
-
const ctx = canvas.getContext('2d');
|
|
2493
|
-
const min = Math.min(img.width, img.height);
|
|
2494
|
-
const sx = (img.width - min) / 2;
|
|
2495
|
-
const sy = (img.height - min) / 2;
|
|
2496
|
-
ctx.drawImage(img, sx, sy, min, min, 0, 0, size, size);
|
|
2497
|
-
const dataUrl = canvas.toDataURL('image/jpeg', 0.7);
|
|
2559
|
+
const dataUrl = encodeVcardPhoto(img, img.width, img.height);
|
|
2560
|
+
if (!dataUrl) return;
|
|
2498
2561
|
state.vcardPhoto = dataUrl;
|
|
2499
2562
|
stopCamera();
|
|
2500
2563
|
showPhotoPreview(dataUrl);
|
|
2564
|
+
setPhotoActionsVisible(false);
|
|
2501
2565
|
saveVcardIdentity();
|
|
2502
2566
|
};
|
|
2503
2567
|
img.src = ev.target.result;
|
|
@@ -2526,6 +2590,7 @@ function setupMainAppHandlers() {
|
|
|
2526
2590
|
const cameraBtn = $('vcard-camera-btn');
|
|
2527
2591
|
if (cameraBtn) cameraBtn.style.display = '';
|
|
2528
2592
|
}
|
|
2593
|
+
setPhotoActionsVisible(false);
|
|
2529
2594
|
const modal = $('photo-remove-confirm-modal');
|
|
2530
2595
|
if (modal) modal.classList.remove('active');
|
|
2531
2596
|
});
|
|
@@ -2543,6 +2608,14 @@ function setupMainAppHandlers() {
|
|
|
2543
2608
|
if (placeholder) placeholder.style.display = '';
|
|
2544
2609
|
const video = $('vcard-camera-video');
|
|
2545
2610
|
if (video) video.style.display = 'none';
|
|
2611
|
+
const removeBtn = $('vcard-photo-remove');
|
|
2612
|
+
if (removeBtn) removeBtn.style.display = 'none';
|
|
2613
|
+
const uploadLabel = document.querySelector('label[for="vcard-photo-input"]');
|
|
2614
|
+
if (uploadLabel) uploadLabel.style.display = '';
|
|
2615
|
+
const cameraBtn = $('vcard-camera-btn');
|
|
2616
|
+
if (cameraBtn && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
2617
|
+
cameraBtn.style.display = '';
|
|
2618
|
+
}
|
|
2546
2619
|
}
|
|
2547
2620
|
|
|
2548
2621
|
function showPhotoPreview(dataUrl) {
|
|
@@ -2559,23 +2632,28 @@ function setupMainAppHandlers() {
|
|
|
2559
2632
|
preview.appendChild(img);
|
|
2560
2633
|
const removeBtn = $('vcard-photo-remove');
|
|
2561
2634
|
if (removeBtn) removeBtn.style.display = '';
|
|
2562
|
-
// Hide upload/camera buttons when photo is present
|
|
2563
2635
|
const uploadLabel = document.querySelector('label[for="vcard-photo-input"]');
|
|
2564
|
-
if (uploadLabel) uploadLabel.style.display = '
|
|
2636
|
+
if (uploadLabel) uploadLabel.style.display = '';
|
|
2565
2637
|
const cameraBtn = $('vcard-camera-btn');
|
|
2566
|
-
if (cameraBtn
|
|
2638
|
+
if (cameraBtn && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
2639
|
+
cameraBtn.style.display = '';
|
|
2640
|
+
}
|
|
2567
2641
|
}
|
|
2568
2642
|
|
|
2569
2643
|
// Camera support
|
|
2570
2644
|
let cameraStream = null;
|
|
2571
2645
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
2572
2646
|
const cameraBtn = $('vcard-camera-btn');
|
|
2573
|
-
if (cameraBtn
|
|
2647
|
+
if (cameraBtn) cameraBtn.style.display = '';
|
|
2574
2648
|
|
|
2575
2649
|
cameraBtn?.addEventListener('click', async () => {
|
|
2576
2650
|
try {
|
|
2577
2651
|
cameraStream = await navigator.mediaDevices.getUserMedia({
|
|
2578
|
-
video: {
|
|
2652
|
+
video: {
|
|
2653
|
+
facingMode: 'user',
|
|
2654
|
+
width: { ideal: 1280, max: 1920 },
|
|
2655
|
+
height: { ideal: 720, max: 1080 },
|
|
2656
|
+
}
|
|
2579
2657
|
});
|
|
2580
2658
|
const video = $('vcard-camera-video');
|
|
2581
2659
|
if (video) {
|
|
@@ -2590,6 +2668,10 @@ function setupMainAppHandlers() {
|
|
|
2590
2668
|
preview.querySelectorAll('img').forEach(el => el.style.display = 'none');
|
|
2591
2669
|
}
|
|
2592
2670
|
cameraBtn.style.display = 'none';
|
|
2671
|
+
const uploadLabel = document.querySelector('label[for="vcard-photo-input"]');
|
|
2672
|
+
if (uploadLabel) uploadLabel.style.display = 'none';
|
|
2673
|
+
const removeBtn = $('vcard-photo-remove');
|
|
2674
|
+
if (removeBtn) removeBtn.style.display = 'none';
|
|
2593
2675
|
const captureBtn = $('vcard-camera-capture');
|
|
2594
2676
|
const cancelBtn = $('vcard-camera-cancel');
|
|
2595
2677
|
if (captureBtn) captureBtn.style.display = '';
|
|
@@ -2603,21 +2685,14 @@ function setupMainAppHandlers() {
|
|
|
2603
2685
|
$('vcard-camera-capture')?.addEventListener('click', () => {
|
|
2604
2686
|
const video = $('vcard-camera-video');
|
|
2605
2687
|
if (!video) return;
|
|
2606
|
-
const canvas = document.createElement('canvas');
|
|
2607
|
-
const size = 128;
|
|
2608
|
-
canvas.width = size;
|
|
2609
|
-
canvas.height = size;
|
|
2610
|
-
const ctx = canvas.getContext('2d');
|
|
2611
2688
|
const vw = video.videoWidth;
|
|
2612
2689
|
const vh = video.videoHeight;
|
|
2613
|
-
const
|
|
2614
|
-
|
|
2615
|
-
const sy = (vh - min) / 2;
|
|
2616
|
-
ctx.drawImage(video, sx, sy, min, min, 0, 0, size, size);
|
|
2617
|
-
const dataUrl = canvas.toDataURL('image/jpeg', 0.7);
|
|
2690
|
+
const dataUrl = encodeVcardPhoto(video, vw, vh);
|
|
2691
|
+
if (!dataUrl) return;
|
|
2618
2692
|
state.vcardPhoto = dataUrl;
|
|
2619
2693
|
stopCamera();
|
|
2620
2694
|
showPhotoPreview(dataUrl);
|
|
2695
|
+
setPhotoActionsVisible(false);
|
|
2621
2696
|
saveVcardIdentity();
|
|
2622
2697
|
});
|
|
2623
2698
|
|
|
@@ -2642,9 +2717,13 @@ function setupMainAppHandlers() {
|
|
|
2642
2717
|
video.style.display = 'none';
|
|
2643
2718
|
}
|
|
2644
2719
|
const cameraBtn = $('vcard-camera-btn');
|
|
2720
|
+
const uploadLabel = document.querySelector('label[for="vcard-photo-input"]');
|
|
2721
|
+
const removeBtn = $('vcard-photo-remove');
|
|
2645
2722
|
const captureBtn = $('vcard-camera-capture');
|
|
2646
2723
|
const cancelBtn = $('vcard-camera-cancel');
|
|
2647
|
-
if (
|
|
2724
|
+
if (uploadLabel) uploadLabel.style.display = '';
|
|
2725
|
+
if (removeBtn) removeBtn.style.display = state.vcardPhoto ? '' : 'none';
|
|
2726
|
+
if (cameraBtn && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) cameraBtn.style.display = '';
|
|
2648
2727
|
if (captureBtn) captureBtn.style.display = 'none';
|
|
2649
2728
|
if (cancelBtn) cancelBtn.style.display = 'none';
|
|
2650
2729
|
}
|
|
@@ -2845,7 +2924,9 @@ function setupMainAppHandlers() {
|
|
|
2845
2924
|
middleName: $('vcard-middlename')?.value || '',
|
|
2846
2925
|
lastName: $('vcard-lastname')?.value || '',
|
|
2847
2926
|
suffix: $('vcard-suffix')?.value || '',
|
|
2927
|
+
nickname: $('vcard-nickname')?.value || '',
|
|
2848
2928
|
email: $('vcard-email')?.value || '',
|
|
2929
|
+
phone: $('vcard-phone')?.value || '',
|
|
2849
2930
|
org: $('vcard-org')?.value || '',
|
|
2850
2931
|
title: $('vcard-title')?.value || '',
|
|
2851
2932
|
includeKeys: true,
|
|
@@ -2856,10 +2937,9 @@ function setupMainAppHandlers() {
|
|
|
2856
2937
|
return;
|
|
2857
2938
|
}
|
|
2858
2939
|
|
|
2859
|
-
const vcard = generateVCard(info);
|
|
2860
|
-
const vcardForQR = generateVCard(info, { skipPhoto: true });
|
|
2861
|
-
|
|
2862
|
-
if (vcardPreview) vcardPreview.textContent = vcard;
|
|
2940
|
+
const vcard = signVCard(generateVCard(info));
|
|
2941
|
+
const vcardForQR = signVCard(generateVCard(info, { skipPhoto: true }));
|
|
2942
|
+
state._exportedVCard = vcard;
|
|
2863
2943
|
|
|
2864
2944
|
try {
|
|
2865
2945
|
const qrCanvas = $('qr-code');
|
|
@@ -2874,11 +2954,49 @@ function setupMainAppHandlers() {
|
|
|
2874
2954
|
const resultView = $('vcard-result-view');
|
|
2875
2955
|
if (formView) formView.style.display = 'none';
|
|
2876
2956
|
if (resultView) resultView.style.display = '';
|
|
2957
|
+
|
|
2958
|
+
// Show/hide signature badge
|
|
2959
|
+
const sigBadge = $('vcard-sig-badge');
|
|
2960
|
+
if (sigBadge) {
|
|
2961
|
+
sigBadge.style.display = state.wallet?.ed25519?.privateKey ? 'flex' : 'none';
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
// Populate raw view (strip PHOTO base64 data)
|
|
2965
|
+
const rawView = $('vcard-raw-view');
|
|
2966
|
+
if (rawView) {
|
|
2967
|
+
const rawText = vcard.replace(/PHOTO;ENCODING=b;TYPE=\w+:[\s\S]*?(?=\n[A-Z])/m, 'PHOTO:[image data omitted]');
|
|
2968
|
+
rawView.textContent = rawText;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// Reset toggle to QR
|
|
2972
|
+
$('vcard-toggle-qr')?.classList.add('active');
|
|
2973
|
+
$('vcard-toggle-raw')?.classList.remove('active');
|
|
2974
|
+
document.querySelector('.qr-container')?.style.setProperty('display', '');
|
|
2975
|
+
if (rawView) rawView.style.display = 'none';
|
|
2877
2976
|
} catch (err) {
|
|
2878
2977
|
alert('Error generating QR code: ' + err.message);
|
|
2879
2978
|
}
|
|
2880
2979
|
});
|
|
2881
2980
|
|
|
2981
|
+
// Toggle QR / Raw
|
|
2982
|
+
$('vcard-toggle-qr')?.addEventListener('click', () => {
|
|
2983
|
+
$('vcard-toggle-qr')?.classList.add('active');
|
|
2984
|
+
$('vcard-toggle-raw')?.classList.remove('active');
|
|
2985
|
+
const qr = document.querySelector('#vcard-result-view .qr-container');
|
|
2986
|
+
const raw = $('vcard-raw-view');
|
|
2987
|
+
if (qr) qr.style.display = '';
|
|
2988
|
+
if (raw) raw.style.display = 'none';
|
|
2989
|
+
});
|
|
2990
|
+
|
|
2991
|
+
$('vcard-toggle-raw')?.addEventListener('click', () => {
|
|
2992
|
+
$('vcard-toggle-raw')?.classList.add('active');
|
|
2993
|
+
$('vcard-toggle-qr')?.classList.remove('active');
|
|
2994
|
+
const qr = document.querySelector('#vcard-result-view .qr-container');
|
|
2995
|
+
const raw = $('vcard-raw-view');
|
|
2996
|
+
if (qr) qr.style.display = 'none';
|
|
2997
|
+
if (raw) raw.style.display = '';
|
|
2998
|
+
});
|
|
2999
|
+
|
|
2882
3000
|
// Back to editor from result view
|
|
2883
3001
|
$('vcard-back-btn')?.addEventListener('click', () => {
|
|
2884
3002
|
const resultView = $('vcard-result-view');
|
|
@@ -2889,7 +3007,7 @@ function setupMainAppHandlers() {
|
|
|
2889
3007
|
|
|
2890
3008
|
// Download vCard
|
|
2891
3009
|
$('download-vcard')?.addEventListener('click', () => {
|
|
2892
|
-
const vcard =
|
|
3010
|
+
const vcard = state._exportedVCard || '';
|
|
2893
3011
|
const blob = new Blob([vcard], { type: 'text/vcard' });
|
|
2894
3012
|
const url = URL.createObjectURL(blob);
|
|
2895
3013
|
const a = document.createElement('a');
|
|
@@ -2901,7 +3019,7 @@ function setupMainAppHandlers() {
|
|
|
2901
3019
|
|
|
2902
3020
|
// Copy vCard
|
|
2903
3021
|
$('copy-vcard')?.addEventListener('click', async () => {
|
|
2904
|
-
const vcard =
|
|
3022
|
+
const vcard = state._exportedVCard || '';
|
|
2905
3023
|
try {
|
|
2906
3024
|
await navigator.clipboard.writeText(vcard);
|
|
2907
3025
|
const btn = $('copy-vcard');
|
|
@@ -3091,7 +3209,7 @@ function setupTrustHandlers() {
|
|
|
3091
3209
|
return;
|
|
3092
3210
|
}
|
|
3093
3211
|
const { exportTrustData } = await import('./trust-ui.js');
|
|
3094
|
-
const xpub = state.hdRoot ? state.hdRoot.
|
|
3212
|
+
const xpub = state.hdRoot ? state.hdRoot.toXpub() : '';
|
|
3095
3213
|
exportTrustData(state.trustTransactions, xpub);
|
|
3096
3214
|
});
|
|
3097
3215
|
|
|
@@ -3158,7 +3276,7 @@ function setupTrustHandlers() {
|
|
|
3158
3276
|
}
|
|
3159
3277
|
|
|
3160
3278
|
// Update encryption tab when it becomes active or HD controls change
|
|
3161
|
-
$qa('.modal-tab[data-modal-tab="
|
|
3279
|
+
$qa('.modal-tab[data-modal-tab="messaging-tab-content"]').forEach(tab => {
|
|
3162
3280
|
tab.addEventListener('click', () => {
|
|
3163
3281
|
if (state.hdRoot) updateEncryptionTab();
|
|
3164
3282
|
});
|