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.
33 lines
7.2 KiB
JavaScript
33 lines
7.2 KiB
JavaScript
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();
|