Files
archy/docker/justworks/business/public/assets/login-panel.js
T
yaya 94899e69d3 feat: integrate Just Works app and native business setup
Preserve hosted website UX inside Archipelago, add isolated Business and owner-authorized native wallet setup, and include deployment handoff. Based on main with native signer import already accepted.
2026-10-08 19:39:38 +01:00

33 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {npubFrom,prod} from './ds/env.js';
import {transferSession} from './business-sso.js';
let npub=npubFrom();
let mounted=false;
const sessionKey=`jw.business.login.${npub}`;
function mount(){if(mounted)return;const header=document.querySelector('.top,body>main header,header');if(!header)return;mounted=true;const trigger=document.createElement('button');trigger.className='jw-login-open';trigger.type='button';trigger.innerHTML='Login <span>with login.justworks.cash</span>';header.append(trigger);sessionStorage.removeItem(sessionKey);sessionStorage.removeItem('jw.business.signer');refreshSession(trigger);document.body.insertAdjacentHTML('beforeend',`<dialog class="jw-login" aria-labelledby="jwLoginTitle"><button class="jw-login-close" aria-label="Close">×</button><p class="jw-kicker">Merchant access</p><h2 id="jwLoginTitle">Just Works Login</h2><p class="jw-intro">Login with the identity you use for your business.</p><form class="jw-simple-login"><label>Login<input name="ownerIdentifier" autocomplete="username" placeholder="Email or identity" required></label><label>Password<input name="ownerPassword" type="password" autocomplete="current-password" required></label><button type="submit">Login <span>→</span></button><p class="jw-login-status" role="status"></p></form><button class="jw-more" type="button" aria-expanded="false">… more options <span>+</span></button><section class="jw-classic" hidden><form class="jw-nsec-login"><label><span><b>Raw nsec</b><small>Expert owner-key login</small></span><input name="ownerKey" type="password" placeholder="nsec1… or ncryptsec1…" autocomplete="off" required></label><button type="submit">Login with owner key</button></form><button type="button" data-nip07><span><b>Browser extension</b><small>NIP-07 · Alby, nos2x and compatible signers</small></span><i>Connect</i></button><label><span><b>Nostr bunker</b><small>NIP-46 remote signer</small></span><input data-bunker placeholder="bunker://… or nostrconnect://…"><button type="button" data-bunker-connect>Connect</button></label><a href="intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end"><span><b>Amber</b><small>Android Nostr signer</small></span><i>Open</i></a><p class="jw-expert-status" role="status"></p></section><footer><span>Non-custodial</span><a href="https://login.justworks.cash/docs" target="_blank" rel="noopener">How Just Works Login works ↗</a></footer></dialog>`);const dialog=document.querySelector('.jw-login'),status=dialog.querySelector('.jw-login-status'),expertStatus=dialog.querySelector('.jw-expert-status');if(!prod){dialog.querySelector('[name="ownerIdentifier"]').value='demo';dialog.querySelector('[name="ownerPassword"]').value='demo';dialog.querySelector('.jw-intro').insertAdjacentHTML('beforeend',' <b class="jw-dev-note">Dev · demo login prefilled</b>')}trigger.onclick=()=>dialog.showModal();dialog.querySelector('.jw-login-close').onclick=()=>dialog.close();dialog.onclick=e=>{if(e.target===dialog)dialog.close()};dialog.querySelector('.jw-more').onclick=function(){const classic=dialog.querySelector('.jw-classic'),open=classic.hidden;classic.hidden=!open;this.setAttribute('aria-expanded',String(open));this.querySelector('span').textContent=open?'−':'+'};dialog.querySelector('.jw-simple-login').onsubmit=event=>login(event,status);dialog.querySelector('.jw-nsec-login').onsubmit=event=>login(event,expertStatus);dialog.querySelector('[data-nip07]').onclick=async()=>{try{if(!window.nostr)throw new Error('No Nostr browser extension found.');const pubkey=await window.nostr.getPublicKey();sessionStorage.setItem('jw.business.signer',JSON.stringify({type:'nip07',pubkey}));expertStatus.textContent=`Extension connected · ${pubkey.slice(0,12)}…`;expertStatus.classList.remove('error')}catch(error){expertStatus.textContent=error.message;expertStatus.classList.add('error')}};dialog.querySelector('[data-bunker-connect]').onclick=()=>{const value=dialog.querySelector('[data-bunker]').value.trim();if(!/^(bunker|nostrconnect):\/\//i.test(value)){expertStatus.textContent='Enter a valid bunker:// or nostrconnect:// URI.';expertStatus.classList.add('error');return}window.jwBusinessSigner={type:'nip46',uri:value};expertStatus.textContent='Bunker connected for this page only.';expertStatus.classList.remove('error')}}
async function login(event,status){event.preventDefault();const form=event.currentTarget,button=form.querySelector('button[type="submit"]');button.disabled=true;status.textContent='Checking your Just Works Login…';status.classList.remove('error');const values=Object.fromEntries(new FormData(form));try{const response=await fetch('/api/business-owner-login',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({npub,...values})}),data=await response.json();if(!response.ok)throw new Error(data.error||'Login failed');form.querySelectorAll('input[type="password"]').forEach(input=>input.value='');status.textContent=`Logged in to ${data.merchant}.`;button.innerHTML='Logged in ✓';await refreshSession(document.querySelector('.jw-login-open'));setTimeout(()=>document.querySelector('.jw-login').close(),900)}catch(error){status.textContent=error.message;status.classList.add('error');button.disabled=false;button.innerHTML='Try again <span>↻</span>'}finally{form.querySelectorAll('input[type="password"]').forEach(input=>input.value='')}}
async function refreshSession(trigger){
try {
const response=await fetch('/api/business-session',{cache:'no-store',credentials:'same-origin'});
const data=await response.json();
if(!npub&&data.legacy_npub)npub=data.legacy_npub;
const authenticated=data.authenticated&&data.session?.npub===npub;
trigger.innerHTML=authenticated?'Logged in <span>Just Works</span>':'Login <span>with login.justworks.cash</span>';
let logout=document.querySelector('.jw-logout');
if(authenticated&&!logout){
logout=document.createElement('button');logout.type='button';logout.className='jw-logout';logout.textContent='Logout';
trigger.after(logout);
logout.onclick=async()=>{await fetch('/api/business-logout',{method:'POST',headers:{'content-type':'application/json'},body:'{}'});logout.remove();await refreshSession(trigger)};
}
if(!authenticated&&logout)logout.remove();
const form=document.querySelector('.jw-simple-login');
let transfer=document.querySelector('.jw-session-transfer');
if(data.sso_enabled&&!authenticated&&npub&&!location.pathname.endsWith('/business-sso')&&form&&!transfer){
transfer=document.createElement('button');transfer.type='button';transfer.className='jw-session-transfer';transfer.textContent='Use existing Business session';form.before(transfer);
transfer.onclick=async()=>{transfer.disabled=true;const status=document.querySelector('.jw-login-status');try{await transferSession(npub,status)}catch(error){status.textContent=error.message;transfer.disabled=false}};
}
if((authenticated||!data.sso_enabled)&&transfer)transfer.remove();
window.dispatchEvent(new CustomEvent('jw-session-changed',{detail:{authenticated,npub}}));
} catch { trigger.innerHTML='Login <span>with login.justworks.cash</span>'; }
}
const observer=new MutationObserver(()=>{mount();if(mounted)observer.disconnect()});observer.observe(document.documentElement,{childList:true,subtree:true});mount();