diff --git a/hubzilla/addon/g1wallet/view/js/g1wallet.js b/hubzilla/addon/g1wallet/view/js/g1wallet.js
new file mode 100644
index 0000000..cdddc21
--- /dev/null
+++ b/hubzilla/addon/g1wallet/view/js/g1wallet.js
@@ -0,0 +1,241 @@
+/**
+ * g1wallet.js — Ğ1 Wallet client-side skeleton.
+ *
+ * At this stage:
+ * - The g1wallet namespace and session object are declared.
+ * - The unlock button is wired — it intercepts the click but does NOT derive keys yet.
+ * - The event API stubs are in place for cry01 and poll01 to listen to.
+ * - scrypt-js is loaded (vendor/scrypt-js-3.0.1.min.js) but not yet called.
+ *
+ * CRITICAL RULES (never relax these):
+ * - The private key NEVER leaves the browser.
+ * - The pseudo and password NEVER leave the browser.
+ * - No key material in localStorage, sessionStorage, or cookies.
+ * - The private key object is marked non-extractable in WebCrypto.
+ * - One document, one confirmation, one click — no batch signing.
+ */
+
+(function () {
+ 'use strict';
+
+ // ------------------------------------------------------------------------
+ // SESSION STATE
+ // Session is held in module-scope memory only.
+ // It is gone when the page is closed or navigated away from.
+ // ------------------------------------------------------------------------
+
+ var _session = {
+ unlocked: false,
+ pubkey: null, // base58-encoded Ed25519 public key
+ // _privkey is intentionally not stored here — it lives only in the
+ // WebCrypto CryptoKey object (non-extractable) once derivation is implemented.
+ _cryptoKey: null // CryptoKey (non-extractable) — null until derivation
+ };
+
+ // ------------------------------------------------------------------------
+ // PUBLIC API
+ // Exposed on window.g1wallet for other addons to use.
+ // ------------------------------------------------------------------------
+
+ window.g1wallet = {
+
+ /**
+ * Returns true if the wallet is currently unlocked.
+ */
+ isUnlocked: function () {
+ return _session.unlocked;
+ },
+
+ /**
+ * Returns the current session pubkey (base58 string), or null if locked.
+ */
+ getPubkey: function () {
+ return _session.unlocked ? _session.pubkey : null;
+ },
+
+ /**
+ * Requests a signature from the wallet for a given document.
+ * If the wallet is locked, dispatches 'g1wallet:sign_request_blocked' instead.
+ * Once signing is implemented, dispatches the result via the callback event.
+ *
+ * @param {string} document - The document string to sign
+ * @param {string} callback - Event name to dispatch with the signed result
+ */
+ requestSignature: function (document, callback) {
+ if (!_session.unlocked || !_session._cryptoKey) {
+ window.dispatchEvent(new CustomEvent('g1wallet:sign_request_blocked', {
+ detail: { reason: 'Wallet is locked. Please unlock your wallet first.' }
+ }));
+ return;
+ }
+ // TODO: implement signing via SubtleCrypto.sign() with Ed25519.
+ // The signed bytes are base64-encoded and dispatched via the callback event.
+ console.warn('[g1wallet] requestSignature: signing not yet implemented.');
+ }
+ };
+
+ // ------------------------------------------------------------------------
+ // WIDGET API
+ // Used by Widget/G1wallet.php inline script to register widget instances.
+ // ------------------------------------------------------------------------
+
+ window.G1WalletWidget = {
+ init: function (uid, walletUrl) {
+ // Listen for session events and update the widget DOM.
+ window.addEventListener('g1wallet:unlocked', function (e) {
+ var statusEl = document.getElementById(uid + '-status');
+ if (!statusEl) return;
+ var pubkey = e.detail.pubkey || '';
+ var short = pubkey.substring(0, 12) + '…';
+ statusEl.innerHTML =
+ '
' +
+ '
🔓 ' +
+ '
Unlocked' +
+ '
' +
+ _escHtml(short) +
+ '
';
+ });
+
+ window.addEventListener('g1wallet:locked', function () {
+ var statusEl = document.getElementById(uid + '-status');
+ if (!statusEl) return;
+ statusEl.innerHTML =
+ '' +
+ '
🔒 ' +
+ '
Locked' +
+ '
' +
+ '
';
+ });
+ }
+ };
+
+ // ------------------------------------------------------------------------
+ // UNLOCK BUTTON — wired on the wallet landing page
+ // ------------------------------------------------------------------------
+
+ document.addEventListener('DOMContentLoaded', function () {
+ var unlockBtn = document.getElementById('g1wallet-unlock-btn');
+ var lockedView = document.getElementById('g1wallet-locked-view');
+ var unlockedView = document.getElementById('g1wallet-unlocked-view');
+ var spinner = document.getElementById('g1wallet-unlock-spinner');
+ var errorEl = document.getElementById('g1wallet-unlock-error');
+ var lockBtn = document.getElementById('g1wallet-lock-btn');
+
+ if (!unlockBtn) return; // Not on the wallet page.
+
+ unlockBtn.addEventListener('click', function () {
+ var pseudo = (document.getElementById('g1wallet-pseudo') || {}).value || '';
+ var password = (document.getElementById('g1wallet-password') || {}).value || '';
+
+ _clearError();
+
+ if (!pseudo || !password) {
+ _showError('Both pseudo and password are required.');
+ return;
+ }
+
+ // TODO: call scrypt derivation here.
+ // scrypt(pseudo, password, N=4096, r=16, p=1) → 32-byte seed → Ed25519 keypair
+ // For now: log and show a placeholder message.
+ console.log('[g1wallet] Unlock clicked. Derivation not yet implemented.');
+
+ if (spinner) spinner.style.display = 'inline';
+ unlockBtn.disabled = true;
+
+ // Simulate async derivation (placeholder).
+ setTimeout(function () {
+ if (spinner) spinner.style.display = 'none';
+ unlockBtn.disabled = false;
+ _showError('Key derivation not yet implemented. This is a skeleton build.');
+ }, 500);
+ });
+
+ if (lockBtn) {
+ lockBtn.addEventListener('click', function () {
+ _lockWallet();
+ if (lockedView) lockedView.style.display = '';
+ if (unlockedView) unlockedView.style.display = 'none';
+ // Clear credential fields.
+ var pseudoEl = document.getElementById('g1wallet-pseudo');
+ var passwordEl = document.getElementById('g1wallet-password');
+ if (pseudoEl) pseudoEl.value = '';
+ if (passwordEl) passwordEl.value = '';
+ });
+ }
+
+ // Listen for wallet:unlocked event (dispatched by _unlockWallet below).
+ window.addEventListener('g1wallet:unlocked', function (e) {
+ if (lockedView) lockedView.style.display = 'none';
+ if (unlockedView) unlockedView.style.display = '';
+ var pubkeyDisplay = document.getElementById('g1wallet-pubkey-display');
+ if (pubkeyDisplay) pubkeyDisplay.textContent = e.detail.pubkey || '—';
+ });
+ });
+
+ // ------------------------------------------------------------------------
+ // INTERNAL HELPERS
+ // ------------------------------------------------------------------------
+
+ function _unlockWallet(pubkey, cryptoKey) {
+ _session.unlocked = true;
+ _session.pubkey = pubkey;
+ _session._cryptoKey = cryptoKey;
+ window.dispatchEvent(new CustomEvent('g1wallet:unlocked', {
+ detail: { pubkey: pubkey }
+ }));
+ }
+
+ function _lockWallet() {
+ _session.unlocked = false;
+ _session.pubkey = null;
+ _session._cryptoKey = null;
+ window.dispatchEvent(new CustomEvent('g1wallet:locked'));
+ }
+
+ function _showError(message) {
+ var el = document.getElementById('g1wallet-unlock-error');
+ if (el) {
+ el.textContent = message;
+ el.style.display = '';
+ }
+ }
+
+ function _clearError() {
+ var el = document.getElementById('g1wallet-unlock-error');
+ if (el) {
+ el.textContent = '';
+ el.style.display = 'none';
+ }
+ }
+
+ function _escHtml(str) {
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+ }
+
+ // ------------------------------------------------------------------------
+ // AUTO-POPULATE: cry01 and poll01 form fields
+ // Other addons place data-g1wallet-target="pubkey" on their pubkey input fields.
+ // When the wallet unlocks, those fields are populated automatically.
+ // ------------------------------------------------------------------------
+
+ window.addEventListener('g1wallet:unlocked', function (e) {
+ var pubkey = e.detail.pubkey || '';
+ var targets = document.querySelectorAll('[data-g1wallet-target="pubkey"]');
+ targets.forEach(function (el) {
+ el.value = pubkey;
+ });
+ });
+
+ window.addEventListener('g1wallet:locked', function () {
+ var targets = document.querySelectorAll('[data-g1wallet-target="pubkey"]');
+ targets.forEach(function (el) {
+ el.value = '';
+ });
+ });
+
+}());