This commit is contained in:
Your Name
2025-09-09 09:32:09 -04:00
commit 37fb89c0a9
135 changed files with 36437 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"quoteProps": "consistent",
"printWidth": 180,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}

167
packages/auth/README.md Normal file
View File

@@ -0,0 +1,167 @@
Nostr-Login
===========
This library is a powerful `window.nostr` provider.
```
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js'></script>
```
Just add the above script to your HTML and
get a nice UI for users to login with Nostr Connect (nip46), with an extension, read-only login,
account switching, OAuth-like sign up, etc. Your app just talks to the `window.nostr`, the rest is handled by `nostr-login`.
See it in action on [nostr.band](https://nostr.band).
## Options
You can set these attributes to the `script` tag to customize the behavior:
- `data-dark-mode` - `true`/`false`, default will use the browser's color theme
- `data-bunkers` - the comma-separated list of domain names of Nostr Connect (nip46) providers for sign up, i.e. `nsec.app,highlighter.com`
- `data-perms` - the comma-separated list of [permissions](https://github.com/nostr-protocol/nips/blob/master/46.md#requested-permissions) requested by the app over Nostr Connect, i.e. `sign_event:1,nip04_encrypt`
- `data-theme` - color themes, one of `default`, `ocean`, `lemonade`, `purple`
- `data-no-banner` - if `true`, do not show the `nostr-login` banner, will need to launch the modals using event dispatch, see below
- `data-methods` - comma-separated list of allowed auth methods, method names: `connect`, `extension`, `readOnly`, `local`, all allowed by default.
- `data-otp-request-url` - URL for requesting OTP code
- `data-otp-reply-url` - URL for replying with OTP code
- `data-title` - title for the welcome screen
- `data-description` - description for the welcome screen
- `data-start-screen` - screen shown by default (banner click, window.nostr.* call), options: `welcome`, `welcome-login`, `welcome-signup`, `signup`, `local-signup`, `login`, `otp`, `connect`, `login-bunker-url`, `login-read-only`, `connection-string`, `switch-account`, `import`
- `data-signup-relays` - comma-separated list of relays where nip65 event will be published on local signup
- `data-outbox-relays` - comma-separated list of relays that will be added to nip65 event on local signup
- `data-signup-nstart` - "true" to use start.njump.me instead of local signup
- `data-follow-npubs` - comma-separated list of npubs to follow if njump.me signup is used
Example:
```
<script src='https://www.unpkg.com/nostr-login@latest/dist/unpkg.js' data-perms="sign_event:1,sign_event:0" data-theme="ocean"></script>
```
## Updating the UI
Whenever user performs an auth-related action using `nostr-login`, a `nlAuth` event will be dispatched on the `document`, which you can listen
to in order to update your UI (show user profile, etc):
```
document.addEventListener('nlAuth', (e) => {
// type is login, signup or logout
if (e.detail.type === 'login' || e.detail.type === 'signup') {
onLogin(); // get pubkey with window.nostr and show user profile
} else {
onLogout() // clear local user data, hide profile info
}
})
```
## Launching, logout, etc
The `nostr-login` auth modals will be automatically launched whenever you
make a call to `window.nostr` if user isn't authed yet. However, you can also launch the auth flow by dispatching a custom `nlLaunch` event:
```
document.dispatchEvent(new CustomEvent('nlLaunch', { detail: 'welcome' }));
```
The `detail` event payload can be empty, or can be one of `welcome`, `signup`, `login`, `login-bunker-url`, `login-read-only`, `switch-account`.
To trigger logout in the `nostr-login`, you can dispatch a `nlLogout` event:
```
document.dispatchEvent(new Event("nlLogout"));
```
To change dark mode in the `nostr-login`, you can dispatch a `nlDarkMode` event, with detail as `darkMode` boolean:
```
document.dispatchEvent(new CustomEvent("nlDarkMode", { detail: true }));
```
## Use as a package
Install `nostr-login` package with `npm` and then:
```
import { init as initNostrLogin } from "nostr-login"
// make sure this is called before any
// window.nostr calls are made
initNostrLogin({/*options*/})
```
Now the `window.nostr` will be initialized and on your first call
to it the auth flow will be launched if user isn't authed yet.
You can also launch the auth flow yourself:
```
import { launch as launchNostrLoginDialog } from "nostr-login"
// make sure init() was called
// on your signup button click
function onSignupClick() {
// launch signup screen
launchNostrLoginDialog({
startScreen: 'signup'
})
}
```
### Next.js Fix for Server Side Rendering (SSR)
`nostr-login` calls `document` which is unavailable for server-side rendering. You will have build errors. To fix this, you can import `nostr-login` on the client side in your component with a `useEffect` like this:
```javascript
useEffect(() => {
import('nostr-login')
.then(async ({ init }) => {
init({
// options
})
})
.catch((error) => console.log('Failed to load nostr-login', error));
}, []);
```
Note: even if your component has `"use client"` in the first line, this fix still may be necessary.
---
API:
- `init(opts)` - set mapping of window.nostr to nostr-login
- `launch(startScreen)` - launch nostr-login UI
- `logout()` - drop the current nip46 connection
Options:
- `theme` - same as `data-theme` above
- `startScreen` - same as `startScreen` for `nlLaunch` event above
- `bunkers` - same as `data-bunkers` above
- `devOverrideBunkerOrigin` - for testing, overrides the bunker origin for local setup
- `onAuth: (npub: string, options: NostrLoginAuthOptions)` - a callback to provide instead of listening to `nlAuth` event
- `perms` - same as `data-perms` above
- `darkMode` - same as `data-dark-mode` above
- `noBanner` - same as `data-no-banner` above
- `isSignInWithExtension` - `true` to bring the *Sign in with exception* button into main list of options, `false` to hide to the *Advanced*, default will behave as `true` if extension is detected.
## OTP login
If you supply both `data-otp-request-url` and `data-otp-reply-url` then "Login with DM" button will appear on the welcome screen.
When user enters their nip05 or npub, a GET request is made to `<data-otp-request-url>[?&]pubkey=<user-pubkey>`. Server should send
a DM with one-time code to that pubkey and should return 200.
After user enters the code, a GET request is made to `<data-otp-reply-url>[?&]pubkey=<user-pubkey>&code=<code>`. Server should check that code matches the pubkey and hasn't expired, and should return 200 status and an optional payload. Nostr-login will deliver the payload as `otpData` field in `nlAuth` event, and will save the payload in localstore and will deliver it again as `nlAuth` on page reload.
The reply payload may be used to supply the session token. If token is sent by the server as a cookie then payload might be empty, otherwise the payload should be used by the app to extract the token and use it in future API calls to the server.
## Examples
* [Basic HTML Example](./examples/usage.html)
## TODO
- fetch bunker list using NIP-89
- Amber support
- allow use without the UIs
- add timeout handling
- more at [issues](https://github.com/nostrband/nostr-login/issues)

30
packages/auth/index.html Normal file
View File

@@ -0,0 +1,30 @@
<!doctype html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" />
<title>Modal Auth Demo</title>
<script type="module">
// import { init } from './dist/index.esm.js';
//
// const test = async () => {
// const bunkerUrl = await launch({
// theme: 'purple',
// startScreen: 'signup',
// });
//
// console.log(bunkerUrl);
// };
</script>
<script src="./dist/unpkg.js" data-start-screen="local-signup"
data-signup-relays="wss://relay.nostr.band/,wss://relay.primal.net"></script>
</head>
<body>
<!--<nl-button title-btn="Sign in" start-screen="login" nl-theme="lemonade"></nl-button>-->
</body>
</html>

View File

@@ -0,0 +1,28 @@
{
"name": "nostr-login",
"version": "1.7.11",
"description": "",
"main": "./dist/index.esm.js",
"types": "./dist/index.d.ts",
"type": "module",
"scripts": {
"build": "rollup -c",
"format": "npx prettier --write src"
},
"author": "a-fralou",
"dependencies": {
"@nostr-dev-kit/ndk": "^2.3.1",
"nostr-tools": "^1.17.0",
"tseep": "^1.2.1"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"nostr-login-components": "^1.0.3",
"prettier": "^3.2.2",
"rollup": "^4.9.6",
"rollup-plugin-typescript2": "^0.36.0"
},
"license": "MIT"
}

View File

@@ -0,0 +1,55 @@
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import typescript from 'rollup-plugin-typescript2';
import terser from '@rollup/plugin-terser';
export default [
{
input: 'src/index.ts',
output: [
{
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true,
},
],
plugins: [
typescript({
tsconfig: 'tsconfig.json',
}),
resolve({
browser: true
}),
commonjs(),
terser({
compress: {
toplevel: true,
}
})
],
},
{
input: 'src/iife-module.ts',
output: [
{
file: 'dist/unpkg.js',
format: 'iife',
}
],
plugins: [
typescript({
tsconfig: 'tsconfig.json',
}),
resolve({
browser: true
}),
commonjs(),
terser({
compress: {
toplevel: true,
},
})
],
}
];

View File

@@ -0,0 +1 @@
export const CALL_TIMEOUT = 5000;

View File

@@ -0,0 +1,81 @@
import { init } from './index';
import { NostrLoginOptions, StartScreens } from './types';
// wrap to hide local vars
(() => {
// currentScript only visible in global scope code, not event handlers
const cs = document.currentScript;
const start = async () => {
const options: NostrLoginOptions = {};
if (cs) {
const dm = cs.getAttribute('data-dark-mode');
if (dm) options.darkMode = dm === 'true';
const bunkers = cs.getAttribute('data-bunkers');
if (bunkers) options.bunkers = bunkers;
const startScreen = cs.getAttribute('data-start-screen');
if (startScreen) options.startScreen = startScreen as StartScreens;
const perms = cs.getAttribute('data-perms');
if (perms) options.perms = perms;
const theme = cs.getAttribute('data-theme');
if (theme) options.theme = theme;
const noBanner = cs.getAttribute('data-no-banner');
if (noBanner) options.noBanner = noBanner === 'true';
const localSignup = cs.getAttribute('data-local-signup');
if (localSignup) options.localSignup = localSignup === 'true';
const signupNjump = cs.getAttribute('data-signup-nstart') || cs.getAttribute('data-signup-njump');
if (signupNjump) options.signupNstart = signupNjump === 'true';
const followNpubs = cs.getAttribute('data-follow-npubs');
if (followNpubs) options.followNpubs = followNpubs;
const otpRequestUrl = cs.getAttribute('data-otp-request-url');
if (otpRequestUrl) options.otpRequestUrl = otpRequestUrl;
const otpReplyUrl = cs.getAttribute('data-otp-reply-url');
if (otpReplyUrl) options.otpReplyUrl = otpReplyUrl;
if (!!otpRequestUrl !== !!otpReplyUrl) console.warn('nostr-login: need request and reply urls for OTP auth');
const methods = cs.getAttribute('data-methods');
if (methods) {
// @ts-ignore
options.methods = methods
.trim()
.split(',')
.filter(m => !!m);
}
const title = cs.getAttribute('data-title');
if (title) options.title = title;
const description = cs.getAttribute('data-description');
if (description) options.description = description;
const signupRelays = cs.getAttribute('data-signup-relays');
if (signupRelays) options.signupRelays = signupRelays;
const outboxRelays = cs.getAttribute('data-outbox-relays');
if (outboxRelays) options.outboxRelays = outboxRelays.split(',');
const dev = cs.getAttribute('data-dev') === 'true';
if (dev) options.dev = dev;
const custom = cs.getAttribute('data-custom-nostr-connect') === 'true';
if (custom) options.customNostrConnect = custom;
console.log('nostr-login options', options);
}
init(options);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
else start();
})();

347
packages/auth/src/index.ts Normal file
View File

@@ -0,0 +1,347 @@
import 'nostr-login-components';
import { AuthNostrService, NostrExtensionService, Popup, NostrParams, Nostr, ProcessManager, BannerManager, ModalManager } from './modules';
import { NostrLoginAuthOptions, NostrLoginOptions, StartScreens } from './types';
import { localStorageGetAccounts, localStorageGetCurrent, localStorageGetRecents, localStorageSetItem } from './utils';
import { Info } from 'nostr-login-components/dist/types/types';
import { NostrObjectParams } from './modules/Nostr';
export class NostrLoginInitializer {
public extensionService: NostrExtensionService;
public params: NostrParams;
public authNostrService: AuthNostrService;
public nostr: Nostr;
public processManager: ProcessManager;
public popupManager: Popup;
public bannerManager: BannerManager;
public modalManager: ModalManager;
private customLaunchCallback?: () => void;
constructor() {
this.params = new NostrParams();
this.processManager = new ProcessManager();
this.popupManager = new Popup();
this.bannerManager = new BannerManager(this.params);
this.authNostrService = new AuthNostrService(this.params);
this.extensionService = new NostrExtensionService(this.params);
this.modalManager = new ModalManager(this.params, this.authNostrService, this.extensionService);
const nostrApi: NostrObjectParams = {
waitReady: async () => {
await this.authNostrService.waitReady();
await this.modalManager.waitReady();
},
getUserInfo: () => this.params.userInfo,
getSigner: () => {
if (this.params.userInfo!.authMethod === 'readOnly') throw new Error('Read only');
return this.params.userInfo!.authMethod === 'extension' ? this.extensionService.getExtension() : this.authNostrService;
},
launch: () => {
return this.launch();
},
wait: cb => this.processManager.wait(cb),
};
this.nostr = new Nostr(nostrApi);
this.processManager.on('onCallTimeout', () => {
this.bannerManager.onCallTimeout();
});
this.processManager.on('onCallEnd', () => {
this.bannerManager.onCallEnd();
this.modalManager.onCallEnd();
});
this.processManager.on('onCallStart', () => {
this.bannerManager.onCallStart();
});
this.authNostrService.on('onIframeUrl', url => {
this.modalManager.onIframeUrl(url);
});
this.authNostrService.on('iframeRestart', ({ iframeUrl }) => {
this.processManager.onIframeUrl();
this.bannerManager.onIframeRestart(iframeUrl);
});
this.authNostrService.on('onAuthUrl', ({ url, iframeUrl, eventToAddAccount }) => {
this.processManager.onAuthUrl();
if (eventToAddAccount) {
this.modalManager.onAuthUrl(url);
return;
}
if (this.params.userInfo) {
// show the 'Please confirm' banner
this.bannerManager.onAuthUrl(url, iframeUrl);
} else {
// if it fails we will either return 'failed'
// to the window.nostr caller, or show proper error
// in our modal
this.modalManager.onAuthUrl(url);
}
});
this.authNostrService.on('updateAccounts', () => {
this.updateAccounts();
});
this.authNostrService.on('onUserInfo', info => {
this.bannerManager.onUserInfo(info);
});
this.modalManager.on('onAuthUrlClick', url => {
this.openPopup(url);
});
this.bannerManager.on('onIframeAuthUrlClick', url => {
this.modalManager.showIframeUrl(url);
});
this.modalManager.on('onSwitchAccount', async (info: Info) => {
this.switchAccount(info);
});
this.modalManager.on('onLogoutBanner', async (info: Info) => {
logout();
});
this.bannerManager.on('onConfirmLogout', async () => {
// @ts-ignore
this.launch('confirm-logout');
});
this.modalManager.on('updateAccounts', () => {
this.updateAccounts();
});
this.bannerManager.on('logout', () => {
logout();
});
this.bannerManager.on('onAuthUrlClick', url => {
this.openPopup(url);
});
this.bannerManager.on('onSwitchAccount', async (info: Info) => {
this.switchAccount(info);
});
this.bannerManager.on('import', () => {
this.launch('import');
});
this.extensionService.on('extensionLogin', (pubkey: string) => {
this.authNostrService.setExtension(pubkey);
});
this.extensionService.on('extensionLogout', () => {
logout();
});
this.bannerManager.on('launch', (startScreen?: StartScreens) => {
this.launch(startScreen);
});
}
private openPopup(url: string) {
this.popupManager.openPopup(url);
}
private async switchAccount(info: Info, signup = false) {
console.log('nostr login switch to info', info);
// make sure extension is unlinked
this.extensionService.unsetExtension(this.nostr);
if (info.authMethod === 'readOnly') {
this.authNostrService.setReadOnly(info.pubkey);
} else if (info.authMethod === 'otp') {
this.authNostrService.setOTP(info.pubkey, info.otpData || '');
} else if (info.authMethod === 'local' && info.sk) {
this.authNostrService.setLocal(info, signup);
} else if (info.authMethod === 'extension') {
// trySetExtensionForPubkey will check if
// we still have the extension and it's the same pubkey
await this.extensionService.trySetExtensionForPubkey(info.pubkey);
} else if (info.authMethod === 'connect' && info.sk && info.relays && info.relays[0]) {
this.authNostrService.setConnect(info);
} else {
throw new Error('Bad auth info');
}
}
private updateAccounts() {
const accounts = localStorageGetAccounts();
const recents = localStorageGetRecents();
this.bannerManager.onUpdateAccounts(accounts);
this.modalManager.onUpdateAccounts(accounts, recents);
}
public async launchCustomNostrConnect() {
try {
if (this.authNostrService.isAuthing()) this.authNostrService.cancelNostrConnect();
const customLaunchPromise = new Promise<void>(ok => (this.customLaunchCallback = ok));
await this.authNostrService.startAuth();
await this.authNostrService.sendNeedAuth();
try {
await this.authNostrService.nostrConnect();
await this.authNostrService.endAuth();
} catch (e) {
// if client manually launches the UI we'll
// have cancelled error from the nostrConnect call,
// and that's when we should block on the customLaunchPromise
if (e === 'cancelled') await customLaunchPromise;
}
} catch (e) {
console.error('launchCustomNostrConnect', e);
}
}
private fulfillCustomLaunchPromise() {
if (this.customLaunchCallback) {
const cb = this.customLaunchCallback;
this.customLaunchCallback = undefined;
cb();
}
}
public launch = async (startScreen?: StartScreens | 'default') => {
if (!startScreen) {
if (this.params.optionsModal.customNostrConnect) {
return this.launchCustomNostrConnect();
}
}
const recent = localStorageGetRecents();
const accounts = localStorageGetAccounts();
const options = { ...this.params.optionsModal };
if (startScreen && startScreen !== 'default') options.startScreen = startScreen;
else if (Boolean(recent?.length) || Boolean(accounts?.length)) {
options.startScreen = 'switch-account';
}
// if we're being manually called in the middle of customNostrConnect
// flow then we'll reset the current auth session and launch
// our manual flow and then release the customNostrConnect session
// as if it finished properly
if (this.customLaunchCallback) this.authNostrService.cancelNostrConnect();
try {
await this.modalManager.launch(options);
// if custom launch was interrupted by manual
// launch then we unlock the custom launch to make
// it proceed
this.fulfillCustomLaunchPromise();
} catch (e) {
// don't throw if cancelled
console.log('nostr-login failed', e);
}
};
public init = async (opt: NostrLoginOptions) => {
// watch for extension trying to overwrite our window.nostr
this.extensionService.startCheckingExtension(this.nostr);
// set ourselves as nostr
// @ts-ignore
window.nostr = this.nostr;
// connect launching of our modals to nl-button elements
this.modalManager.connectModals(opt);
// launch
this.bannerManager.launchAuthBanner(opt);
// store options
if (opt) {
this.params.optionsModal = { ...opt };
}
try {
// read conf from localstore
const info = localStorageGetCurrent();
// have current session?
if (info) {
// wtf?
if (!info.pubkey) throw new Error('Bad stored info');
// switch to it
await this.switchAccount(info);
}
} catch (e) {
console.log('nostr login init error', e);
await logout();
}
// ensure current state
this.updateAccounts();
};
public logout = async () => {
// replace back
this.extensionService.unsetExtension(this.nostr);
await this.authNostrService.logout();
};
public setDarkMode = (dark: boolean) => {
localStorageSetItem('nl-dark-mode', `${dark}`);
this.bannerManager.onDarkMode(dark);
this.modalManager.onDarkMode(dark);
};
public setAuth = async (o: NostrLoginAuthOptions) => {
if (!o.type) throw new Error('Invalid auth event');
if (o.type !== 'login' && o.type !== 'logout' && o.type !== 'signup') throw new Error('Invalid auth event');
if (o.method && o.method !== 'connect' && o.method !== 'extension' && o.method !== 'local' && o.method !== 'otp' && o.method !== 'readOnly')
throw new Error('Invalid auth event');
if (o.type === 'logout') return this.logout();
if (!o.method || !o.pubkey) throw new Error('Invalid pubkey');
const info: Info = {
authMethod: o.method,
pubkey: o.pubkey,
relays: o.relays,
sk: o.localNsec,
otpData: o.otpData,
name: o.name,
};
await this.switchAccount(info, o.type === 'signup');
};
public cancelNeedAuth = () => {
console.log("cancelNeedAuth");
this.fulfillCustomLaunchPromise();
this.authNostrService.cancelNostrConnect();
};
}
const initializer = new NostrLoginInitializer();
export const { init, launch, logout, setDarkMode, setAuth, cancelNeedAuth } = initializer;
document.addEventListener('nlLogout', logout);
document.addEventListener('nlLaunch', (event: any) => {
launch(event.detail || '');
});
document.addEventListener('nlNeedAuthCancel', () => {
cancelNeedAuth();
});
document.addEventListener('nlDarkMode', (event: any) => {
setDarkMode(!!event.detail);
});
document.addEventListener('nlSetAuth', (event: any) => {
setAuth(event.detail as NostrLoginAuthOptions);
});

View File

@@ -0,0 +1,718 @@
import { localStorageAddAccount, bunkerUrlToInfo, isBunkerUrl, fetchProfile, getBunkerUrl, localStorageRemoveCurrentAccount, createProfile, getIcon } from '../utils';
import { ConnectionString, Info } from 'nostr-login-components/dist/types/types';
import { generatePrivateKey, getEventHash, getPublicKey, nip19 } from 'nostr-tools';
import { NostrLoginAuthOptions, Response } from '../types';
import NDK, { NDKEvent, NDKNip46Signer, NDKRpcResponse, NDKUser, NostrEvent } from '@nostr-dev-kit/ndk';
import { NostrParams } from './';
import { EventEmitter } from 'tseep';
import { Signer } from './Nostr';
import { Nip44 } from '../utils/nip44';
import { IframeNostrRpc, Nip46Signer, ReadyListener } from './Nip46';
import { PrivateKeySigner } from './Signer';
const OUTBOX_RELAYS = ['wss://user.kindpag.es', 'wss://purplepag.es', 'wss://relay.nos.social'];
const DEFAULT_NOSTRCONNECT_RELAY = 'wss://relay.nsec.app/';
const NOSTRCONNECT_APPS: ConnectionString[] = [
{
name: 'Nsec.app',
domain: 'nsec.app',
canImport: true,
img: 'https://nsec.app/assets/favicon.ico',
link: 'https://use.nsec.app/<nostrconnect>',
relay: 'wss://relay.nsec.app/',
},
{
name: 'Amber',
img: 'https://raw.githubusercontent.com/greenart7c3/Amber/refs/heads/master/assets/android-icon.svg',
link: '<nostrconnect>',
relay: 'wss://relay.nsec.app/',
},
{
name: 'Other key stores',
img: '',
link: '<nostrconnect>',
relay: 'wss://relay.nsec.app/',
},
];
class AuthNostrService extends EventEmitter implements Signer {
private ndk: NDK;
private profileNdk: NDK;
private signer: Nip46Signer | null = null;
private localSigner: PrivateKeySigner | null = null;
private params: NostrParams;
private signerPromise?: Promise<void>;
private signerErrCallback?: (err: string) => void;
private readyPromise?: Promise<void>;
private readyCallback?: () => void;
private nip44Codec = new Nip44();
private nostrConnectKey: string = '';
private nostrConnectSecret: string = '';
private iframe?: HTMLIFrameElement;
private starterReady?: ReadyListener;
nip04: {
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
};
nip44: {
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
};
constructor(params: NostrParams) {
super();
this.params = params;
this.ndk = new NDK({
enableOutboxModel: false,
});
this.profileNdk = new NDK({
enableOutboxModel: true,
explicitRelayUrls: OUTBOX_RELAYS,
});
this.profileNdk.connect();
this.nip04 = {
encrypt: this.encrypt04.bind(this),
decrypt: this.decrypt04.bind(this),
};
this.nip44 = {
encrypt: this.encrypt44.bind(this),
decrypt: this.decrypt44.bind(this),
};
}
public isIframe() {
return !!this.iframe;
}
public async waitReady() {
if (this.signerPromise) {
try {
await this.signerPromise;
} catch {}
}
if (this.readyPromise) {
try {
await this.readyPromise;
} catch {}
}
}
public cancelNostrConnect() {
this.releaseSigner();
this.resetAuth();
}
public async nostrConnect(
relay?: string,
{
domain = '',
link = '',
iframeUrl = '',
importConnect = false,
}: {
domain?: string;
link?: string;
importConnect?: boolean;
iframeUrl?: string;
} = {},
) {
relay = relay || DEFAULT_NOSTRCONNECT_RELAY;
const info: Info = {
authMethod: 'connect',
pubkey: '', // unknown yet!
signerPubkey: '', // unknown too!
sk: this.nostrConnectKey,
domain: domain,
relays: [relay],
iframeUrl,
};
console.log('nostrconnect info', info, link);
// non-iframe flow
if (link && !iframeUrl) window.open(link, '_blank', 'width=400,height=700');
// init nip46 signer
await this.initSigner(info, { listen: true });
// signer learns the remote pubkey
if (!info.pubkey || !info.signerPubkey) throw new Error('Bad remote pubkey');
info.bunkerUrl = `bunker://${info.signerPubkey}?relay=${relay}`;
// callback
if (!importConnect) this.onAuth('login', info);
return info;
}
public async createNostrConnect(relay?: string) {
this.nostrConnectKey = generatePrivateKey();
this.nostrConnectSecret = Math.random().toString(36).substring(7);
const pubkey = getPublicKey(this.nostrConnectKey);
const meta = {
name: encodeURIComponent(document.location.host),
url: encodeURIComponent(document.location.origin),
icon: encodeURIComponent(await getIcon()),
perms: encodeURIComponent(this.params.optionsModal.perms || ''),
};
return `nostrconnect://${pubkey}?image=${meta.icon}&url=${meta.url}&name=${meta.name}&perms=${meta.perms}&secret=${this.nostrConnectSecret}${relay ? `&relay=${relay}` : ''}`;
}
public async getNostrConnectServices(): Promise<[string, ConnectionString[]]> {
const nostrconnect = await this.createNostrConnect();
// copy defaults
const apps = NOSTRCONNECT_APPS.map(a => ({ ...a }));
// if (this.params.optionsModal.dev) {
// apps.push({
// name: 'Dev.Nsec.app',
// domain: 'new.nsec.app',
// canImport: true,
// img: 'https://new.nsec.app/assets/favicon.ico',
// link: 'https://dev.nsec.app/<nostrconnect>',
// relay: 'wss://relay.nsec.app/',
// });
// }
for (const a of apps) {
let relay = DEFAULT_NOSTRCONNECT_RELAY;
if (a.link.startsWith('https://')) {
let domain = a.domain || new URL(a.link).hostname;
try {
const info = await (await fetch(`https://${domain}/.well-known/nostr.json`)).json();
const pubkey = info.names['_'];
const relays = info.nip46[pubkey] as string[];
if (relays && relays.length) relay = relays[0];
a.iframeUrl = info.nip46.iframe_url || '';
} catch (e) {
console.log('Bad app info', e, a);
}
}
const nc = nostrconnect + '&relay=' + relay;
if (a.iframeUrl) {
// pass plain nc url for iframe-based flow
a.link = nc;
} else {
// we will open popup ourselves
a.link = a.link.replace('<nostrconnect>', nc);
}
}
return [nostrconnect, apps];
}
public async localSignup(name: string, sk?: string) {
const signup = !sk;
sk = sk || generatePrivateKey();
const pubkey = getPublicKey(sk);
const info: Info = {
pubkey,
sk,
name,
authMethod: 'local',
};
console.log(`localSignup name: ${name}`);
await this.setLocal(info, signup);
}
public async setLocal(info: Info, signup?: boolean) {
this.releaseSigner();
this.localSigner = new PrivateKeySigner(info.sk!);
if (signup) await createProfile(info, this.profileNdk, this.localSigner, this.params.optionsModal.signupRelays, this.params.optionsModal.outboxRelays);
this.onAuth(signup ? 'signup' : 'login', info);
}
public prepareImportUrl(url: string) {
// for OTP we choose interactive import
if (this.params.userInfo?.authMethod === 'otp') return url + '&import=true';
// for local we export our existing key
if (!this.localSigner || this.params.userInfo?.authMethod !== 'local') throw new Error('Most be local keys');
return url + '#import=' + nip19.nsecEncode(this.localSigner.privateKey!);
}
public async importAndConnect(cs: ConnectionString) {
const { relay, domain, link, iframeUrl } = cs;
if (!domain) throw new Error('Domain required');
const info = await this.nostrConnect(relay, { domain, link, importConnect: true, iframeUrl });
// logout to remove local keys from storage
// but keep the connect signer
await this.logout(/*keepSigner*/ true);
// release local one
this.localSigner = null;
// notify app that we've switched to 'connect' keys
this.onAuth('login', info);
}
public setReadOnly(pubkey: string) {
const info: Info = { pubkey, authMethod: 'readOnly' };
this.onAuth('login', info);
}
public setExtension(pubkey: string) {
const info: Info = { pubkey, authMethod: 'extension' };
this.onAuth('login', info);
}
public setOTP(pubkey: string, data: string) {
const info: Info = { pubkey, authMethod: 'otp', otpData: data };
this.onAuth('login', info);
}
public async setConnect(info: Info) {
this.releaseSigner();
await this.startAuth();
await this.initSigner(info);
this.onAuth('login', info);
await this.endAuth();
}
public async createAccount(nip05: string) {
const [name, domain] = nip05.split('@');
// bunker's own url
const bunkerUrl = await getBunkerUrl(`_@${domain}`, this.params.optionsModal);
console.log("create account bunker's url", bunkerUrl);
// parse bunker url and generate local nsec
const info = bunkerUrlToInfo(bunkerUrl);
if (!info.signerPubkey) throw new Error('Bad bunker url');
const eventToAddAccount = Boolean(this.params.userInfo);
// init signer to talk to the bunker (not the user!)
await this.initSigner(info, { eventToAddAccount });
const userPubkey = await this.signer!.createAccount2({ bunkerPubkey: info.signerPubkey!, name, domain, perms: this.params.optionsModal.perms });
return {
bunkerUrl: `bunker://${userPubkey}?relay=${info.relays?.[0]}`,
sk: info.sk, // reuse the same local key
};
}
private releaseSigner() {
this.signer = null;
this.signerErrCallback?.('cancelled');
this.localSigner = null;
// disconnect from signer relays
for (const r of this.ndk.pool.relays.keys()) {
this.ndk.pool.removeRelay(r);
}
}
public async logout(keepSigner = false) {
if (!keepSigner) this.releaseSigner();
// move current to recent
localStorageRemoveCurrentAccount();
// notify everyone
this.onAuth('logout');
this.emit('updateAccounts');
}
private setUserInfo(userInfo: Info | null) {
this.params.userInfo = userInfo;
this.emit('onUserInfo', userInfo);
if (userInfo) {
localStorageAddAccount(userInfo);
this.emit('updateAccounts');
}
}
public exportKeys() {
if (!this.params.userInfo) return '';
if (this.params.userInfo.authMethod !== 'local') return '';
return nip19.nsecEncode(this.params.userInfo.sk!);
}
private onAuth(type: 'login' | 'signup' | 'logout', info: Info | null = null) {
if (type !== 'logout' && !info) throw new Error('No user info in onAuth');
// make sure we emulate logout first
if (info && this.params.userInfo && (info.pubkey !== this.params.userInfo.pubkey || info.authMethod !== this.params.userInfo.authMethod)) {
const event = new CustomEvent('nlAuth', { detail: { type: 'logout' } });
console.log('nostr-login auth', event.detail);
document.dispatchEvent(event)
}
this.setUserInfo(info);
if (info) {
// async profile fetch
fetchProfile(info, this.profileNdk).then(p => {
if (this.params.userInfo !== info) return;
const userInfo = {
...this.params.userInfo,
picture: p?.image || p?.picture,
name: p?.name || p?.displayName || p?.nip05 || nip19.npubEncode(info.pubkey),
// NOTE: do not overwrite info.nip05 with the one from profile!
// info.nip05 refers to nip46 provider,
// profile.nip05 is just a fancy name that user has chosen
// nip05: p?.nip05
};
this.setUserInfo(userInfo);
});
}
try {
const npub = info ? nip19.npubEncode(info.pubkey) : '';
const options: NostrLoginAuthOptions = {
type,
};
if (type === 'logout') {
// reset
if (this.iframe) this.iframe.remove();
this.iframe = undefined;
} else {
options.pubkey = info!.pubkey;
options.name = info!.name;
if (info!.sk) {
options.localNsec = nip19.nsecEncode(info!.sk);
}
if (info!.relays) {
options.relays = info!.relays;
}
if (info!.otpData) {
options.otpData = info!.otpData;
}
options.method = info!.authMethod || 'connect';
}
const event = new CustomEvent('nlAuth', { detail: options });
console.log('nostr-login auth', options);
document.dispatchEvent(event);
if (this.params.optionsModal.onAuth) {
this.params.optionsModal.onAuth(npub, options);
}
} catch (e) {
console.log('onAuth error', e);
}
}
private async createIframe(iframeUrl?: string) {
if (!iframeUrl) return undefined;
// ensure iframe
const url = new URL(iframeUrl);
const domain = url.hostname;
let iframe: HTMLIFrameElement | undefined;
// one iframe per domain
const did = domain.replaceAll('.', '-');
const id = '__nostr-login-worker-iframe-' + did;
iframe = document.querySelector(`#${id}`) as HTMLIFrameElement;
console.log('iframe', id, iframe);
if (!iframe) {
iframe = document.createElement('iframe');
iframe.setAttribute('width', '0');
iframe.setAttribute('height', '0');
iframe.setAttribute('border', '0');
iframe.style.display = 'none';
// iframe.setAttribute('sandbox', 'allow-forms allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts');
iframe.id = id;
document.body.append(iframe);
}
// wait until loaded
iframe.setAttribute('src', iframeUrl);
// we start listening right now to avoid races
// with 'load' event below
const ready = new ReadyListener(['workerReady', 'workerError'], url.origin);
await new Promise(ok => {
iframe!.addEventListener('load', ok);
});
// now make sure the iframe is ready,
// timeout timer starts here
const r = await ready.wait();
// FIXME wait until the iframe is ready to accept requests,
// maybe it should send us some message?
console.log('nostr-login iframe ready', iframeUrl, r);
return { iframe, port: r[1] as MessagePort };
}
// private async getIframeUrl(domain?: string) {
// if (!domain) return '';
// try {
// const r = await fetch(`https://${domain}/.well-known/nostr.json`);
// const data = await r.json();
// return data.nip46?.iframe_url || '';
// } catch (e) {
// console.log('failed to fetch iframe url', e, domain);
// return '';
// }
// }
public async sendNeedAuth() {
const [nostrconnect] = await this.getNostrConnectServices();
const event = new CustomEvent('nlNeedAuth', { detail: { nostrconnect } });
console.log('nostr-login need auth', nostrconnect);
document.dispatchEvent(event);
}
public isAuthing() {
return !!this.readyCallback;
}
public async startAuth() {
console.log("startAuth");
if (this.readyCallback) throw new Error('Already started');
// start the new promise
this.readyPromise = new Promise<void>(ok => (this.readyCallback = ok));
}
public async endAuth() {
console.log('endAuth', this.params.userInfo);
if (this.params.userInfo && this.params.userInfo.iframeUrl) {
// create iframe
const { iframe, port } = (await this.createIframe(this.params.userInfo.iframeUrl)) || {};
this.iframe = iframe;
if (!this.iframe || !port) return;
// assign iframe to RPC object
(this.signer!.rpc as IframeNostrRpc).setWorkerIframePort(port);
}
this.readyCallback!();
this.readyCallback = undefined;
}
public resetAuth() {
if (this.readyCallback) this.readyCallback();
this.readyCallback = undefined;
}
private async listen(info: Info) {
if (!info.iframeUrl) return this.signer!.listen(this.nostrConnectSecret);
const r = await this.starterReady!.wait();
if (r[0] === 'starterError') throw new Error(r[1]);
return this.signer!.setListenReply(r[1], this.nostrConnectSecret);
}
public async connect(info: Info, perms?: string) {
return this.signer!.connect(info.token, perms);
}
public async initSigner(info: Info, { listen = false, connect = false, eventToAddAccount = false } = {}) {
// mutex
if (this.signerPromise) {
try {
await this.signerPromise;
} catch {}
}
// we remove support for iframe from nip05 and bunker-url methods,
// only nostrconnect flow will use it.
// info.iframeUrl = info.iframeUrl || (await this.getIframeUrl(info.domain));
console.log('initSigner info', info);
// start listening for the ready signal
const iframeOrigin = info.iframeUrl ? new URL(info.iframeUrl!).origin : undefined;
if (iframeOrigin) this.starterReady = new ReadyListener(['starterDone', 'starterError'], iframeOrigin);
// notify modals so they could show the starter iframe,
// FIXME shouldn't this come from nostrconnect service list?
this.emit('onIframeUrl', info.iframeUrl);
this.signerPromise = new Promise<void>(async (ok, err) => {
this.signerErrCallback = err;
try {
// pre-connect if we're creating the connection (listen|connect) or
// not iframe mode
if (info.relays && !info.iframeUrl) {
for (const r of info.relays) {
this.ndk.addExplicitRelay(r, undefined);
}
}
// wait until we connect, otherwise
// signer won't start properly
await this.ndk.connect();
// create and prepare the signer
const localSigner = new PrivateKeySigner(info.sk!);
this.signer = new Nip46Signer(this.ndk, localSigner, info.signerPubkey!, iframeOrigin);
// we should notify the banner the same way as
// the onAuthUrl does
this.signer.on(`iframeRestart`, async () => {
const iframeUrl = info.iframeUrl + (info.iframeUrl!.includes('?') ? '&' : '?') + 'pubkey=' + info.pubkey + '&rebind=' + localSigner.pubkey;
this.emit('iframeRestart', { pubkey: info.pubkey, iframeUrl });
});
// OAuth flow
// if (!listen) {
this.signer.on('authUrl', (url: string) => {
console.log('nostr login auth url', url);
// notify our UI
this.emit('onAuthUrl', { url, iframeUrl: info.iframeUrl, eventToAddAccount });
});
// }
if (listen) {
// nostrconnect: flow
// wait for the incoming message from signer
await this.listen(info);
} else if (connect) {
// bunker: flow
// send 'connect' message to signer
await this.connect(info, this.params.optionsModal.perms);
} else {
// provide saved pubkey as a hint
await this.signer!.initUserPubkey(info.pubkey);
}
// ensure, we're using it in callbacks above
// and expect info to be valid after this call
info.pubkey = this.signer!.userPubkey;
// learned after nostrconnect flow
info.signerPubkey = this.signer!.remotePubkey;
ok();
} catch (e) {
console.log('initSigner failure', e);
// make sure signer isn't set
this.signer = null;
err(e);
}
});
return this.signerPromise;
}
public async authNip46(
type: 'login' | 'signup',
{ name, bunkerUrl, sk = '', domain = '', iframeUrl = '' }: { name: string; bunkerUrl: string; sk?: string; domain?: string; iframeUrl?: string },
) {
try {
const info = bunkerUrlToInfo(bunkerUrl, sk);
if (isBunkerUrl(name)) info.bunkerUrl = name;
else {
info.nip05 = name;
info.domain = name.split('@')[1];
}
if (domain) info.domain = domain;
if (iframeUrl) info.iframeUrl = iframeUrl;
// console.log('nostr login auth info', info);
if (!info.signerPubkey || !info.sk || !info.relays?.[0]) {
throw new Error(`Bad bunker url ${bunkerUrl}`);
}
const eventToAddAccount = Boolean(this.params.userInfo);
console.log('authNip46', type, info);
// updates the info
await this.initSigner(info, { connect: true, eventToAddAccount });
// callback
this.onAuth(type, info);
} catch (e) {
console.log('nostr login auth failed', e);
// make ure it's closed
// this.popupManager.closePopup();
throw e;
}
}
public async signEvent(event: any) {
if (this.localSigner) {
event.pubkey = getPublicKey(this.localSigner.privateKey!);
event.id = getEventHash(event);
event.sig = await this.localSigner.sign(event);
} else {
event.pubkey = this.signer?.remotePubkey;
event.id = getEventHash(event);
event.sig = await this.signer?.sign(event);
}
console.log('signed', { event });
return event;
}
private async codec_call(method: string, pubkey: string, param: string) {
return new Promise<string>((resolve, reject) => {
this.signer!.rpc.sendRequest(this.signer!.remotePubkey!, method, [pubkey, param], 24133, (response: NDKRpcResponse) => {
if (!response.error) {
resolve(response.result);
} else {
reject(response.error);
}
});
});
}
public async encrypt04(pubkey: string, plaintext: string) {
if (this.localSigner) {
return this.localSigner.encrypt(new NDKUser({ pubkey }), plaintext);
} else {
return this.signer!.encrypt(new NDKUser({ pubkey }), plaintext);
}
}
public async decrypt04(pubkey: string, ciphertext: string) {
if (this.localSigner) {
return this.localSigner.decrypt(new NDKUser({ pubkey }), ciphertext);
} else {
// decrypt is broken in ndk v2.3.1, and latest
// ndk v2.8.1 doesn't allow to override connect easily,
// so we reimplement and fix decrypt here as a temporary fix
return this.codec_call('nip04_decrypt', pubkey, ciphertext);
}
}
public async encrypt44(pubkey: string, plaintext: string) {
if (this.localSigner) {
return this.nip44Codec.encrypt(this.localSigner.privateKey!, pubkey, plaintext);
} else {
// no support of nip44 in ndk yet
return this.codec_call('nip44_encrypt', pubkey, plaintext);
}
}
public async decrypt44(pubkey: string, ciphertext: string) {
if (this.localSigner) {
return this.nip44Codec.decrypt(this.localSigner.privateKey!, pubkey, ciphertext);
} else {
// no support of nip44 in ndk yet
return this.codec_call('nip44_decrypt', pubkey, ciphertext);
}
}
}
export default AuthNostrService;

View File

@@ -0,0 +1,146 @@
import { NostrLoginOptions, TypeBanner } from '../types';
import { NostrParams } from '.';
import { Info } from 'nostr-login-components/dist/types/types';
import { EventEmitter } from 'tseep';
import { getDarkMode } from '../utils';
import { ReadyListener } from './Nip46';
class BannerManager extends EventEmitter {
private banner: TypeBanner | null = null;
private iframeReady?: ReadyListener;
private params: NostrParams;
constructor(params: NostrParams) {
super();
this.params = params;
}
public onAuthUrl(url: string, iframeUrl: string) {
if (this.banner) {
if (url)
this.banner.notify = {
mode: iframeUrl ? 'iframeAuthUrl' : 'authUrl',
url,
};
else
this.banner.notify = {
mode: '',
};
}
}
public onIframeRestart(iframeUrl: string) {
if (this.banner) {
this.iframeReady = new ReadyListener(['rebinderDone', 'rebinderError'], new URL(iframeUrl).origin);
this.banner.notify = {
mode: 'rebind',
url: iframeUrl,
};
}
}
public onUserInfo(info: Info | null) {
if (this.banner) {
this.banner.userInfo = info;
}
}
public onCallTimeout() {
if (this.banner) {
this.banner.notify = {
mode: 'timeout',
};
}
}
public onCallStart() {
if (this.banner) {
this.banner.isLoading = true;
}
}
public async onCallEnd() {
if (this.banner) {
if (this.iframeReady) {
await this.iframeReady.wait();
this.iframeReady = undefined;
}
this.banner.isLoading = false;
this.banner.notify = { mode: '' };
}
}
public onUpdateAccounts(accounts: Info[]) {
if (this.banner) {
this.banner.accounts = accounts;
}
}
public onDarkMode(dark: boolean) {
if (this.banner) this.banner.darkMode = dark;
}
public launchAuthBanner(opt: NostrLoginOptions) {
this.banner = document.createElement('nl-banner');
this.banner.setAttribute('dark-mode', String(getDarkMode(opt)));
if (opt.theme) this.banner.setAttribute('theme', opt.theme);
if (opt.noBanner) this.banner.setAttribute('hidden-mode', 'true');
this.banner.addEventListener('handleLoginBanner', (event: any) => {
this.emit('launch', event.detail);
});
this.banner.addEventListener('handleConfirmLogout', () => {
this.emit('onConfirmLogout');
});
this.banner.addEventListener('handleLogoutBanner', async () => {
this.emit('logout');
});
this.banner.addEventListener('handleImportModal', (event: any) => {
this.emit('import');
});
this.banner.addEventListener('handleNotifyConfirmBanner', (event: any) => {
this.emit('onAuthUrlClick', event.detail);
});
this.banner.addEventListener('handleNotifyConfirmBannerIframe', (event: any) => {
this.emit('onIframeAuthUrlClick', event.detail);
});
this.banner.addEventListener('handleSwitchAccount', (event: any) => {
this.emit('onSwitchAccount', event.detail);
});
this.banner.addEventListener('handleOpenWelcomeModal', () => {
this.emit('launch');
if (this.banner) {
this.banner.isOpen = false;
}
});
// this.banner.addEventListener('handleRetryConfirmBanner', () => {
// const url = this.listNotifies.pop();
// // FIXME go to nip05 domain?
// if (!url) {
// return;
// }
// if (this.banner) {
// this.banner.listNotifies = this.listNotifies;
// }
// this.emit('onAuthUrlClick', url);
// });
document.body.appendChild(this.banner);
}
}
export default BannerManager;

View File

@@ -0,0 +1,635 @@
import { NostrLoginOptions, StartScreens, TypeModal } from '../types';
import { checkNip05, getBunkerUrl, getDarkMode, localStorageRemoveRecent, localStorageSetItem, prepareSignupRelays } from '../utils';
import { AuthNostrService, NostrExtensionService, NostrParams } from '.';
import { EventEmitter } from 'tseep';
import { ConnectionString, Info, RecentType } from 'nostr-login-components/dist/types/types';
import { nip19 } from 'nostr-tools';
import { setDarkMode } from '..';
class ModalManager extends EventEmitter {
private modal: TypeModal | null = null;
private params: NostrParams;
private extensionService: NostrExtensionService;
private authNostrService: AuthNostrService;
private launcherPromise?: Promise<void>;
private accounts: Info[] = [];
private recents: RecentType[] = [];
private opt?: NostrLoginOptions;
constructor(params: NostrParams, authNostrService: AuthNostrService, extensionManager: NostrExtensionService) {
super();
this.params = params;
this.extensionService = extensionManager;
this.authNostrService = authNostrService;
}
public async waitReady() {
if (this.launcherPromise) {
try {
await this.launcherPromise;
} catch {}
this.launcherPromise = undefined;
}
}
public async launch(opt: NostrLoginOptions) {
console.log('nostr-login launch', opt);
// mutex
if (this.launcherPromise) await this.waitReady();
// hmm?!
if (this.authNostrService.isAuthing()) this.authNostrService.resetAuth();
this.opt = opt;
const dialog = document.createElement('dialog');
this.modal = document.createElement('nl-auth');
this.modal.accounts = this.accounts;
this.modal.recents = this.recents;
this.modal.setAttribute('dark-mode', String(getDarkMode(opt)));
if (opt.theme) {
this.modal.setAttribute('theme', opt.theme);
}
if (opt.startScreen) {
this.modal.setAttribute('start-screen', opt.startScreen);
}
if (opt.bunkers) {
this.modal.setAttribute('bunkers', opt.bunkers);
} else {
let bunkers = 'nsec.app,highlighter.com';
// if (opt.dev) bunkers += ',new.nsec.app';
this.modal.setAttribute('bunkers', bunkers);
}
if (opt.methods !== undefined) {
this.modal.authMethods = opt.methods;
}
if (opt.localSignup !== undefined) {
this.modal.localSignup = opt.localSignup;
}
if (opt.signupNstart !== undefined) {
this.modal.signupNjump = opt.signupNstart;
}
if (opt.title) {
this.modal.welcomeTitle = opt.title;
}
if (opt.description) {
this.modal.welcomeDescription = opt.description;
}
this.modal.hasExtension = this.extensionService.hasExtension();
this.modal.hasOTP = !!opt.otpRequestUrl && !!opt.otpReplyUrl;
this.modal.isLoadingExtension = false;
this.modal.isLoading = false;
[this.modal.connectionString, this.modal.connectionStringServices] = await this.authNostrService.getNostrConnectServices();
dialog.appendChild(this.modal);
document.body.appendChild(dialog);
let otpPubkey = '';
this.launcherPromise = new Promise<void>((ok, err) => {
dialog.addEventListener('close', () => {
// noop if already resolved
err(new Error('Closed'));
this.authNostrService.resetAuth();
if (this.modal) {
// it's reset on modal creation
// // reset state
// this.modal.isLoading = false;
// this.modal.authUrl = '';
// this.modal.iframeUrl = '';
// this.modal.error = '';
// this.modal.isLoadingExtension = false;
// drop it
// @ts-ignore
document.body.removeChild(this.modal.parentNode);
this.modal = null;
}
});
const done = async (ok: () => void) => {
if (this.modal) this.modal.isLoading = false;
await this.authNostrService.endAuth();
dialog.close();
this.modal = null;
ok();
};
const exec = async (
body: () => Promise<void>,
options?: {
start?: boolean;
end?: boolean;
},
) => {
if (this.modal) {
this.modal.isLoading = true;
}
try {
if (!options || options.start) await this.authNostrService.startAuth();
await body();
if (!options || options.end) await done(ok);
} catch (e: any) {
console.log('error', e);
if (this.modal) {
this.modal.isLoading = false;
this.modal.authUrl = '';
this.modal.iframeUrl = '';
if (e !== 'cancelled') this.modal.error = e.toString();
}
}
};
const login = async (name: string, domain?: string) => {
await exec(async () => {
// convert name to bunker url
const bunkerUrl = await getBunkerUrl(name, this.params.optionsModal);
// connect to bunker by url
await this.authNostrService.authNip46('login', { name, bunkerUrl, domain });
});
};
const signup = async (name: string) => {
await exec(async () => {
// create acc on service and get bunker url
const { bunkerUrl, sk } = await this.authNostrService.createAccount(name);
// connect to bunker by url
await this.authNostrService.authNip46('signup', { name, bunkerUrl, sk });
});
};
const exportKeys = async () => {
try {
await navigator.clipboard.writeText(this.authNostrService.exportKeys());
localStorageSetItem('backupKey', 'true');
} catch (err) {
console.error('Failed to copy to clipboard: ', err);
}
};
const importKeys = async (cs: ConnectionString) => {
await exec(async () => {
const { iframeUrl } = cs;
cs.link = this.authNostrService.prepareImportUrl(cs.link);
if (this.modal && iframeUrl) {
// we pass the link down to iframe so it could open it
this.modal.authUrl = cs.link;
this.modal.iframeUrl = iframeUrl;
this.modal.isLoading = false;
console.log('nostrconnect authUrl', this.modal.authUrl, this.modal.iframeUrl);
}
await this.authNostrService.importAndConnect(cs);
});
};
const nostrConnect = async (cs?: ConnectionString) => {
await exec(async () => {
const { relay, domain, link, iframeUrl } = cs || {};
console.log('nostrConnect', cs, relay, domain, link, iframeUrl);
if (this.modal) {
if (iframeUrl) {
// we pass the link down to iframe so it could open it
this.modal.authUrl = link;
this.modal.iframeUrl = iframeUrl;
this.modal.isLoading = false;
console.log('nostrconnect authUrl', this.modal.authUrl, this.modal.iframeUrl);
}
if (!cs) this.modal.isLoading = false;
}
await this.authNostrService.nostrConnect(relay, { domain, link, iframeUrl });
});
};
const localSignup = async (name?: string) => {
await exec(async () => {
if (!name) throw new Error('Please enter some nickname');
await this.authNostrService.localSignup(name);
});
};
const signupNjump = async () => {
await exec(async () => {
const self = new URL(window.location.href);
const name =
self.hostname
.toLocaleLowerCase()
.replace(/^www\./i, '')
.charAt(0)
.toUpperCase() + self.hostname.slice(1);
const relays = prepareSignupRelays(this.params.optionsModal.signupRelays);
// const url = `https://start.njump.me/?an=${name}&at=popup&ac=${window.location.href}&s=${this.opt!.followNpubs || ''}&arr=${relays}&awr=${relays}`;
// console.log('njump url', url);
this.modal!.njumpIframe = `
<html><body>
<script src='https://start.njump.me/modal.js'></script>
<script>
new NstartModal({
baseUrl: 'https://start.njump.me',
// Required parameters
an: '${name}',
// Optional parameters
s: [${this.opt!.followNpubs ? `'${this.opt!.followNpubs}'` : ''}],
afb: false, // forceBunker
asb: false, // skipBunker
aan: false, // avoidNsec
aac: true, // avoidNcryptsec
ahc: true, // hide close button
arr: ${JSON.stringify(relays)}, //readRelays
awr: ${JSON.stringify(relays)}, //writeRelays
// Callbacks
onComplete: (result) => {
console.log('Login token:', result.nostrLogin);
window.parent.location.href='${window.location.href}#nostr-login='+result.nostrLogin;
},
onCancel: () => {
window.parent.location.href='${window.location.href}#nostr-login=null';
},
}).open();
</script>
</body></html>
`.replaceAll('&', '&amp;'); // needed?
return new Promise((ok, err) => {
const process = async (nsecOrBunker: string) => {
// process the returned value
console.log('nsecOrBunker', nsecOrBunker);
if (nsecOrBunker.startsWith('nsec1')) {
let decoded;
try {
decoded = nip19.decode(nsecOrBunker);
} catch (e) {
throw new Error('Bad nsec value');
}
if (decoded.type !== 'nsec') throw new Error('Bad bech32 type');
await this.authNostrService.localSignup('', decoded.data);
ok();
} else if (nsecOrBunker.startsWith('bunker:')) {
await this.authNostrService.authNip46('login', { name: '', bunkerUrl: nsecOrBunker });
ok();
} else if (nsecOrBunker === 'null') {
err('Cancelled');
} else {
err('Unknown return value');
}
};
const onOpen = async () => {
if (window.location.hash.startsWith('#nostr-login=')) {
const nsecOrBunker = window.location.hash.split('#nostr-login=')[1];
// clear hash from history
const url = new URL(window.location.toString());
url.hash = '';
window.history.replaceState({}, '', url.toString());
process(nsecOrBunker);
}
};
// // use random 'target' to make sure window.opener is
// // accessible to the popup
// window.open(url, '' + Date.now(), 'popup=true,width=600,height=950');
window.addEventListener('hashchange', onOpen);
});
});
};
if (!this.modal) throw new Error('WTH?');
this.modal.addEventListener('handleContinue', () => {
if (this.modal) {
this.modal.isLoading = true;
this.emit('onAuthUrlClick', this.modal.authUrl);
}
});
this.modal.addEventListener('nlLogin', (event: any) => {
login(event.detail);
});
this.modal.addEventListener('nlSignup', (event: any) => {
signup(event.detail);
});
this.modal.addEventListener('nlLocalSignup', (event: any) => {
localSignup(event.detail);
});
this.modal.addEventListener('nlSignupNjump', (event: any) => {
signupNjump();
});
this.modal.addEventListener('nlImportAccount', (event: any) => {
importKeys(event.detail);
});
this.modal.addEventListener('nlExportKeys', (event: any) => {
exportKeys();
});
this.modal.addEventListener('handleLogoutBanner', () => {
this.emit('onLogoutBanner');
});
this.modal.addEventListener('nlNostrConnect', (event: any) => {
nostrConnect(event.detail);
});
this.modal.addEventListener('nlNostrConnectDefault', () => {
// dedup the calls
if (!this.authNostrService.isAuthing()) nostrConnect();
});
this.modal.addEventListener('nlNostrConnectDefaultCancel', () => {
console.log('nlNostrConnectDefaultCancel');
this.authNostrService.cancelNostrConnect();
});
this.modal.addEventListener('nlSwitchAccount', (event: any) => {
const eventInfo: Info = event.detail as Info;
this.emit('onSwitchAccount', eventInfo);
// wait a bit, if dialog closes before
// switching finishes then launched promise rejects
// FIXME this calls resetAuth which then prevents
// endAuth from getting properly called. 300 is not
// enough to init iframe, so there should be a
// feedback from switchAccount here
setTimeout(() => dialog.close(), 300);
});
this.modal.addEventListener('nlLoginRecentAccount', async (event: any) => {
const userInfo: Info = event.detail as Info;
if (userInfo.authMethod === 'readOnly') {
this.authNostrService.setReadOnly(userInfo.pubkey);
dialog.close();
} else if (userInfo.authMethod === 'otp') {
try {
this.modal!.dispatchEvent(
new CustomEvent('nlLoginOTPUser', {
detail: userInfo.nip05 || userInfo.pubkey,
}),
);
} catch (e) {
console.error(e);
}
} else if (userInfo.authMethod === 'extension') {
await this.extensionService.trySetExtensionForPubkey(userInfo.pubkey);
dialog.close();
} else {
const input = userInfo.bunkerUrl || userInfo.nip05;
if (!input) throw new Error('Bad connect info');
login(input, userInfo.domain);
}
});
this.modal.addEventListener('nlRemoveRecent', (event: any) => {
localStorageRemoveRecent(event.detail as RecentType);
this.emit('updateAccounts');
});
const nameToPubkey = async (nameNpub: string) => {
let pubkey = '';
if (nameNpub.includes('@')) {
const { error, pubkey: nip05pubkey } = await checkNip05(nameNpub);
if (nip05pubkey) pubkey = nip05pubkey;
else throw new Error(error);
} else if (nameNpub.startsWith('npub')) {
const { type, data } = nip19.decode(nameNpub);
if (type === 'npub') pubkey = data as string;
else throw new Error('Bad npub');
} else if (nameNpub.trim().length === 64) {
pubkey = nameNpub.trim();
nip19.npubEncode(pubkey); // check
}
return pubkey;
};
this.modal.addEventListener('nlLoginReadOnly', async (event: any) => {
await exec(async () => {
const nameNpub = event.detail;
const pubkey = await nameToPubkey(nameNpub);
this.authNostrService.setReadOnly(pubkey);
});
});
this.modal.addEventListener('nlLoginExtension', async () => {
if (!this.extensionService.hasExtension()) {
throw new Error('No extension');
}
await exec(async () => {
if (!this.modal) return;
this.modal.isLoadingExtension = true;
await this.extensionService.setExtension();
this.modal.isLoadingExtension = false;
});
});
this.modal.addEventListener('nlLoginOTPUser', async (event: any) => {
await exec(
async () => {
if (!this.modal) return;
const nameNpub = event.detail;
const pubkey = await nameToPubkey(nameNpub);
const url = this.opt!.otpRequestUrl! + (this.opt!.otpRequestUrl!.includes('?') ? '&' : '?') + 'pubkey=' + pubkey;
const r = await fetch(url);
if (r.status !== 200) {
console.warn('nostr-login: bad otp reply', r);
throw new Error('Failed to send DM');
}
// switch to 'enter code' mode
this.modal.isOTP = true;
// remember for code handler below
otpPubkey = pubkey;
// spinner off
this.modal.isLoading = false;
},
{ start: true },
);
});
this.modal.addEventListener('nlLoginOTPCode', async (event: any) => {
await exec(
async () => {
if (!this.modal) return;
const code = event.detail;
const url = this.opt!.otpReplyUrl! + (this.opt!.otpRequestUrl!.includes('?') ? '&' : '?') + 'pubkey=' + otpPubkey + '&code=' + code;
const r = await fetch(url);
if (r.status !== 200) {
console.warn('nostr-login: bad otp reply', r);
throw new Error('Invalid code');
}
const data = await r.text();
this.authNostrService.setOTP(otpPubkey, data);
this.modal.isOTP = false;
},
{ end: true },
);
});
this.modal.addEventListener('nlCheckSignup', async (event: any) => {
const { available, taken, error } = await checkNip05(event.detail);
if (this.modal) {
this.modal.error = String(error);
if (!error && taken) {
this.modal.error = 'Already taken';
}
this.modal.signupNameIsAvailable = available;
}
});
this.modal.addEventListener('nlCheckLogin', async (event: any) => {
const { available, taken, error } = await checkNip05(event.detail);
if (this.modal) {
this.modal.error = String(error);
if (available) {
this.modal.error = 'Name not found';
}
this.modal.loginIsGood = taken;
}
});
const cancel = () => {
if (this.modal) {
this.modal.isLoading = false;
}
// this.authNostrService.cancelListenNostrConnect();
dialog.close();
err(new Error('Cancelled'));
};
this.modal.addEventListener('stopFetchHandler', cancel);
this.modal.addEventListener('nlCloseModal', cancel);
this.modal.addEventListener('nlChangeDarkMode', (event: any) => {
setDarkMode(event.detail);
document.dispatchEvent(new CustomEvent('nlDarkMode', { detail: event.detail }));
});
this.on('onIframeAuthUrlCallEnd', () => {
dialog.close();
this.modal = null;
ok();
});
dialog.showModal();
});
return this.launcherPromise;
}
public async showIframeUrl(url: string) {
// make sure we consume the previous promise,
// otherwise launch will start await-ing
// before modal is created and setting iframeUrl will fail
await this.waitReady();
this.launch({
startScreen: 'iframe' as StartScreens,
}).catch(() => console.log('closed auth iframe'));
this.modal!.authUrl = url;
}
public connectModals(defaultOpt: NostrLoginOptions) {
const initialModals = async (opt: NostrLoginOptions) => {
await this.launch(opt);
};
const nlElements = document.getElementsByTagName('nl-button');
for (let i = 0; i < nlElements.length; i++) {
const theme = nlElements[i].getAttribute('nl-theme');
const startScreen = nlElements[i].getAttribute('start-screen');
const elementOpt = {
...defaultOpt,
};
if (theme) elementOpt.theme = theme;
switch (startScreen as StartScreens) {
case 'login':
case 'login-bunker-url':
case 'login-read-only':
case 'signup':
case 'switch-account':
case 'welcome':
elementOpt.startScreen = startScreen as StartScreens;
}
nlElements[i].addEventListener('click', function () {
initialModals(elementOpt);
});
}
}
public onAuthUrl(url: string) {
if (this.modal) {
this.modal.authUrl = url;
this.modal.isLoading = false;
}
}
public onIframeUrl(url: string) {
if (this.modal) {
console.log('modal iframe url', url);
this.modal.iframeUrl = url;
}
}
public onCallEnd() {
if (this.modal && this.modal.authUrl && this.params.userInfo?.iframeUrl) {
this.emit('onIframeAuthUrlCallEnd');
}
}
public onUpdateAccounts(accounts: Info[], recents: RecentType[]) {
this.accounts = accounts;
this.recents = recents;
if (!this.modal) return;
this.modal.accounts = accounts;
this.modal.recents = recents;
}
public onDarkMode(dark: boolean) {
if (this.modal) this.modal.darkMode = dark;
}
}
export default ModalManager;

View File

@@ -0,0 +1,429 @@
import NDK, { NDKEvent, NDKFilter, NDKNip46Signer, NDKNostrRpc, NDKRpcRequest, NDKRpcResponse, NDKSubscription, NDKSubscriptionCacheUsage, NostrEvent } from '@nostr-dev-kit/ndk';
import { validateEvent, verifySignature } from 'nostr-tools';
import { PrivateKeySigner } from './Signer';
class NostrRpc extends NDKNostrRpc {
protected _ndk: NDK;
protected _signer: PrivateKeySigner;
protected requests: Set<string> = new Set();
private sub?: NDKSubscription;
protected _useNip44: boolean = false;
public constructor(ndk: NDK, signer: PrivateKeySigner) {
super(ndk, signer, ndk.debug.extend('nip46:signer:rpc'));
this._ndk = ndk;
this._signer = signer;
}
public async subscribe(filter: NDKFilter): Promise<NDKSubscription> {
// NOTE: fixing ndk
filter.kinds = filter.kinds?.filter(k => k === 24133);
this.sub = await super.subscribe(filter);
return this.sub;
}
public stop() {
if (this.sub) {
this.sub.stop();
this.sub = undefined;
}
}
public setUseNip44(useNip44: boolean) {
this._useNip44 = useNip44;
}
private isNip04(ciphertext: string) {
const l = ciphertext.length;
if (l < 28) return false;
return ciphertext[l - 28] === '?' && ciphertext[l - 27] === 'i' && ciphertext[l - 26] === 'v' && ciphertext[l - 25] === '=';
}
// override to auto-decrypt nip04/nip44
public async parseEvent(event: NDKEvent): Promise<NDKRpcRequest | NDKRpcResponse> {
const remoteUser = this._ndk.getUser({ pubkey: event.pubkey });
remoteUser.ndk = this._ndk;
const decrypt = this.isNip04(event.content) ? this._signer.decrypt : this._signer.decryptNip44;
const decryptedContent = await decrypt.call(this._signer, remoteUser, event.content);
const parsedContent = JSON.parse(decryptedContent);
const { id, method, params, result, error } = parsedContent;
if (method) {
return { id, pubkey: event.pubkey, method, params, event };
} else {
return { id, result, error, event };
}
}
public async parseNostrConnectReply(reply: any, secret: string) {
const event = new NDKEvent(this._ndk, reply);
const parsedEvent = await this.parseEvent(event);
console.log('nostr connect parsedEvent', parsedEvent);
if (!(parsedEvent as NDKRpcRequest).method) {
const response = parsedEvent as NDKRpcResponse;
if (response.result !== secret) throw new Error(response.error);
return event.pubkey;
} else {
throw new Error('Bad nostr connect reply');
}
}
// ndk doesn't support nostrconnect:
// we just listed to an unsolicited reply to
// our pubkey and if it's ack/secret - we're fine
public async listen(nostrConnectSecret: string): Promise<string> {
const pubkey = this._signer.pubkey;
console.log('nostr-login listening for conn to', pubkey);
const sub = await this.subscribe({
'kinds': [24133],
'#p': [pubkey],
});
return new Promise<string>((ok, err) => {
sub.on('event', async (event: NDKEvent) => {
try {
const parsedEvent = await this.parseEvent(event);
// console.log('ack parsedEvent', parsedEvent);
if (!(parsedEvent as NDKRpcRequest).method) {
const response = parsedEvent as NDKRpcResponse;
// ignore
if (response.result === 'auth_url') return;
// FIXME for now accept 'ack' replies, later on only
// accept secrets
if (response.result === 'ack' || response.result === nostrConnectSecret) {
ok(event.pubkey);
} else {
err(response.error);
}
}
} catch (e) {
console.log('error parsing event', e, event.rawEvent());
}
// done
this.stop();
});
});
}
// since ndk doesn't yet support perms param
// we reimplement the 'connect' call here
// instead of await signer.blockUntilReady();
public async connect(pubkey: string, token?: string, perms?: string) {
return new Promise<void>((ok, err) => {
const connectParams = [pubkey!, token || '', perms || ''];
this.sendRequest(pubkey!, 'connect', connectParams, 24133, (response: NDKRpcResponse) => {
if (response.result === 'ack') {
ok();
} else {
err(response.error);
}
});
});
}
protected getId(): string {
return Math.random().toString(36).substring(7);
}
public async sendRequest(remotePubkey: string, method: string, params: string[] = [], kind = 24133, cb?: (res: NDKRpcResponse) => void): Promise<NDKRpcResponse> {
const id = this.getId();
// response handler will deduplicate auth urls and responses
this.setResponseHandler(id, cb);
// create and sign request
const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
console.log("sendRequest", { event, method, remotePubkey, params });
// send to relays
await event.publish();
// NOTE: ndk returns a promise that never resolves and
// in fact REQUIRES cb to be provided (otherwise no way
// to consume the result), we've already stepped on the bug
// of waiting for this unresolvable result, so now we return
// undefined to make sure waiters fail, not hang.
// @ts-ignore
return undefined as NDKRpcResponse;
}
protected setResponseHandler(id: string, cb?: (res: NDKRpcResponse) => void) {
let authUrlSent = false;
const now = Date.now();
return new Promise<NDKRpcResponse>(() => {
const responseHandler = (response: NDKRpcResponse) => {
if (response.result === 'auth_url') {
this.once(`response-${id}`, responseHandler);
if (!authUrlSent) {
authUrlSent = true;
this.emit('authUrl', response.error);
}
} else if (cb) {
if (this.requests.has(id)) {
this.requests.delete(id);
console.log('nostr-login processed nip46 request in', Date.now() - now, 'ms');
cb(response);
}
}
};
this.once(`response-${id}`, responseHandler);
});
}
protected async createRequestEvent(id: string, remotePubkey: string, method: string, params: string[] = [], kind = 24133) {
this.requests.add(id);
const localUser = await this._signer.user();
const remoteUser = this._ndk.getUser({ pubkey: remotePubkey });
const request = { id, method, params };
const event = new NDKEvent(this._ndk, {
kind,
content: JSON.stringify(request),
tags: [['p', remotePubkey]],
pubkey: localUser.pubkey,
} as NostrEvent);
const useNip44 = this._useNip44 && method !== 'create_account';
const encrypt = useNip44 ? this._signer.encryptNip44 : this._signer.encrypt;
event.content = await encrypt.call(this._signer, remoteUser, event.content);
await event.sign(this._signer);
return event;
}
}
export class IframeNostrRpc extends NostrRpc {
private peerOrigin?: string;
private iframePort?: MessagePort;
private iframeRequests = new Map<string, { id: string; pubkey: string }>();
public constructor(ndk: NDK, localSigner: PrivateKeySigner, iframePeerOrigin?: string) {
super(ndk, localSigner);
this._ndk = ndk;
this.peerOrigin = iframePeerOrigin;
}
public async subscribe(filter: NDKFilter): Promise<NDKSubscription> {
if (!this.peerOrigin) return super.subscribe(filter);
return new NDKSubscription(
this._ndk,
{},
{
// don't send to relay
closeOnEose: true,
cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE,
},
);
}
public setWorkerIframePort(port: MessagePort) {
if (!this.peerOrigin) throw new Error('Unexpected iframe port');
this.iframePort = port;
// to make sure Chrome doesn't terminate the channel
setInterval(() => {
console.log('iframe-nip46 ping');
this.iframePort!.postMessage('ping');
}, 5000);
port.onmessage = async ev => {
console.log('iframe-nip46 got response', ev.data);
if (typeof ev.data === 'string' && ev.data.startsWith('errorNoKey')) {
const event_id = ev.data.split(':')[1];
const { id = '', pubkey = '' } = this.iframeRequests.get(event_id) || {};
if (id && pubkey && this.requests.has(id)) this.emit(`iframeRestart-${pubkey}`);
return;
}
// a copy-paste from rpc.subscribe
try {
const event = ev.data;
if (!validateEvent(event)) throw new Error('Invalid event from iframe');
if (!verifySignature(event)) throw new Error('Invalid event signature from iframe');
const nevent = new NDKEvent(this._ndk, event);
const parsedEvent = await this.parseEvent(nevent);
// we're only implementing client-side rpc
if (!(parsedEvent as NDKRpcRequest).method) {
console.log('parsed response', parsedEvent);
this.emit(`response-${parsedEvent.id}`, parsedEvent);
}
} catch (e) {
console.log('error parsing event', e, ev.data);
}
};
}
public async sendRequest(remotePubkey: string, method: string, params: string[] = [], kind = 24133, cb?: (res: NDKRpcResponse) => void): Promise<NDKRpcResponse> {
const id = this.getId();
// create and sign request event
const event = await this.createRequestEvent(id, remotePubkey, method, params, kind);
// set response handler, it will dedup auth urls,
// and also dedup response handlers - we're sending
// to relays and to iframe
this.setResponseHandler(id, cb);
if (this.iframePort) {
// map request event id to request id, if iframe
// has no key it will reply with error:event_id (it can't
// decrypt the request id without keys)
this.iframeRequests.set(event.id, { id, pubkey: remotePubkey });
// send to iframe
console.log('iframe-nip46 sending request to', this.peerOrigin, event.rawEvent());
this.iframePort.postMessage(event.rawEvent());
} else {
// send to relays
await event.publish();
}
// see notes in 'super'
// @ts-ignore
return undefined as NDKRpcResponse;
}
}
export class ReadyListener {
origin: string;
messages: string[];
promise: Promise<any>;
constructor(messages: string[], origin: string) {
this.origin = origin;
this.messages = messages;
this.promise = new Promise<any>(ok => {
console.log(new Date(), 'started listener for', this.messages);
// ready message handler
const onReady = async (e: MessageEvent) => {
const originHostname = new URL(origin!).hostname;
const messageHostname = new URL(e.origin).hostname;
// same host or subdomain
const validHost = messageHostname === originHostname || messageHostname.endsWith('.' + originHostname);
if (!validHost || !Array.isArray(e.data) || !e.data.length || !this.messages.includes(e.data[0])) {
// console.log(new Date(), 'got invalid ready message', e.origin, e.data);
return;
}
console.log(new Date(), 'got ready message from', e.origin, e.data);
window.removeEventListener('message', onReady);
ok(e.data);
};
window.addEventListener('message', onReady);
});
}
async wait(): Promise<any> {
console.log(new Date(), 'waiting for', this.messages);
const r = await this.promise;
// NOTE: timer here doesn't help bcs it must be activated when
// user "confirms", but that's happening on a different
// origin and we can't really know.
// await new Promise<any>((ok, err) => {
// // 10 sec should be more than enough
// setTimeout(() => err(new Date() + ' timeout for ' + this.message), 10000);
// // if promise already resolved or will resolve in the future
// this.promise.then(ok);
// });
console.log(new Date(), 'finished waiting for', this.messages, r);
return r;
}
}
export class Nip46Signer extends NDKNip46Signer {
private _userPubkey: string = '';
private _rpc: IframeNostrRpc;
constructor(ndk: NDK, localSigner: PrivateKeySigner, signerPubkey: string, iframeOrigin?: string) {
super(ndk, signerPubkey, localSigner);
// override with our own rpc implementation
this._rpc = new IframeNostrRpc(ndk, localSigner, iframeOrigin);
this._rpc.setUseNip44(true); // !!this.params.optionsModal.dev);
this._rpc.on('authUrl', (url: string) => {
this.emit('authUrl', url);
});
this.rpc = this._rpc;
}
get userPubkey() {
return this._userPubkey;
}
private async setSignerPubkey(signerPubkey: string, sameAsUser: boolean = false) {
console.log("setSignerPubkey", signerPubkey);
// ensure it's set
this.remotePubkey = signerPubkey;
// when we're sure it's known
this._rpc.on(`iframeRestart-${signerPubkey}`, () => {
this.emit('iframeRestart');
});
// now call getPublicKey and swap remotePubkey w/ that
await this.initUserPubkey(sameAsUser ? signerPubkey : '');
}
public async initUserPubkey(hintPubkey?: string) {
if (this._userPubkey) throw new Error('Already called initUserPubkey');
if (hintPubkey) {
this._userPubkey = hintPubkey;
return;
}
this._userPubkey = await new Promise<string>((ok, err) => {
if (!this.remotePubkey) throw new Error('Signer pubkey not set');
console.log("get_public_key", this.remotePubkey);
this._rpc.sendRequest(this.remotePubkey, 'get_public_key', [], 24133, (response: NDKRpcResponse) => {
ok(response.result);
});
});
}
public async listen(nostrConnectSecret: string) {
const signerPubkey = await (this.rpc as IframeNostrRpc).listen(nostrConnectSecret);
await this.setSignerPubkey(signerPubkey);
}
public async connect(token?: string, perms?: string) {
if (!this.remotePubkey) throw new Error('No signer pubkey');
await this._rpc.connect(this.remotePubkey, token, perms);
await this.setSignerPubkey(this.remotePubkey);
}
public async setListenReply(reply: any, nostrConnectSecret: string) {
const signerPubkey = await this._rpc.parseNostrConnectReply(reply, nostrConnectSecret);
await this.setSignerPubkey(signerPubkey, true);
}
public async createAccount2({ bunkerPubkey, name, domain, perms = '' }: { bunkerPubkey: string; name: string; domain: string; perms?: string }) {
const params = [
name,
domain,
'', // email
perms,
];
const r = await new Promise<NDKRpcResponse>(ok => {
this.rpc.sendRequest(bunkerPubkey, 'create_account', params, undefined, ok);
});
console.log('create_account pubkey', r);
if (r.result === 'error') {
throw new Error(r.error);
}
return r.result;
}
}

View File

@@ -0,0 +1,107 @@
import { Info } from 'nostr-login-components/dist/types/types';
export interface Signer {
signEvent: (event: any) => Promise<any>;
nip04: {
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
};
nip44: {
encrypt: (pubkey: string, plaintext: string) => Promise<string>;
decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
};
}
export interface NostrObjectParams {
waitReady(): Promise<void>;
getUserInfo(): Info | null;
launch(): Promise<void>;
getSigner(): Signer;
wait<T>(cb: () => Promise<T>): Promise<T>;
}
class Nostr {
#params: NostrObjectParams;
private nip04: {
encrypt: (pubkey: string, plaintext: string) => Promise<any>;
decrypt: (pubkey: string, ciphertext: string) => Promise<any>;
};
private nip44: {
encrypt: (pubkey: string, plaintext: string) => Promise<any>;
decrypt: (pubkey: string, ciphertext: string) => Promise<any>;
};
constructor(params: NostrObjectParams) {
this.#params = params;
this.getPublicKey = this.getPublicKey.bind(this);
this.signEvent = this.signEvent.bind(this);
this.getRelays = this.getRelays.bind(this);
this.nip04 = {
encrypt: this.encrypt04.bind(this),
decrypt: this.decrypt04.bind(this),
};
this.nip44 = {
encrypt: this.encrypt44.bind(this),
decrypt: this.decrypt44.bind(this),
};
}
private async ensureAuth() {
await this.#params.waitReady();
// authed?
if (this.#params.getUserInfo()) return;
// launch auth flow
await this.#params.launch();
// give up
if (!this.#params.getUserInfo()) {
throw new Error('Rejected by user');
}
}
async getPublicKey() {
await this.ensureAuth();
const userInfo = this.#params.getUserInfo();
if (userInfo) {
return userInfo.pubkey;
} else {
throw new Error('No user');
}
}
// @ts-ignore
async signEvent(event) {
await this.ensureAuth();
return this.#params.wait(async () => await this.#params.getSigner().signEvent(event));
}
async getRelays() {
// FIXME implement!
return {};
}
async encrypt04(pubkey: string, plaintext: string) {
await this.ensureAuth();
return this.#params.wait(async () => await this.#params.getSigner().nip04.encrypt(pubkey, plaintext));
}
async decrypt04(pubkey: string, ciphertext: string) {
await this.ensureAuth();
return this.#params.wait(async () => await this.#params.getSigner().nip04.decrypt(pubkey, ciphertext));
}
async encrypt44(pubkey: string, plaintext: string) {
await this.ensureAuth();
return this.#params.wait(async () => await this.#params.getSigner().nip44.encrypt(pubkey, plaintext));
}
async decrypt44(pubkey: string, ciphertext: string) {
await this.ensureAuth();
return this.#params.wait(async () => await this.#params.getSigner().nip44.decrypt(pubkey, ciphertext));
}
}
export default Nostr;

View File

@@ -0,0 +1,99 @@
import { Nostr, NostrParams } from './';
import { EventEmitter } from 'tseep';
class NostrExtensionService extends EventEmitter {
private params: NostrParams;
private nostrExtension: any | undefined;
constructor(params: NostrParams) {
super();
this.params = params;
}
public startCheckingExtension(nostr: Nostr) {
if (this.checkExtension(nostr)) return;
// watch out for extension trying to overwrite us
const to = setInterval(() => {
if (this.checkExtension(nostr)) clearTimeout(to);
}, 100);
}
private checkExtension(nostr: Nostr) {
// @ts-ignore
if (!this.nostrExtension && window.nostr && window.nostr !== nostr) {
this.initExtension(nostr);
return true;
}
return false;
}
private async initExtension(nostr: Nostr, lastTry?: boolean) {
// @ts-ignore
this.nostrExtension = window.nostr;
// @ts-ignore
window.nostr = nostr;
// we're signed in with extesions? well execute that
if (this.params.userInfo?.authMethod === 'extension') {
await this.trySetExtensionForPubkey(this.params.userInfo.pubkey);
}
// schedule another check
if (!lastTry) {
setTimeout(() => {
// NOTE: we can't know if user has >1 extension and thus
// if the current one we detected is the actual 'last one'
// that will set the window.nostr. So the simplest
// solution is to wait a bit more, hoping that if one
// extension started then the rest are likely to start soon,
// and then just capture the most recent one
// @ts-ignore
if (window.nostr !== nostr && this.nostrExtension !== window.nostr) {
this.initExtension(nostr, true);
}
}, 300);
}
// in the worst case of app saving the nostrExtension reference
// it will be calling it directly, not a big deal
}
private async setExtensionReadPubkey(expectedPubkey?: string) {
window.nostr = this.nostrExtension;
// @ts-ignore
const pubkey = await window.nostr.getPublicKey();
if (expectedPubkey && expectedPubkey !== pubkey) {
this.emit('extensionLogout');
} else {
this.emit('extensionLogin', pubkey);
}
}
public async trySetExtensionForPubkey(expectedPubkey: string) {
if (this.nostrExtension) {
return this.setExtensionReadPubkey(expectedPubkey);
}
}
public async setExtension() {
return this.setExtensionReadPubkey();
}
public unsetExtension(nostr: Nostr) {
if (window.nostr === this.nostrExtension) {
// @ts-ignore
window.nostr = nostr;
}
}
public getExtension() {
return this.nostrExtension;
}
public hasExtension() {
return !!this.nostrExtension;
}
}
export default NostrExtensionService;

View File

@@ -0,0 +1,18 @@
import { Info } from 'nostr-login-components/dist/types/types';
import { NostrLoginOptions } from '../types';
class NostrParams {
public userInfo: Info | null;
public optionsModal: NostrLoginOptions;
constructor() {
this.userInfo = null;
this.optionsModal = {
theme: 'default',
startScreen: 'welcome',
devOverrideBunkerOrigin: '',
};
}
}
export default NostrParams;

View File

@@ -0,0 +1,27 @@
class Popup {
private popup: Window | null = null;
constructor() {}
public openPopup(url: string) {
// user might have closed it already
if (!this.popup || this.popup.closed) {
// NOTE: do not set noreferrer, bunker might use referrer to
// simplify the naming of the connected app.
// NOTE: do not pass noopener, otherwise null is returned
this.popup = window.open(url, '_blank', 'width=400,height=700');
console.log('popup', this.popup);
if (!this.popup) throw new Error('Popup blocked. Try again, please!');
}
}
public closePopup() {
// make sure we release the popup
try {
this.popup?.close();
this.popup = null;
} catch {}
}
}
export default Popup;

View File

@@ -0,0 +1,67 @@
import { EventEmitter } from 'tseep';
import { CALL_TIMEOUT } from '../const';
class ProcessManager extends EventEmitter {
private callCount: number = 0;
private callTimer: NodeJS.Timeout | undefined;
constructor() {
super();
}
public onAuthUrl() {
if (Boolean(this.callTimer)) {
clearTimeout(this.callTimer);
}
}
public onIframeUrl() {
if (Boolean(this.callTimer)) {
clearTimeout(this.callTimer);
}
}
public async wait<T>(cb: () => Promise<T>): Promise<T> {
// FIXME only allow 1 parallel req
if (!this.callTimer) {
this.callTimer = setTimeout(() => this.emit('onCallTimeout'), CALL_TIMEOUT);
}
if (!this.callCount) {
this.emit('onCallStart');
}
this.callCount++;
let error;
let result;
try {
result = await cb();
} catch (e) {
error = e;
}
this.callCount--;
this.emit('onCallEnd');
if (this.callTimer) {
clearTimeout(this.callTimer);
}
this.callTimer = undefined;
if (error) {
throw error;
}
// we can't return undefined bcs an exception is
// thrown above on error
// @ts-ignore
return result;
}
}
export default ProcessManager;

View File

@@ -0,0 +1,25 @@
import { NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk';
import { Nip44 } from '../utils/nip44';
import { getPublicKey } from 'nostr-tools';
export class PrivateKeySigner extends NDKPrivateKeySigner {
private nip44: Nip44 = new Nip44();
private _pubkey: string;
constructor(privateKey: string) {
super(privateKey);
this._pubkey = getPublicKey(privateKey);
}
get pubkey() {
return this._pubkey;
}
encryptNip44(recipient: NDKUser, value: string): Promise<string> {
return Promise.resolve(this.nip44.encrypt(this.privateKey!, recipient.pubkey, value));
}
decryptNip44(sender: NDKUser, value: string): Promise<string> {
return Promise.resolve(this.nip44.decrypt(this.privateKey!, sender.pubkey, value));
}
}

View File

@@ -0,0 +1,8 @@
export { default as BannerManager } from './BannerManager';
export { default as AuthNostrService } from './AuthNostrService';
export { default as ModalManager } from './ModalManager';
export { default as Nostr } from './Nostr';
export { default as NostrExtensionService } from './NostrExtensionService';
export { default as NostrParams } from './NostrParams';
export { default as Popup } from './Popup';
export { default as ProcessManager } from './ProcessManager';

124
packages/auth/src/types.ts Normal file
View File

@@ -0,0 +1,124 @@
import { Info, AuthMethod, ConnectionString, RecentType, BannerNotify } from 'nostr-login-components/dist/types/types';
export interface NostrLoginAuthOptions {
localNsec?: string;
relays?: string[];
type: 'login' | 'signup' | 'logout';
method?: AuthMethod;
pubkey?: string;
otpData?: string;
name?: string;
}
// NOTE: must be a subset of CURRENT_MODULE enum
export type StartScreens =
| 'welcome'
| 'welcome-login'
| 'welcome-signup'
| 'signup'
| 'local-signup'
| 'login'
| 'otp'
| 'connect'
| 'login-bunker-url'
| 'login-read-only'
| 'connection-string'
| 'switch-account'
| 'import';
export interface NostrLoginOptions {
// optional
theme?: string;
startScreen?: StartScreens;
bunkers?: string;
onAuth?: (npub: string, options: NostrLoginAuthOptions) => void;
perms?: string;
darkMode?: boolean;
// do not show the banner, modals must be `launch`-ed
noBanner?: boolean;
// forward reqs to this bunker origin for testing
devOverrideBunkerOrigin?: string;
// deprecated, use methods=['local']
// use local signup instead of nostr connect
localSignup?: boolean;
// allowed auth methods
methods?: AuthMethod[];
// otp endpoints
otpRequestUrl?: string;
otpReplyUrl?: string;
// welcome screen's title/desc
title?: string;
description?: string;
// comma-separated list of relays added
// to relay list of new profiles created with local signup
signupRelays?: string;
// relay list to override hardcoded `OUTBOX_RELAYS` constant
outboxRelays?: string[];
// dev mode
dev?: boolean;
// use start.njump.me instead of local signup
signupNstart?: boolean;
// list of npubs to auto/suggest-follow on signup
followNpubs?: string;
// when method call auth needed, instead of showing
// the modal, we start waiting for incoming nip46
// connection and send the nostrconnect string using
// nlNeedAuth event
customNostrConnect?: boolean;
}
export interface IBanner {
userInfo?: Info | null;
titleBanner?: string;
isLoading?: boolean;
listNotifies?: string[];
accounts?: Info[];
isOpen?: boolean;
darkMode?: boolean;
notify?: BannerNotify;
}
export type TypeBanner = IBanner & HTMLElement;
export interface IModal {
authUrl?: string;
iframeUrl?: string;
isLoading?: boolean;
isOTP?: boolean;
isLoadingExtension?: boolean;
localSignup?: boolean;
signupNjump?: boolean;
njumpIframe?: string;
authMethods?: AuthMethod[];
hasExtension?: boolean;
hasOTP?: boolean;
error?: string;
signupNameIsAvailable?: string | boolean;
loginIsGood?: string | boolean;
recents?: RecentType[];
accounts?: Info[];
darkMode?: boolean;
welcomeTitle?: string;
welcomeDescription?: string;
connectionString?: string;
connectionStringServices?: ConnectionString[];
}
export type TypeModal = IModal & HTMLElement;
export interface Response {
result?: string;
error?: string;
}

View File

@@ -0,0 +1,326 @@
import { Info, RecentType } from 'nostr-login-components/dist/types/types';
import NDK, { NDKEvent, NDKRelaySet, NDKSigner, NDKUser } from '@nostr-dev-kit/ndk';
import { generatePrivateKey } from 'nostr-tools';
import { NostrLoginOptions } from '../types';
const LOCAL_STORE_KEY = '__nostrlogin_nip46';
const LOGGED_IN_ACCOUNTS = '__nostrlogin_accounts';
const RECENT_ACCOUNTS = '__nostrlogin_recent';
const OUTBOX_RELAYS = ['wss://purplepag.es', 'wss://relay.nos.social', 'wss://user.kindpag.es', 'wss://relay.damus.io', 'wss://nos.lol'];
const DEFAULT_SIGNUP_RELAYS = ['wss://relay.damus.io/', 'wss://nos.lol/', 'wss://relay.primal.net/'];
export const localStorageSetItem = (key: string, value: string) => {
localStorage.setItem(key, value);
};
export const localStorageGetItem = (key: string) => {
const value = window.localStorage.getItem(key);
if (value) {
try {
return JSON.parse(value);
} catch {}
}
return null;
};
export const localStorageRemoveItem = (key: string) => {
localStorage.removeItem(key);
};
export const fetchProfile = async (info: Info, profileNdk: NDK) => {
const user = new NDKUser({ pubkey: info.pubkey });
user.ndk = profileNdk;
return await user.fetchProfile();
};
export const prepareSignupRelays = (signupRelays?: string) => {
const relays = (signupRelays || '')
.split(',')
.map(r => r.trim())
.filter(r => r.startsWith('ws'));
if (!relays.length) relays.push(...DEFAULT_SIGNUP_RELAYS);
return relays;
};
export const createProfile = async (info: Info, profileNdk: NDK, signer: NDKSigner, signupRelays?: string, outboxRelays?: string[]) => {
const meta = {
name: info.name,
};
const profileEvent = new NDKEvent(profileNdk, {
kind: 0,
created_at: Math.floor(Date.now() / 1000),
pubkey: info.pubkey,
content: JSON.stringify(meta),
tags: [],
});
if (window.location.hostname) profileEvent.tags.push(['client', window.location.hostname]);
const relaysEvent = new NDKEvent(profileNdk, {
kind: 10002,
created_at: Math.floor(Date.now() / 1000),
pubkey: info.pubkey,
content: '',
tags: [],
});
const relays = prepareSignupRelays(signupRelays)
for (const r of relays) {
relaysEvent.tags.push(['r', r]);
}
await profileEvent.sign(signer);
console.log('signed profile', profileEvent);
await relaysEvent.sign(signer);
console.log('signed relays', relaysEvent);
const outboxRelaysFinal = outboxRelays && outboxRelays.length ? outboxRelays : OUTBOX_RELAYS;
await profileEvent.publish(NDKRelaySet.fromRelayUrls(outboxRelaysFinal, profileNdk));
console.log('published profile', profileEvent);
await relaysEvent.publish(NDKRelaySet.fromRelayUrls(outboxRelaysFinal, profileNdk));
console.log('published relays', relaysEvent);
};
export const bunkerUrlToInfo = (bunkerUrl: string, sk = ''): Info => {
const url = new URL(bunkerUrl);
return {
pubkey: '',
signerPubkey: url.hostname || url.pathname.split('//')[1],
sk: sk || generatePrivateKey(),
relays: url.searchParams.getAll('relay'),
token: url.searchParams.get('secret') || '',
authMethod: 'connect',
};
};
export const isBunkerUrl = (value: string) => value.startsWith('bunker://');
export const getBunkerUrl = async (value: string, optionsModal: NostrLoginOptions) => {
if (!value) {
return '';
}
if (isBunkerUrl(value)) {
return value;
}
if (value.includes('@')) {
const [name, domain] = value.toLocaleLowerCase().split('@');
const origin = optionsModal.devOverrideBunkerOrigin || `https://${domain}`;
const bunkerUrl = `${origin}/.well-known/nostr.json?name=_`;
const userUrl = `${origin}/.well-known/nostr.json?name=${name}`;
const bunker = await fetch(bunkerUrl);
const bunkerData = await bunker.json();
const bunkerPubkey = bunkerData.names['_'];
const bunkerRelays = bunkerData.nip46[bunkerPubkey];
const user = await fetch(userUrl);
const userData = await user.json();
const userPubkey = userData.names[name];
// console.log({
// bunkerData, userData, bunkerPubkey, bunkerRelays, userPubkey,
// name, domain, origin
// })
if (!bunkerRelays.length) {
throw new Error('Bunker relay not provided');
}
return `bunker://${userPubkey}?relay=${bunkerRelays[0]}`;
}
throw new Error('Invalid user name or bunker url');
};
export const checkNip05 = async (nip05: string) => {
let available = false;
let error = '';
let pubkey = '';
await (async () => {
if (!nip05 || !nip05.includes('@')) return;
const [name, domain] = nip05.toLocaleLowerCase().split('@');
if (!name) return;
const REGEXP = new RegExp(/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g);
if (!REGEXP.test(nip05)) {
error = 'Invalid name';
return;
}
if (!domain) {
error = 'Select service';
return;
}
const url = `https://${domain}/.well-known/nostr.json?name=${name.toLowerCase()}`;
try {
const r = await fetch(url);
const d = await r.json();
if (d.names[name]) {
pubkey = d.names[name];
return;
}
} catch {}
available = true;
})();
return {
available,
taken: pubkey != '',
error,
pubkey,
};
};
const upgradeInfo = (info: Info | RecentType) => {
if ('typeAuthMethod' in info) delete info['typeAuthMethod'];
if (!info.authMethod) {
if ('extension' in info && info['extension']) info.authMethod = 'extension';
else if ('readOnly' in info && info['readOnly']) info.authMethod = 'readOnly';
else info.authMethod = 'connect';
}
if (info.nip05 && isBunkerUrl(info.nip05)) {
info.bunkerUrl = info.nip05;
info.nip05 = '';
}
if (info.authMethod === 'connect' && !info.signerPubkey) {
info.signerPubkey = info.pubkey;
}
};
export const localStorageAddAccount = (info: Info) => {
// make current
localStorageSetItem(LOCAL_STORE_KEY, JSON.stringify(info));
const loggedInAccounts: Info[] = localStorageGetItem(LOGGED_IN_ACCOUNTS) || [];
const recentAccounts: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
// upgrade first
loggedInAccounts.forEach(a => upgradeInfo(a));
recentAccounts.forEach(a => upgradeInfo(a));
// upsert new info into accounts
const accounts: Info[] = loggedInAccounts;
const index = loggedInAccounts.findIndex((el: Info) => el.pubkey === info.pubkey && el.authMethod === info.authMethod);
if (index !== -1) {
accounts[index] = info;
} else {
accounts.push(info);
}
// remove new info from recent
const recents = recentAccounts.filter(el => el.pubkey !== info.pubkey || el.authMethod !== info.authMethod);
localStorageSetItem(RECENT_ACCOUNTS, JSON.stringify(recents));
localStorageSetItem(LOGGED_IN_ACCOUNTS, JSON.stringify(accounts));
};
export const localStorageRemoveCurrentAccount = () => {
const user: Info = localStorageGetItem(LOCAL_STORE_KEY);
if (!user) return;
// make sure it's valid
upgradeInfo(user);
// remove secret fields
const recentUser: RecentType = { ...user };
// make sure session keys are dropped
// @ts-ignore
delete recentUser['sk'];
// @ts-ignore
delete recentUser['otpData'];
// get accounts and recent
const loggedInAccounts: Info[] = localStorageGetItem(LOGGED_IN_ACCOUNTS) || [];
const recentsAccounts: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
// upgrade first
loggedInAccounts.forEach(a => upgradeInfo(a));
recentsAccounts.forEach(a => upgradeInfo(a));
const recents: RecentType[] = recentsAccounts;
if (recentUser.authMethod === 'connect' && recentUser.bunkerUrl && recentUser.bunkerUrl.includes('secret=')) {
console.log('nostr login bunker conn with a secret not saved to recent');
} else if (recentUser.authMethod === 'local') {
console.log('nostr login temporary local keys not save to recent');
} else {
// upsert to recent
const index = recentsAccounts.findIndex((el: RecentType) => el.pubkey === recentUser.pubkey && el.authMethod === recentUser.authMethod);
if (index !== -1) {
recents[index] = recentUser;
} else {
recents.push(recentUser);
}
}
// remove from accounts
const accounts = loggedInAccounts.filter(el => el.pubkey !== user.pubkey || el.authMethod !== user.authMethod);
// update accounts and recent, clear current
localStorageSetItem(RECENT_ACCOUNTS, JSON.stringify(recents));
localStorageSetItem(LOGGED_IN_ACCOUNTS, JSON.stringify(accounts));
localStorageRemoveItem(LOCAL_STORE_KEY);
};
export const localStorageRemoveRecent = (user: RecentType) => {
const recentsAccounts: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
recentsAccounts.forEach(a => upgradeInfo(a));
const recents = recentsAccounts.filter(el => el.pubkey !== user.pubkey || el.authMethod !== user.authMethod);
localStorageSetItem(RECENT_ACCOUNTS, JSON.stringify(recents));
};
export const localStorageGetRecents = (): RecentType[] => {
const recents: RecentType[] = localStorageGetItem(RECENT_ACCOUNTS) || [];
recents.forEach(r => upgradeInfo(r));
return recents;
};
export const localStorageGetAccounts = (): Info[] => {
const accounts: Info[] = localStorageGetItem(LOGGED_IN_ACCOUNTS) || [];
accounts.forEach(a => upgradeInfo(a));
return accounts;
};
export const localStorageGetCurrent = (): Info | null => {
const info = localStorageGetItem(LOCAL_STORE_KEY);
if (info) upgradeInfo(info);
return info;
};
export const setDarkMode = (dark: boolean) => {
localStorageSetItem('nl-dark-mode', dark ? 'true' : 'false');
};
export const getDarkMode = (opt: NostrLoginOptions) => {
const getDarkModeLocal = localStorage.getItem('nl-dark-mode');
if (getDarkModeLocal) {
// user already changed it
return Boolean(JSON.parse(getDarkModeLocal));
} else if (opt.darkMode !== undefined) {
// app provided an option
return opt.darkMode;
} else {
// auto-detect
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return true;
} else {
return false;
}
}
};
export const getIcon = async () => {
// FIXME look at meta tags or manifest
return document.location.origin + '/favicon.ico';
};

View File

@@ -0,0 +1,185 @@
import { chacha20 } from "@noble/ciphers/chacha"
import { concatBytes, randomBytes, utf8ToBytes } from "@noble/hashes/utils"
import { equalBytes } from "@noble/ciphers/utils";
import { secp256k1 } from "@noble/curves/secp256k1"
import {
expand as hkdf_expand,
extract as hkdf_extract,
} from "@noble/hashes/hkdf"
import { sha256 } from "@noble/hashes/sha256"
import { hmac } from "@noble/hashes/hmac";
import { base64 } from "@scure/base";
import { getPublicKey } from 'nostr-tools'
// from https://github.com/nbd-wtf/nostr-tools
const decoder = new TextDecoder()
const u = {
minPlaintextSize: 0x0001, // 1b msg => padded to 32b
maxPlaintextSize: 0xffff, // 65535 (64kb-1) => padded to 64kb
utf8Encode: utf8ToBytes,
utf8Decode(bytes: Uint8Array): string {
return decoder.decode(bytes);
},
getConversationKey(privkeyA: string, pubkeyB: string): Uint8Array {
const sharedX = secp256k1
.getSharedSecret(privkeyA, "02" + pubkeyB)
.subarray(1, 33);
return hkdf_extract(sha256, sharedX, "nip44-v2");
},
getMessageKeys(conversationKey: Uint8Array, nonce: Uint8Array) {
const keys = hkdf_expand(sha256, conversationKey, nonce, 76);
return {
chacha_key: keys.subarray(0, 32),
chacha_nonce: keys.subarray(32, 44),
hmac_key: keys.subarray(44, 76),
};
},
calcPaddedLen(len: number): number {
if (!Number.isSafeInteger(len) || len < 1)
throw new Error("expected positive integer");
if (len <= 32) return 32;
const nextPower = 1 << (Math.floor(Math.log2(len - 1)) + 1);
const chunk = nextPower <= 256 ? 32 : nextPower / 8;
return chunk * (Math.floor((len - 1) / chunk) + 1);
},
writeU16BE(num: number): Uint8Array {
if (
!Number.isSafeInteger(num) ||
num < u.minPlaintextSize ||
num > u.maxPlaintextSize
)
throw new Error(
"invalid plaintext size: must be between 1 and 65535 bytes"
);
const arr = new Uint8Array(2);
new DataView(arr.buffer).setUint16(0, num, false);
return arr;
},
pad(plaintext: string): Uint8Array {
const unpadded = u.utf8Encode(plaintext);
const unpaddedLen = unpadded.length;
const prefix = u.writeU16BE(unpaddedLen);
const suffix = new Uint8Array(u.calcPaddedLen(unpaddedLen) - unpaddedLen);
return concatBytes(prefix, unpadded, suffix);
},
unpad(padded: Uint8Array): string {
const unpaddedLen = new DataView(padded.buffer).getUint16(0);
const unpadded = padded.subarray(2, 2 + unpaddedLen);
if (
unpaddedLen < u.minPlaintextSize ||
unpaddedLen > u.maxPlaintextSize ||
unpadded.length !== unpaddedLen ||
padded.length !== 2 + u.calcPaddedLen(unpaddedLen)
)
throw new Error("invalid padding");
return u.utf8Decode(unpadded);
},
hmacAad(key: Uint8Array, message: Uint8Array, aad: Uint8Array): Uint8Array {
if (aad.length !== 32)
throw new Error("AAD associated data must be 32 bytes");
const combined = concatBytes(aad, message);
return hmac(sha256, key, combined);
},
// metadata: always 65b (version: 1b, nonce: 32b, max: 32b)
// plaintext: 1b to 0xffff
// padded plaintext: 32b to 0xffff
// ciphertext: 32b+2 to 0xffff+2
// raw payload: 99 (65+32+2) to 65603 (65+0xffff+2)
// compressed payload (base64): 132b to 87472b
decodePayload(payload: string): {
nonce: Uint8Array;
ciphertext: Uint8Array;
mac: Uint8Array;
} {
if (typeof payload !== "string")
throw new Error("payload must be a valid string");
const plen = payload.length;
if (plen < 132 || plen > 87472)
throw new Error("invalid payload length: " + plen);
if (payload[0] === "#") throw new Error("unknown encryption version");
let data: Uint8Array;
try {
data = base64.decode(payload);
} catch (error) {
throw new Error("invalid base64: " + (error as Error).message);
}
const dlen = data.length;
if (dlen < 99 || dlen > 65603)
throw new Error("invalid data length: " + dlen);
const vers = data[0];
if (vers !== 2) throw new Error("unknown encryption version " + vers);
return {
nonce: data.subarray(1, 33),
ciphertext: data.subarray(33, -32),
mac: data.subarray(-32),
};
},
};
export function encryptNip44(
plaintext: string,
conversationKey: Uint8Array,
nonce: Uint8Array = randomBytes(32)
): string {
const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(
conversationKey,
nonce
);
const padded = u.pad(plaintext);
const ciphertext = chacha20(chacha_key, chacha_nonce, padded);
const mac = u.hmacAad(hmac_key, ciphertext, nonce);
return base64.encode(
concatBytes(new Uint8Array([2]), nonce, ciphertext, mac)
);
}
export function decryptNip44(payload: string, conversationKey: Uint8Array): string {
const { nonce, ciphertext, mac } = u.decodePayload(payload);
const { chacha_key, chacha_nonce, hmac_key } = u.getMessageKeys(
conversationKey,
nonce
);
const calculatedMac = u.hmacAad(hmac_key, ciphertext, nonce);
if (!equalBytes(calculatedMac, mac)) throw new Error("invalid MAC");
const padded = chacha20(chacha_key, chacha_nonce, ciphertext);
return u.unpad(padded);
}
export class Nip44 {
private cache = new Map<string, Uint8Array>()
public createKey(privkey: string, pubkey: string) {
return u.getConversationKey(privkey, pubkey)
}
private getKey(privkey: string, pubkey: string, extractable?: boolean) {
const id = getPublicKey(privkey) + pubkey
let cryptoKey = this.cache.get(id)
if (cryptoKey) return cryptoKey
const key = this.createKey(privkey, pubkey)
this.cache.set(id, key)
return key
}
public encrypt(privkey: string, pubkey: string, text: string): string {
const key = this.getKey(privkey, pubkey)
return encryptNip44(text, key)
}
public decrypt(privkey: string, pubkey: string, data: string): string {
const key = this.getKey(privkey, pubkey)
return decryptNip44(data, key)
}
}

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"declaration": true,
"outDir": "./dist",
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"],
}

View File

@@ -0,0 +1,15 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

26
packages/components/.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
dist/
www/
loader/
*~
*.sw[mnpcod]
*.log
*.lock
*.tmp
*.tmp.*
log.txt
*.sublime-project
*.sublime-workspace
.stencil/
.idea/
.vscode/
.sass-cache/
.versions/
node_modules/
$RECYCLE.BIN/
.DS_Store
Thumbs.db
UserInterfaceState.xcuserstate
.env

View File

@@ -0,0 +1,13 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"quoteProps": "consistent",
"printWidth": 180,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}

View File

@@ -0,0 +1,49 @@
{
"name": "nostr-login-components",
"version": "1.0.3",
"main": "dist/index.cjs.js",
"module": "dist/index.js",
"es2015": "dist/esm/index.js",
"es2017": "dist/esm/index.js",
"types": "dist/types/index.d.ts",
"collection": "dist/collection/collection-manifest.json",
"collection:main": "dist/collection/index.js",
"author": "a-fralou",
"files": [
"dist/",
"loader/"
],
"exports": {
".": "./dist/components/index.js"
},
"scripts": {
"build": "stencil build --docs --prod && node post-build-plugin.js",
"dev": "stencil build --dev --watch --serve",
"test": "stencil test --spec --e2e",
"test.watch": "stencil test --spec --e2e --watchAll",
"generate": "stencil generate",
"format": "npx prettier --write src"
},
"dependencies": {
"@stencil/core": "^4.20.0",
"@stencil/sass": "^3.0.12",
"@stencil/store": "^2.0.16",
"@tailwindcss/forms": "^0.5.7",
"qrcode": "^1.5.4",
"tailwindcss": "^3.4.0",
"tailwindcss-rtl": "^0.9.0"
},
"devDependencies": {
"@types/jest": "^29.5.6",
"@types/node": "^16.18.11",
"@types/qrcode": "^1.5.5",
"jest": "^29.7.0",
"jest-cli": "^29.7.0",
"prettier": "^3.2.2",
"puppeteer": "21.1.1",
"stencil-tailwind-plugin": "^1.8.0",
"typescript": "^5.3.3",
"workbox-build": "^4.3.1"
},
"license": "MIT"
}

View File

@@ -0,0 +1,48 @@
const fs = require('fs');
const path = require('path');
const folderPath = './dist/components';
const cssFilePath = './dist/components/css.js';
let countFileRead = 0;
// Function to get all JavaScript files in the directory
function getFilesInDirectory(dir) {
const files = fs.readdirSync(dir);
return files.filter(file => file.endsWith('.js')); // Filter out only .js files
}
// Function to process a single file
function processFile(filePath, cssContent) {
const fileContent = fs.readFileSync(filePath, 'utf8');
// Replace the CSS variable declarations and import them from css.js
const updatedContent = fileContent.replace(/const\s+(\w+Css)\s*=\s*(`[^`]*`|".*?"|'.*?')(?:\s*;|(\s*\/\/.*?))?(?=\s*const|$)/g, (match, varName, varValue) => {
// Save CSS variables and their values in cssContent
if (countFileRead === 0) {
cssContent.push(`export const baseCss = ${varValue};`);
countFileRead = countFileRead + 1;
}
// Return the string with the import from css.js
return `import { baseCss } from './css.js';\nconst ${varName} = baseCss;\n`;
});
// Write the modified content back to the file
fs.writeFileSync(filePath, updatedContent, 'utf8');
}
// Main function to process all files
function main() {
const cssContent = [];
const files = getFilesInDirectory(folderPath);
files.forEach(file => {
const filePath = path.join(folderPath, file);
processFile(filePath, cssContent);
});
// Write collected CSS variables to css.js
fs.writeFileSync(cssFilePath, cssContent.join('\n'), 'utf8');
console.log('Done! All CSS variables have been moved to css.js.');
}
main();

929
packages/components/src/components.d.ts vendored Normal file
View File

@@ -0,0 +1,929 @@
/* eslint-disable */
/* tslint:disable */
/**
* This is an autogenerated file created by the Stencil compiler.
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { AuthMethod, BannerNotify, ConnectionString, Info, NlTheme, RecentType } from "./types/index";
import { OptionType } from "./components/nl-select/nl-select";
export { AuthMethod, BannerNotify, ConnectionString, Info, NlTheme, RecentType } from "./types/index";
export { OptionType } from "./components/nl-select/nl-select";
export namespace Components {
interface ButtonBase {
"darkMode": boolean;
"disabled": boolean;
"theme": NlTheme;
"titleBtn": string;
}
interface NlAuth {
"accounts": Info[];
"authMethods": AuthMethod[];
"authUrl": string;
"bunkers": string;
"connectionString": string;
"connectionStringServices": ConnectionString[];
"darkMode": boolean;
"error": string;
"hasExtension": boolean;
"hasOTP": boolean;
"iframeUrl": string;
"isLoading": boolean;
"isLoadingExtension": boolean;
"isOTP": boolean;
"localSignup": boolean;
"njumpIframe": string;
"recents": RecentType[];
"signupNjump": boolean;
"startScreen": string;
"theme": NlTheme;
"welcomeDescription": string;
"welcomeTitle": string;
}
interface NlBanner {
"accounts": Info[];
"darkMode": boolean;
"hiddenMode": boolean;
"isLoading": boolean;
"isOpen": boolean;
"notify": BannerNotify | null;
"theme": NlTheme;
"titleBanner": string;
"userInfo": Info | null;
}
interface NlButton {
"darkMode": boolean;
"disabled": boolean;
"theme": NlTheme;
"titleBtn": string;
}
interface NlChangeAccount {
"accounts": Info[];
"currentAccount": Info;
"darkMode": boolean;
"theme": 'default' | 'ocean' | 'lemonade' | 'purple';
}
interface NlConfirmLogout {
"description": string;
"titleModal": string;
}
interface NlConnect {
"authMethods": AuthMethod[];
"connectionStringServices": ConnectionString[];
"hasOTP": boolean;
"titleWelcome": string;
}
interface NlDialog {
}
interface NlIframe {
"description": string;
"iframeUrl": string;
"titleModal": string;
}
interface NlImportFlow {
"services": ConnectionString[];
"titleImport": string;
"titleInfo": string;
}
interface NlInfo {
"darkMode": boolean;
"theme": NlTheme;
}
interface NlInfoExtension {
}
interface NlLoading {
"path": string;
}
interface NlLocalSignup {
"description": string;
"descriptionNjump": string;
"signupNjump": boolean;
"titleSignup": string;
}
interface NlLoginStatus {
"info": RecentType | Info | undefined;
}
interface NlOtpMigrate {
"services": ConnectionString[];
"textImport": string;
"titleImport": string;
"titleInfo": string;
}
interface NlPreviouslyLogged {
"accounts": Info[];
"description": string;
"recents": RecentType[];
"titlePage": string;
}
interface NlSelect {
"darkMode": boolean;
"options": OptionType[];
"selected": number;
"theme": 'default' | 'ocean' | 'lemonade' | 'purple';
}
interface NlSignin {
"description": string;
"titleLogin": string;
}
interface NlSigninBunkerUrl {
"description": string;
"titleLogin": string;
}
interface NlSigninConnectionString {
"connectionString": string;
"description": string;
"titleLogin": string;
}
interface NlSigninOtp {
"description": string;
"descriptionOTP": string;
"titleLogin": string;
"titleLoginOTP": string;
}
interface NlSigninReadOnly {
"description": string;
"titleLogin": string;
}
interface NlSignup {
"bunkers": string;
"description": string;
"titleSignup": string;
}
interface NlWelcome {
"description": string;
"titleWelcome": string;
}
interface NlWelcomeSignin {
"authMethods": AuthMethod[];
"hasExtension": boolean;
"hasOTP": boolean;
"titleWelcome": string;
}
interface NlWelcomeSignup {
"description": string;
"titleWelcome": string;
}
}
export interface NlAuthCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlAuthElement;
}
export interface NlBannerCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlBannerElement;
}
export interface NlChangeAccountCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlChangeAccountElement;
}
export interface NlConfirmLogoutCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlConfirmLogoutElement;
}
export interface NlConnectCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlConnectElement;
}
export interface NlIframeCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlIframeElement;
}
export interface NlImportFlowCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlImportFlowElement;
}
export interface NlLoadingCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlLoadingElement;
}
export interface NlLocalSignupCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlLocalSignupElement;
}
export interface NlOtpMigrateCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlOtpMigrateElement;
}
export interface NlPreviouslyLoggedCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlPreviouslyLoggedElement;
}
export interface NlSelectCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlSelectElement;
}
export interface NlSigninCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlSigninElement;
}
export interface NlSigninBunkerUrlCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlSigninBunkerUrlElement;
}
export interface NlSigninConnectionStringCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlSigninConnectionStringElement;
}
export interface NlSigninOtpCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlSigninOtpElement;
}
export interface NlSigninReadOnlyCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlSigninReadOnlyElement;
}
export interface NlSignupCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlSignupElement;
}
export interface NlWelcomeSigninCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLNlWelcomeSigninElement;
}
declare global {
interface HTMLButtonBaseElement extends Components.ButtonBase, HTMLStencilElement {
}
var HTMLButtonBaseElement: {
prototype: HTMLButtonBaseElement;
new (): HTMLButtonBaseElement;
};
interface HTMLNlAuthElementEventMap {
"nlCloseModal": any;
"nlChangeDarkMode": boolean;
"nlNostrConnectDefaultCancel": void;
}
interface HTMLNlAuthElement extends Components.NlAuth, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlAuthElementEventMap>(type: K, listener: (this: HTMLNlAuthElement, ev: NlAuthCustomEvent<HTMLNlAuthElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlAuthElementEventMap>(type: K, listener: (this: HTMLNlAuthElement, ev: NlAuthCustomEvent<HTMLNlAuthElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlAuthElement: {
prototype: HTMLNlAuthElement;
new (): HTMLNlAuthElement;
};
interface HTMLNlBannerElementEventMap {
"handleNotifyConfirmBanner": string;
"handleNotifyConfirmBannerIframe": string;
"handleLoginBanner": string;
"handleLogoutBanner": string;
"handleOpenWelcomeModal": string;
"handleConfirmLogout": string;
"handleImportModal": string;
}
interface HTMLNlBannerElement extends Components.NlBanner, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlBannerElementEventMap>(type: K, listener: (this: HTMLNlBannerElement, ev: NlBannerCustomEvent<HTMLNlBannerElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlBannerElementEventMap>(type: K, listener: (this: HTMLNlBannerElement, ev: NlBannerCustomEvent<HTMLNlBannerElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlBannerElement: {
prototype: HTMLNlBannerElement;
new (): HTMLNlBannerElement;
};
interface HTMLNlButtonElement extends Components.NlButton, HTMLStencilElement {
}
var HTMLNlButtonElement: {
prototype: HTMLNlButtonElement;
new (): HTMLNlButtonElement;
};
interface HTMLNlChangeAccountElementEventMap {
"handleOpenWelcomeModal": string;
"handleSwitchAccount": Info;
}
interface HTMLNlChangeAccountElement extends Components.NlChangeAccount, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlChangeAccountElementEventMap>(type: K, listener: (this: HTMLNlChangeAccountElement, ev: NlChangeAccountCustomEvent<HTMLNlChangeAccountElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlChangeAccountElementEventMap>(type: K, listener: (this: HTMLNlChangeAccountElement, ev: NlChangeAccountCustomEvent<HTMLNlChangeAccountElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlChangeAccountElement: {
prototype: HTMLNlChangeAccountElement;
new (): HTMLNlChangeAccountElement;
};
interface HTMLNlConfirmLogoutElementEventMap {
"handleLogoutBanner": string;
"handleBackUpModal": string;
"nlCloseModal": any;
}
interface HTMLNlConfirmLogoutElement extends Components.NlConfirmLogout, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlConfirmLogoutElementEventMap>(type: K, listener: (this: HTMLNlConfirmLogoutElement, ev: NlConfirmLogoutCustomEvent<HTMLNlConfirmLogoutElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlConfirmLogoutElementEventMap>(type: K, listener: (this: HTMLNlConfirmLogoutElement, ev: NlConfirmLogoutCustomEvent<HTMLNlConfirmLogoutElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlConfirmLogoutElement: {
prototype: HTMLNlConfirmLogoutElement;
new (): HTMLNlConfirmLogoutElement;
};
interface HTMLNlConnectElementEventMap {
"nlNostrConnect": ConnectionString;
}
interface HTMLNlConnectElement extends Components.NlConnect, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlConnectElementEventMap>(type: K, listener: (this: HTMLNlConnectElement, ev: NlConnectCustomEvent<HTMLNlConnectElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlConnectElementEventMap>(type: K, listener: (this: HTMLNlConnectElement, ev: NlConnectCustomEvent<HTMLNlConnectElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlConnectElement: {
prototype: HTMLNlConnectElement;
new (): HTMLNlConnectElement;
};
interface HTMLNlDialogElement extends Components.NlDialog, HTMLStencilElement {
}
var HTMLNlDialogElement: {
prototype: HTMLNlDialogElement;
new (): HTMLNlDialogElement;
};
interface HTMLNlIframeElementEventMap {
"nlCloseModal": any;
}
interface HTMLNlIframeElement extends Components.NlIframe, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlIframeElementEventMap>(type: K, listener: (this: HTMLNlIframeElement, ev: NlIframeCustomEvent<HTMLNlIframeElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlIframeElementEventMap>(type: K, listener: (this: HTMLNlIframeElement, ev: NlIframeCustomEvent<HTMLNlIframeElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlIframeElement: {
prototype: HTMLNlIframeElement;
new (): HTMLNlIframeElement;
};
interface HTMLNlImportFlowElementEventMap {
"nlImportAccount": ConnectionString;
"nlExportKeys": void;
}
interface HTMLNlImportFlowElement extends Components.NlImportFlow, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlImportFlowElementEventMap>(type: K, listener: (this: HTMLNlImportFlowElement, ev: NlImportFlowCustomEvent<HTMLNlImportFlowElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlImportFlowElementEventMap>(type: K, listener: (this: HTMLNlImportFlowElement, ev: NlImportFlowCustomEvent<HTMLNlImportFlowElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlImportFlowElement: {
prototype: HTMLNlImportFlowElement;
new (): HTMLNlImportFlowElement;
};
interface HTMLNlInfoElement extends Components.NlInfo, HTMLStencilElement {
}
var HTMLNlInfoElement: {
prototype: HTMLNlInfoElement;
new (): HTMLNlInfoElement;
};
interface HTMLNlInfoExtensionElement extends Components.NlInfoExtension, HTMLStencilElement {
}
var HTMLNlInfoExtensionElement: {
prototype: HTMLNlInfoExtensionElement;
new (): HTMLNlInfoExtensionElement;
};
interface HTMLNlLoadingElementEventMap {
"stopFetchHandler": boolean;
"handleContinue": boolean;
}
interface HTMLNlLoadingElement extends Components.NlLoading, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlLoadingElementEventMap>(type: K, listener: (this: HTMLNlLoadingElement, ev: NlLoadingCustomEvent<HTMLNlLoadingElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlLoadingElementEventMap>(type: K, listener: (this: HTMLNlLoadingElement, ev: NlLoadingCustomEvent<HTMLNlLoadingElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlLoadingElement: {
prototype: HTMLNlLoadingElement;
new (): HTMLNlLoadingElement;
};
interface HTMLNlLocalSignupElementEventMap {
"nlLocalSignup": string;
"nlSignupNjump": void;
"fetchHandler": boolean;
}
interface HTMLNlLocalSignupElement extends Components.NlLocalSignup, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlLocalSignupElementEventMap>(type: K, listener: (this: HTMLNlLocalSignupElement, ev: NlLocalSignupCustomEvent<HTMLNlLocalSignupElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlLocalSignupElementEventMap>(type: K, listener: (this: HTMLNlLocalSignupElement, ev: NlLocalSignupCustomEvent<HTMLNlLocalSignupElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlLocalSignupElement: {
prototype: HTMLNlLocalSignupElement;
new (): HTMLNlLocalSignupElement;
};
interface HTMLNlLoginStatusElement extends Components.NlLoginStatus, HTMLStencilElement {
}
var HTMLNlLoginStatusElement: {
prototype: HTMLNlLoginStatusElement;
new (): HTMLNlLoginStatusElement;
};
interface HTMLNlOtpMigrateElementEventMap {
"nlImportAccount": ConnectionString;
}
interface HTMLNlOtpMigrateElement extends Components.NlOtpMigrate, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlOtpMigrateElementEventMap>(type: K, listener: (this: HTMLNlOtpMigrateElement, ev: NlOtpMigrateCustomEvent<HTMLNlOtpMigrateElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlOtpMigrateElementEventMap>(type: K, listener: (this: HTMLNlOtpMigrateElement, ev: NlOtpMigrateCustomEvent<HTMLNlOtpMigrateElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlOtpMigrateElement: {
prototype: HTMLNlOtpMigrateElement;
new (): HTMLNlOtpMigrateElement;
};
interface HTMLNlPreviouslyLoggedElementEventMap {
"nlSwitchAccount": Info;
"nlLoginRecentAccount": RecentType;
"nlRemoveRecent": RecentType;
}
interface HTMLNlPreviouslyLoggedElement extends Components.NlPreviouslyLogged, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlPreviouslyLoggedElementEventMap>(type: K, listener: (this: HTMLNlPreviouslyLoggedElement, ev: NlPreviouslyLoggedCustomEvent<HTMLNlPreviouslyLoggedElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlPreviouslyLoggedElementEventMap>(type: K, listener: (this: HTMLNlPreviouslyLoggedElement, ev: NlPreviouslyLoggedCustomEvent<HTMLNlPreviouslyLoggedElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlPreviouslyLoggedElement: {
prototype: HTMLNlPreviouslyLoggedElement;
new (): HTMLNlPreviouslyLoggedElement;
};
interface HTMLNlSelectElementEventMap {
"selectDomain": string;
}
interface HTMLNlSelectElement extends Components.NlSelect, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlSelectElementEventMap>(type: K, listener: (this: HTMLNlSelectElement, ev: NlSelectCustomEvent<HTMLNlSelectElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlSelectElementEventMap>(type: K, listener: (this: HTMLNlSelectElement, ev: NlSelectCustomEvent<HTMLNlSelectElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlSelectElement: {
prototype: HTMLNlSelectElement;
new (): HTMLNlSelectElement;
};
interface HTMLNlSigninElementEventMap {
"nlLogin": string;
"nlCheckLogin": string;
}
interface HTMLNlSigninElement extends Components.NlSignin, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlSigninElementEventMap>(type: K, listener: (this: HTMLNlSigninElement, ev: NlSigninCustomEvent<HTMLNlSigninElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlSigninElementEventMap>(type: K, listener: (this: HTMLNlSigninElement, ev: NlSigninCustomEvent<HTMLNlSigninElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlSigninElement: {
prototype: HTMLNlSigninElement;
new (): HTMLNlSigninElement;
};
interface HTMLNlSigninBunkerUrlElementEventMap {
"nlLogin": string;
"nlCheckLogin": string;
}
interface HTMLNlSigninBunkerUrlElement extends Components.NlSigninBunkerUrl, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlSigninBunkerUrlElementEventMap>(type: K, listener: (this: HTMLNlSigninBunkerUrlElement, ev: NlSigninBunkerUrlCustomEvent<HTMLNlSigninBunkerUrlElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlSigninBunkerUrlElementEventMap>(type: K, listener: (this: HTMLNlSigninBunkerUrlElement, ev: NlSigninBunkerUrlCustomEvent<HTMLNlSigninBunkerUrlElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlSigninBunkerUrlElement: {
prototype: HTMLNlSigninBunkerUrlElement;
new (): HTMLNlSigninBunkerUrlElement;
};
interface HTMLNlSigninConnectionStringElementEventMap {
"nlNostrConnectDefault": void;
}
interface HTMLNlSigninConnectionStringElement extends Components.NlSigninConnectionString, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlSigninConnectionStringElementEventMap>(type: K, listener: (this: HTMLNlSigninConnectionStringElement, ev: NlSigninConnectionStringCustomEvent<HTMLNlSigninConnectionStringElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlSigninConnectionStringElementEventMap>(type: K, listener: (this: HTMLNlSigninConnectionStringElement, ev: NlSigninConnectionStringCustomEvent<HTMLNlSigninConnectionStringElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlSigninConnectionStringElement: {
prototype: HTMLNlSigninConnectionStringElement;
new (): HTMLNlSigninConnectionStringElement;
};
interface HTMLNlSigninOtpElementEventMap {
"nlLoginOTPUser": string;
"nlLoginOTPCode": string;
"nlCheckLogin": string;
}
interface HTMLNlSigninOtpElement extends Components.NlSigninOtp, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlSigninOtpElementEventMap>(type: K, listener: (this: HTMLNlSigninOtpElement, ev: NlSigninOtpCustomEvent<HTMLNlSigninOtpElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlSigninOtpElementEventMap>(type: K, listener: (this: HTMLNlSigninOtpElement, ev: NlSigninOtpCustomEvent<HTMLNlSigninOtpElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlSigninOtpElement: {
prototype: HTMLNlSigninOtpElement;
new (): HTMLNlSigninOtpElement;
};
interface HTMLNlSigninReadOnlyElementEventMap {
"nlLoginReadOnly": string;
"nlCheckLogin": string;
}
interface HTMLNlSigninReadOnlyElement extends Components.NlSigninReadOnly, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlSigninReadOnlyElementEventMap>(type: K, listener: (this: HTMLNlSigninReadOnlyElement, ev: NlSigninReadOnlyCustomEvent<HTMLNlSigninReadOnlyElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlSigninReadOnlyElementEventMap>(type: K, listener: (this: HTMLNlSigninReadOnlyElement, ev: NlSigninReadOnlyCustomEvent<HTMLNlSigninReadOnlyElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlSigninReadOnlyElement: {
prototype: HTMLNlSigninReadOnlyElement;
new (): HTMLNlSigninReadOnlyElement;
};
interface HTMLNlSignupElementEventMap {
"nlSignup": string;
"nlCheckSignup": string;
"fetchHandler": boolean;
}
interface HTMLNlSignupElement extends Components.NlSignup, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlSignupElementEventMap>(type: K, listener: (this: HTMLNlSignupElement, ev: NlSignupCustomEvent<HTMLNlSignupElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlSignupElementEventMap>(type: K, listener: (this: HTMLNlSignupElement, ev: NlSignupCustomEvent<HTMLNlSignupElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlSignupElement: {
prototype: HTMLNlSignupElement;
new (): HTMLNlSignupElement;
};
interface HTMLNlWelcomeElement extends Components.NlWelcome, HTMLStencilElement {
}
var HTMLNlWelcomeElement: {
prototype: HTMLNlWelcomeElement;
new (): HTMLNlWelcomeElement;
};
interface HTMLNlWelcomeSigninElementEventMap {
"nlLoginExtension": void;
}
interface HTMLNlWelcomeSigninElement extends Components.NlWelcomeSignin, HTMLStencilElement {
addEventListener<K extends keyof HTMLNlWelcomeSigninElementEventMap>(type: K, listener: (this: HTMLNlWelcomeSigninElement, ev: NlWelcomeSigninCustomEvent<HTMLNlWelcomeSigninElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLNlWelcomeSigninElementEventMap>(type: K, listener: (this: HTMLNlWelcomeSigninElement, ev: NlWelcomeSigninCustomEvent<HTMLNlWelcomeSigninElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLNlWelcomeSigninElement: {
prototype: HTMLNlWelcomeSigninElement;
new (): HTMLNlWelcomeSigninElement;
};
interface HTMLNlWelcomeSignupElement extends Components.NlWelcomeSignup, HTMLStencilElement {
}
var HTMLNlWelcomeSignupElement: {
prototype: HTMLNlWelcomeSignupElement;
new (): HTMLNlWelcomeSignupElement;
};
interface HTMLElementTagNameMap {
"button-base": HTMLButtonBaseElement;
"nl-auth": HTMLNlAuthElement;
"nl-banner": HTMLNlBannerElement;
"nl-button": HTMLNlButtonElement;
"nl-change-account": HTMLNlChangeAccountElement;
"nl-confirm-logout": HTMLNlConfirmLogoutElement;
"nl-connect": HTMLNlConnectElement;
"nl-dialog": HTMLNlDialogElement;
"nl-iframe": HTMLNlIframeElement;
"nl-import-flow": HTMLNlImportFlowElement;
"nl-info": HTMLNlInfoElement;
"nl-info-extension": HTMLNlInfoExtensionElement;
"nl-loading": HTMLNlLoadingElement;
"nl-local-signup": HTMLNlLocalSignupElement;
"nl-login-status": HTMLNlLoginStatusElement;
"nl-otp-migrate": HTMLNlOtpMigrateElement;
"nl-previously-logged": HTMLNlPreviouslyLoggedElement;
"nl-select": HTMLNlSelectElement;
"nl-signin": HTMLNlSigninElement;
"nl-signin-bunker-url": HTMLNlSigninBunkerUrlElement;
"nl-signin-connection-string": HTMLNlSigninConnectionStringElement;
"nl-signin-otp": HTMLNlSigninOtpElement;
"nl-signin-read-only": HTMLNlSigninReadOnlyElement;
"nl-signup": HTMLNlSignupElement;
"nl-welcome": HTMLNlWelcomeElement;
"nl-welcome-signin": HTMLNlWelcomeSigninElement;
"nl-welcome-signup": HTMLNlWelcomeSignupElement;
}
}
declare namespace LocalJSX {
interface ButtonBase {
"darkMode"?: boolean;
"disabled"?: boolean;
"theme"?: NlTheme;
"titleBtn"?: string;
}
interface NlAuth {
"accounts"?: Info[];
"authMethods"?: AuthMethod[];
"authUrl"?: string;
"bunkers"?: string;
"connectionString"?: string;
"connectionStringServices"?: ConnectionString[];
"darkMode"?: boolean;
"error"?: string;
"hasExtension"?: boolean;
"hasOTP"?: boolean;
"iframeUrl"?: string;
"isLoading"?: boolean;
"isLoadingExtension"?: boolean;
"isOTP"?: boolean;
"localSignup"?: boolean;
"njumpIframe"?: string;
"onNlChangeDarkMode"?: (event: NlAuthCustomEvent<boolean>) => void;
"onNlCloseModal"?: (event: NlAuthCustomEvent<any>) => void;
"onNlNostrConnectDefaultCancel"?: (event: NlAuthCustomEvent<void>) => void;
"recents"?: RecentType[];
"signupNjump"?: boolean;
"startScreen"?: string;
"theme"?: NlTheme;
"welcomeDescription"?: string;
"welcomeTitle"?: string;
}
interface NlBanner {
"accounts"?: Info[];
"darkMode"?: boolean;
"hiddenMode"?: boolean;
"isLoading"?: boolean;
"isOpen"?: boolean;
"notify"?: BannerNotify | null;
"onHandleConfirmLogout"?: (event: NlBannerCustomEvent<string>) => void;
"onHandleImportModal"?: (event: NlBannerCustomEvent<string>) => void;
"onHandleLoginBanner"?: (event: NlBannerCustomEvent<string>) => void;
"onHandleLogoutBanner"?: (event: NlBannerCustomEvent<string>) => void;
"onHandleNotifyConfirmBanner"?: (event: NlBannerCustomEvent<string>) => void;
"onHandleNotifyConfirmBannerIframe"?: (event: NlBannerCustomEvent<string>) => void;
"onHandleOpenWelcomeModal"?: (event: NlBannerCustomEvent<string>) => void;
"theme"?: NlTheme;
"titleBanner"?: string;
"userInfo"?: Info | null;
}
interface NlButton {
"darkMode"?: boolean;
"disabled"?: boolean;
"theme"?: NlTheme;
"titleBtn"?: string;
}
interface NlChangeAccount {
"accounts"?: Info[];
"currentAccount"?: Info;
"darkMode"?: boolean;
"onHandleOpenWelcomeModal"?: (event: NlChangeAccountCustomEvent<string>) => void;
"onHandleSwitchAccount"?: (event: NlChangeAccountCustomEvent<Info>) => void;
"theme"?: 'default' | 'ocean' | 'lemonade' | 'purple';
}
interface NlConfirmLogout {
"description"?: string;
"onHandleBackUpModal"?: (event: NlConfirmLogoutCustomEvent<string>) => void;
"onHandleLogoutBanner"?: (event: NlConfirmLogoutCustomEvent<string>) => void;
"onNlCloseModal"?: (event: NlConfirmLogoutCustomEvent<any>) => void;
"titleModal"?: string;
}
interface NlConnect {
"authMethods"?: AuthMethod[];
"connectionStringServices"?: ConnectionString[];
"hasOTP"?: boolean;
"onNlNostrConnect"?: (event: NlConnectCustomEvent<ConnectionString>) => void;
"titleWelcome"?: string;
}
interface NlDialog {
}
interface NlIframe {
"description"?: string;
"iframeUrl"?: string;
"onNlCloseModal"?: (event: NlIframeCustomEvent<any>) => void;
"titleModal"?: string;
}
interface NlImportFlow {
"onNlExportKeys"?: (event: NlImportFlowCustomEvent<void>) => void;
"onNlImportAccount"?: (event: NlImportFlowCustomEvent<ConnectionString>) => void;
"services"?: ConnectionString[];
"titleImport"?: string;
"titleInfo"?: string;
}
interface NlInfo {
"darkMode"?: boolean;
"theme"?: NlTheme;
}
interface NlInfoExtension {
}
interface NlLoading {
"onHandleContinue"?: (event: NlLoadingCustomEvent<boolean>) => void;
"onStopFetchHandler"?: (event: NlLoadingCustomEvent<boolean>) => void;
"path"?: string;
}
interface NlLocalSignup {
"description"?: string;
"descriptionNjump"?: string;
"onFetchHandler"?: (event: NlLocalSignupCustomEvent<boolean>) => void;
"onNlLocalSignup"?: (event: NlLocalSignupCustomEvent<string>) => void;
"onNlSignupNjump"?: (event: NlLocalSignupCustomEvent<void>) => void;
"signupNjump"?: boolean;
"titleSignup"?: string;
}
interface NlLoginStatus {
"info"?: RecentType | Info | undefined;
}
interface NlOtpMigrate {
"onNlImportAccount"?: (event: NlOtpMigrateCustomEvent<ConnectionString>) => void;
"services"?: ConnectionString[];
"textImport"?: string;
"titleImport"?: string;
"titleInfo"?: string;
}
interface NlPreviouslyLogged {
"accounts"?: Info[];
"description"?: string;
"onNlLoginRecentAccount"?: (event: NlPreviouslyLoggedCustomEvent<RecentType>) => void;
"onNlRemoveRecent"?: (event: NlPreviouslyLoggedCustomEvent<RecentType>) => void;
"onNlSwitchAccount"?: (event: NlPreviouslyLoggedCustomEvent<Info>) => void;
"recents"?: RecentType[];
"titlePage"?: string;
}
interface NlSelect {
"darkMode"?: boolean;
"onSelectDomain"?: (event: NlSelectCustomEvent<string>) => void;
"options"?: OptionType[];
"selected"?: number;
"theme"?: 'default' | 'ocean' | 'lemonade' | 'purple';
}
interface NlSignin {
"description"?: string;
"onNlCheckLogin"?: (event: NlSigninCustomEvent<string>) => void;
"onNlLogin"?: (event: NlSigninCustomEvent<string>) => void;
"titleLogin"?: string;
}
interface NlSigninBunkerUrl {
"description"?: string;
"onNlCheckLogin"?: (event: NlSigninBunkerUrlCustomEvent<string>) => void;
"onNlLogin"?: (event: NlSigninBunkerUrlCustomEvent<string>) => void;
"titleLogin"?: string;
}
interface NlSigninConnectionString {
"connectionString"?: string;
"description"?: string;
"onNlNostrConnectDefault"?: (event: NlSigninConnectionStringCustomEvent<void>) => void;
"titleLogin"?: string;
}
interface NlSigninOtp {
"description"?: string;
"descriptionOTP"?: string;
"onNlCheckLogin"?: (event: NlSigninOtpCustomEvent<string>) => void;
"onNlLoginOTPCode"?: (event: NlSigninOtpCustomEvent<string>) => void;
"onNlLoginOTPUser"?: (event: NlSigninOtpCustomEvent<string>) => void;
"titleLogin"?: string;
"titleLoginOTP"?: string;
}
interface NlSigninReadOnly {
"description"?: string;
"onNlCheckLogin"?: (event: NlSigninReadOnlyCustomEvent<string>) => void;
"onNlLoginReadOnly"?: (event: NlSigninReadOnlyCustomEvent<string>) => void;
"titleLogin"?: string;
}
interface NlSignup {
"bunkers"?: string;
"description"?: string;
"onFetchHandler"?: (event: NlSignupCustomEvent<boolean>) => void;
"onNlCheckSignup"?: (event: NlSignupCustomEvent<string>) => void;
"onNlSignup"?: (event: NlSignupCustomEvent<string>) => void;
"titleSignup"?: string;
}
interface NlWelcome {
"description"?: string;
"titleWelcome"?: string;
}
interface NlWelcomeSignin {
"authMethods"?: AuthMethod[];
"hasExtension"?: boolean;
"hasOTP"?: boolean;
"onNlLoginExtension"?: (event: NlWelcomeSigninCustomEvent<void>) => void;
"titleWelcome"?: string;
}
interface NlWelcomeSignup {
"description"?: string;
"titleWelcome"?: string;
}
interface IntrinsicElements {
"button-base": ButtonBase;
"nl-auth": NlAuth;
"nl-banner": NlBanner;
"nl-button": NlButton;
"nl-change-account": NlChangeAccount;
"nl-confirm-logout": NlConfirmLogout;
"nl-connect": NlConnect;
"nl-dialog": NlDialog;
"nl-iframe": NlIframe;
"nl-import-flow": NlImportFlow;
"nl-info": NlInfo;
"nl-info-extension": NlInfoExtension;
"nl-loading": NlLoading;
"nl-local-signup": NlLocalSignup;
"nl-login-status": NlLoginStatus;
"nl-otp-migrate": NlOtpMigrate;
"nl-previously-logged": NlPreviouslyLogged;
"nl-select": NlSelect;
"nl-signin": NlSignin;
"nl-signin-bunker-url": NlSigninBunkerUrl;
"nl-signin-connection-string": NlSigninConnectionString;
"nl-signin-otp": NlSigninOtp;
"nl-signin-read-only": NlSigninReadOnly;
"nl-signup": NlSignup;
"nl-welcome": NlWelcome;
"nl-welcome-signin": NlWelcomeSignin;
"nl-welcome-signup": NlWelcomeSignup;
}
}
export { LocalJSX as JSX };
declare module "@stencil/core" {
export namespace JSX {
interface IntrinsicElements {
"button-base": LocalJSX.ButtonBase & JSXBase.HTMLAttributes<HTMLButtonBaseElement>;
"nl-auth": LocalJSX.NlAuth & JSXBase.HTMLAttributes<HTMLNlAuthElement>;
"nl-banner": LocalJSX.NlBanner & JSXBase.HTMLAttributes<HTMLNlBannerElement>;
"nl-button": LocalJSX.NlButton & JSXBase.HTMLAttributes<HTMLNlButtonElement>;
"nl-change-account": LocalJSX.NlChangeAccount & JSXBase.HTMLAttributes<HTMLNlChangeAccountElement>;
"nl-confirm-logout": LocalJSX.NlConfirmLogout & JSXBase.HTMLAttributes<HTMLNlConfirmLogoutElement>;
"nl-connect": LocalJSX.NlConnect & JSXBase.HTMLAttributes<HTMLNlConnectElement>;
"nl-dialog": LocalJSX.NlDialog & JSXBase.HTMLAttributes<HTMLNlDialogElement>;
"nl-iframe": LocalJSX.NlIframe & JSXBase.HTMLAttributes<HTMLNlIframeElement>;
"nl-import-flow": LocalJSX.NlImportFlow & JSXBase.HTMLAttributes<HTMLNlImportFlowElement>;
"nl-info": LocalJSX.NlInfo & JSXBase.HTMLAttributes<HTMLNlInfoElement>;
"nl-info-extension": LocalJSX.NlInfoExtension & JSXBase.HTMLAttributes<HTMLNlInfoExtensionElement>;
"nl-loading": LocalJSX.NlLoading & JSXBase.HTMLAttributes<HTMLNlLoadingElement>;
"nl-local-signup": LocalJSX.NlLocalSignup & JSXBase.HTMLAttributes<HTMLNlLocalSignupElement>;
"nl-login-status": LocalJSX.NlLoginStatus & JSXBase.HTMLAttributes<HTMLNlLoginStatusElement>;
"nl-otp-migrate": LocalJSX.NlOtpMigrate & JSXBase.HTMLAttributes<HTMLNlOtpMigrateElement>;
"nl-previously-logged": LocalJSX.NlPreviouslyLogged & JSXBase.HTMLAttributes<HTMLNlPreviouslyLoggedElement>;
"nl-select": LocalJSX.NlSelect & JSXBase.HTMLAttributes<HTMLNlSelectElement>;
"nl-signin": LocalJSX.NlSignin & JSXBase.HTMLAttributes<HTMLNlSigninElement>;
"nl-signin-bunker-url": LocalJSX.NlSigninBunkerUrl & JSXBase.HTMLAttributes<HTMLNlSigninBunkerUrlElement>;
"nl-signin-connection-string": LocalJSX.NlSigninConnectionString & JSXBase.HTMLAttributes<HTMLNlSigninConnectionStringElement>;
"nl-signin-otp": LocalJSX.NlSigninOtp & JSXBase.HTMLAttributes<HTMLNlSigninOtpElement>;
"nl-signin-read-only": LocalJSX.NlSigninReadOnly & JSXBase.HTMLAttributes<HTMLNlSigninReadOnlyElement>;
"nl-signup": LocalJSX.NlSignup & JSXBase.HTMLAttributes<HTMLNlSignupElement>;
"nl-welcome": LocalJSX.NlWelcome & JSXBase.HTMLAttributes<HTMLNlWelcomeElement>;
"nl-welcome-signin": LocalJSX.NlWelcomeSignin & JSXBase.HTMLAttributes<HTMLNlWelcomeSigninElement>;
"nl-welcome-signup": LocalJSX.NlWelcomeSignup & JSXBase.HTMLAttributes<HTMLNlWelcomeSignupElement>;
}
}
}

View File

@@ -0,0 +1,27 @@
:host {
display: block;
}
.animate-spin-loading {
background: var(--qa-dark-color);
animation: spin2 1s linear infinite;
}
@keyframes spin2 {
to {
transform: rotate(360deg);
}
}
.active {
animation: blink 0.7s infinite alternate;
}
@keyframes blink {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@@ -0,0 +1,41 @@
import { Component, h, Prop, Element } from '@stencil/core';
import { NlTheme } from '@/types';
import { IButton } from '@/types/button';
@Component({
tag: 'button-base',
styleUrl: 'button-base.css',
shadow: false,
})
export class ButtonBase implements IButton {
@Element() element: HTMLElement;
@Prop({ mutable: true }) theme: NlTheme = 'default';
@Prop({ mutable: true }) darkMode: boolean = false;
@Prop() titleBtn = 'Open modal';
@Prop() disabled = false;
componentDidRender() {
const svgElement = this.element.querySelector('svg');
if (svgElement) {
svgElement.classList.add('flex-shrink-0', 'w-4', 'h-4', 'block');
svgElement.removeAttribute('style'); // hack frieze svg
}
}
render() {
return (
<div class={`theme-${this.theme}`}>
<div class="animate-spin-loading active"></div>
<button
disabled={this.disabled}
type="button"
class="nl-button py-2.5 px-3 w-full inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg disabled:opacity-50 disabled:pointer-events-none dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600"
>
<slot name="icon-start" />
{this.titleBtn}
</button>
</div>
);
}
}

View File

@@ -0,0 +1,61 @@
# button-base
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ---------- | ----------- | ----------- | -------------------------------------------------------------------- | -------------- |
| `darkMode` | `dark-mode` | | `boolean` | `false` |
| `disabled` | `disabled` | | `boolean` | `false` |
| `theme` | `theme` | | `"crab" \| "default" \| "laan" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
| `titleBtn` | `title-btn` | | `string` | `'Open modal'` |
## Dependencies
### Used by
- [nl-banner](../nl-banner)
- [nl-button](../nl-button)
- [nl-confirm-logout](../nl-confirm-logout)
- [nl-connect](../nl-connect)
- [nl-import-flow](../nl-import-flow)
- [nl-loading](../nl-loading)
- [nl-local-signup](../nl-local-signup)
- [nl-otp-migrate](../nl-otp-migrate)
- [nl-signin](../nl-signin)
- [nl-signin-bunker-url](../nl-signin-bunker-url)
- [nl-signin-otp](../nl-signin-otp)
- [nl-signin-read-only](../nl-signin-read-only)
- [nl-signup](../nl-signup)
- [nl-welcome](../nl-welcome)
- [nl-welcome-signin](../nl-welcome-signin)
- [nl-welcome-signup](../nl-welcome-signup)
### Graph
```mermaid
graph TD;
nl-banner --> button-base
nl-button --> button-base
nl-confirm-logout --> button-base
nl-connect --> button-base
nl-import-flow --> button-base
nl-loading --> button-base
nl-local-signup --> button-base
nl-otp-migrate --> button-base
nl-signin --> button-base
nl-signin-bunker-url --> button-base
nl-signin-otp --> button-base
nl-signin-read-only --> button-base
nl-signup --> button-base
nl-welcome --> button-base
nl-welcome-signin --> button-base
nl-welcome-signup --> button-base
style button-base fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,332 @@
import { Component, Event, EventEmitter, Fragment, h, Prop, Watch } from '@stencil/core';
import { AuthMethod, ConnectionString, CURRENT_MODULE, Info, NlTheme, RecentType } from '@/types';
import { state } from '@/store';
@Component({
tag: 'nl-auth',
styleUrl: 'nl-auth.css',
shadow: true,
})
export class NlAuth {
@Prop({ mutable: true }) theme: NlTheme = 'default';
@Prop() bunkers: string = '';
@Prop() startScreen: string = CURRENT_MODULE.WELCOME;
@Prop() authMethods: AuthMethod[] = [];
@Prop() hasExtension: boolean = false;
@Prop() hasOTP: boolean = false;
@Prop() isLoading: boolean = false;
@Prop() isLoadingExtension: boolean = false;
@Prop() isOTP: boolean = false;
@Prop() authUrl: string = '';
@Prop() iframeUrl: string = '';
@Prop() error: string = '';
@Prop() localSignup: boolean = false;
@Prop() signupNjump: boolean = false;
@Prop() njumpIframe: string = '';
@Prop({ mutable: true }) accounts: Info[] = [];
@Prop({ mutable: true }) recents: RecentType[] = [];
@Prop({ mutable: true }) darkMode: boolean = false;
@Prop() welcomeTitle: string = '';
@Prop() welcomeDescription: string = '';
@Prop() connectionString: string = '';
@Prop() connectionStringServices: ConnectionString[] = [];
@Event() nlCloseModal: EventEmitter;
@Event() nlChangeDarkMode: EventEmitter<boolean>;
@Event() nlNostrConnectDefaultCancel: EventEmitter<void>;
prevPath: string = '';
@Watch('isLoading')
watchLoadingHandler(newValue: boolean) {
state.isLoading = newValue;
}
@Watch('isLoadingExtension')
watchLoadingExtensionHandler(newValue: boolean) {
state.isLoadingExtension = newValue;
}
@Watch('isOTP')
watchOTPHandler(newValue: boolean) {
state.isOTP = newValue;
}
@Watch('authUrl')
watchAuthUrlHandler(newValue: string) {
state.authUrl = newValue;
}
@Watch('iframeUrl')
watchIframeUrlHandler(newValue: string) {
state.iframeUrl = newValue;
}
@Watch('njumpIframe')
watchNjumpIframeHandler(newValue: string) {
state.njumpIframe = newValue;
}
@Watch('error')
watchErrorHandler(newValue: string) {
state.error = newValue;
}
handleClose() {
this.nlCloseModal.emit();
}
handleChangeDarkMode() {
this.nlChangeDarkMode.emit(!this.darkMode);
}
componentWillLoad() {
// init state
state.path = [this.startScreen as CURRENT_MODULE];
state.error = '';
state.iframeUrl = '';
state.authUrl = '';
state.isLoading = false;
state.isLoadingExtension = false;
state.isOTP = false;
console.log('path', state.path);
}
handleClickToBack() {
state.path.pop();
state.path = [...state.path];
// reset
state.isLoading = false;
state.isLoadingExtension = false;
state.authUrl = '';
state.isOTP = false;
}
switchSignSignUpStrategy(str: CURRENT_MODULE) {
if (CURRENT_MODULE.LOCAL_SIGNUP === str) {
state.path = [CURRENT_MODULE.WELCOME, CURRENT_MODULE.WELCOME_SIGNUP, str];
return;
}
state.path = [CURRENT_MODULE.WELCOME, str];
}
render() {
const classWrapper = `w-full h-full fixed top-0 start-0 z-[80] overflow-x-hidden overflow-y-auto flex items-center ${this.darkMode ? 'dark' : ''}`;
const currentModule = state.path.at(-1);
if (currentModule !== this.prevPath && this.prevPath === CURRENT_MODULE.CONNECTION_STRING) {
this.nlNostrConnectDefaultCancel.emit();
}
this.prevPath = currentModule;
const renderModule = () => {
if (state.isOTP) return <nl-signin-otp />;
// @ts-ignore
// const t: CURRENT_MODULE = 'import' // lastValuePath
switch (currentModule) {
case CURRENT_MODULE.WELCOME:
return <nl-welcome titleWelcome={this.welcomeTitle || undefined} description={this.welcomeDescription || undefined} />;
case CURRENT_MODULE.LOGIN:
return <nl-signin />;
case CURRENT_MODULE.SIGNUP:
return <nl-signup bunkers={this.bunkers} />;
case CURRENT_MODULE.LOCAL_SIGNUP:
return <nl-local-signup signupNjump={this.signupNjump} />;
case CURRENT_MODULE.CONFIRM_LOGOUT:
return <nl-confirm-logout />;
case CURRENT_MODULE.IMPORT_FLOW:
return <nl-import-flow services={this.connectionStringServices} />;
case CURRENT_MODULE.IMPORT_OTP:
return <nl-otp-migrate services={this.connectionStringServices} />;
case CURRENT_MODULE.INFO:
return <nl-info theme={this.theme} darkMode={this.darkMode} />;
case CURRENT_MODULE.EXTENSION:
return <nl-info-extension />;
case CURRENT_MODULE.LOGIN_READ_ONLY:
return <nl-signin-read-only />;
case CURRENT_MODULE.LOGIN_BUNKER_URL:
return <nl-signin-bunker-url />;
case CURRENT_MODULE.LOGIN_OTP:
return <nl-signin-otp />;
case CURRENT_MODULE.WELCOME_LOGIN:
return <nl-welcome-signin hasOTP={this.hasOTP} authMethods={this.authMethods} hasExtension={this.hasExtension} />;
case CURRENT_MODULE.WELCOME_SIGNUP:
return <nl-welcome-signup />;
case CURRENT_MODULE.CONNECTION_STRING:
return <nl-signin-connection-string connectionString={this.connectionString} />;
case CURRENT_MODULE.CONNECT:
return <nl-connect connectionStringServices={this.connectionStringServices} authMethods={this.authMethods} />;
case CURRENT_MODULE.PREVIOUSLY_LOGGED:
return <nl-previously-logged accounts={this.accounts} recents={this.recents} />;
case CURRENT_MODULE.IFRAME:
return <nl-iframe iframeUrl={this.authUrl} />;
default:
return <nl-welcome />;
}
};
const showLogin =
state.isOTP ||
(currentModule !== CURRENT_MODULE.INFO &&
currentModule !== CURRENT_MODULE.CONFIRM_LOGOUT &&
currentModule !== CURRENT_MODULE.IMPORT_FLOW &&
currentModule !== CURRENT_MODULE.WELCOME &&
currentModule !== CURRENT_MODULE.EXTENSION &&
currentModule !== CURRENT_MODULE.IFRAME &&
currentModule !== CURRENT_MODULE.PREVIOUSLY_LOGGED);
const showSignup =
currentModule !== CURRENT_MODULE.IFRAME &&
(!this.authMethods.length || (!this.localSignup && this.authMethods.includes('connect')) || (this.localSignup && this.authMethods.includes('local')));
return (
<div class={`theme-${this.theme}`} dir="ltr">
<div class={classWrapper}>
<div onClick={() => this.handleClose()} class="absolute top-0 left-0 w-full h-full bg-gray-500 bg-opacity-75 z-[80]" />
<div class="nl-bg relative z-[81] w-full flex flex-col rounded-xl sm:max-w-lg sm:w-full sm:mx-auto">
<div class={`flex justify-between items-center py-3 px-4`}>
<div class="flex gap-2 items-center">
<svg class="w-7 h-7" width="225" height="224" viewBox="0 0 225 224" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="224.047" height="224" rx="64" fill={this.theme === 'laan' ? (this.darkMode ? 'white' : 'black') : '#6951FA'} />
<path
d="M162.441 135.941V88.0593C170.359 85.1674 176 77.5348 176 68.6696C176 57.2919 166.708 48 155.33 48C143.953 48 134.661 57.2444 134.661 68.6696C134.661 77.5822 140.302 85.1674 148.219 88.0593V135.941C147.698 136.13 147.176 136.367 146.655 136.604L87.3956 77.3452C88.6282 74.6904 89.2919 71.7511 89.2919 68.6696C89.2919 57.2444 80.0474 48 68.6696 48C57.2919 48 48 57.2444 48 68.6696C48 77.5822 53.6415 85.1674 61.5585 88.0593V135.941C53.6415 138.833 48 146.465 48 155.33C48 166.708 57.2444 176 68.6696 176C80.0948 176 89.3393 166.708 89.3393 155.33C89.3393 146.418 83.6978 138.833 75.7807 135.941V88.0593C76.3022 87.8696 76.8237 87.6326 77.3452 87.3956L136.604 146.655C135.372 149.31 134.708 152.249 134.708 155.33C134.708 166.708 143.953 176 155.378 176C166.803 176 176.047 166.708 176.047 155.33C176.047 146.418 170.406 138.833 162.489 135.941H162.441Z"
fill={this.theme === 'laan' ? (this.darkMode ? 'black' : 'white') : 'white'}
/>
</svg>
<p class="font-bold nl-logo text-base">
Nostr <span class="font-light">Login</span>
</p>
</div>
<div class="flex gap-1">
<button
onClick={() => this.handleChangeDarkMode()}
type="button"
class="nl-action-button flex justify-center items-center w-7 h-7 text-sm font-semibold rounded-full border border-transparent"
>
<span class="sr-only">Change theme</span>
{this.darkMode ? (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-5 h-5">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-5 h-5">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"
/>
</svg>
)}
</button>
{!state.isLoading && (
<button
onClick={() => (state.path = [...state.path, CURRENT_MODULE.INFO])}
type="button"
class="nl-action-button flex justify-center items-center w-7 h-7 text-sm font-semibold rounded-full border border-transparent"
>
<span class="sr-only">Info</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-5 h-5">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</button>
)}
<button
onClick={() => this.handleClose()}
type="button"
class="nl-action-button flex justify-center items-center w-7 h-7 text-sm font-semibold rounded-full border border-transparent"
>
<span class="sr-only">Close</span>
<svg
class="flex-shrink-0 w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
</div>
{state.path.length > 1 && !state.isLoading && (
<div class="p-4">
<button
onClick={() => this.handleClickToBack()}
type="button"
class="nl-action-button flex justify-center items-center w-7 h-7 text-sm font-semibold rounded-full border border-transparent dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600"
data-hs-overlay="#hs-vertically-centered-modal"
>
<span class="sr-only">Back</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
</button>
</div>
)}
{state.isLoading || state.authUrl ? (
<nl-loading path={currentModule} />
) : (
<Fragment>
{renderModule()}
{showLogin && (
<Fragment>
{currentModule === CURRENT_MODULE.WELCOME_SIGNUP || currentModule === CURRENT_MODULE.SIGNUP || currentModule === CURRENT_MODULE.LOCAL_SIGNUP ? (
<div class="p-4 overflow-y-auto">
<p class="nl-footer font-light text-center text-sm pt-3 max-w-96 mx-auto">
If you already have a profile please{' '}
<span onClick={() => this.switchSignSignUpStrategy(CURRENT_MODULE.WELCOME_LOGIN)} class="cursor-pointer text-blue-400">
log in
</span>
.
</p>
</div>
) : (
showSignup && (
<div class="p-4 overflow-y-auto">
<p class="nl-footer font-light text-center text-sm pt-3 max-w-96 mx-auto">
If you don't have a profile please{' '}
<span
onClick={() =>
this.localSignup ? this.switchSignSignUpStrategy(CURRENT_MODULE.LOCAL_SIGNUP) : this.switchSignSignUpStrategy(CURRENT_MODULE.WELCOME_SIGNUP)
}
class="cursor-pointer text-blue-400"
>
sign up
</span>
.
</p>
</div>
)
)}
</Fragment>
)}
</Fragment>
)}
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,112 @@
# nl-auth
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| -------------------------- | ---------------------- | ----------- | -------------------------------------------------------------------- | ------------------------ |
| `accounts` | -- | | `Info[]` | `[]` |
| `authMethods` | -- | | `AuthMethod[]` | `[]` |
| `authUrl` | `auth-url` | | `string` | `''` |
| `bunkers` | `bunkers` | | `string` | `''` |
| `connectionString` | `connection-string` | | `string` | `''` |
| `connectionStringServices` | -- | | `ConnectionString[]` | `[]` |
| `darkMode` | `dark-mode` | | `boolean` | `false` |
| `error` | `error` | | `string` | `''` |
| `hasExtension` | `has-extension` | | `boolean` | `false` |
| `hasOTP` | `has-o-t-p` | | `boolean` | `false` |
| `iframeUrl` | `iframe-url` | | `string` | `''` |
| `isLoading` | `is-loading` | | `boolean` | `false` |
| `isLoadingExtension` | `is-loading-extension` | | `boolean` | `false` |
| `isOTP` | `is-o-t-p` | | `boolean` | `false` |
| `localSignup` | `local-signup` | | `boolean` | `false` |
| `njumpIframe` | `njump-iframe` | | `string` | `''` |
| `recents` | -- | | `RecentType[]` | `[]` |
| `signupNjump` | `signup-njump` | | `boolean` | `false` |
| `startScreen` | `start-screen` | | `string` | `CURRENT_MODULE.WELCOME` |
| `theme` | `theme` | | `"crab" \| "default" \| "laan" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
| `welcomeDescription` | `welcome-description` | | `string` | `''` |
| `welcomeTitle` | `welcome-title` | | `string` | `''` |
## Events
| Event | Description | Type |
| ----------------------------- | ----------- | ---------------------- |
| `nlChangeDarkMode` | | `CustomEvent<boolean>` |
| `nlCloseModal` | | `CustomEvent<any>` |
| `nlNostrConnectDefaultCancel` | | `CustomEvent<void>` |
## Dependencies
### Depends on
- [nl-signin-otp](../nl-signin-otp)
- [nl-welcome](../nl-welcome)
- [nl-signin](../nl-signin)
- [nl-signup](../nl-signup)
- [nl-local-signup](../nl-local-signup)
- [nl-confirm-logout](../nl-confirm-logout)
- [nl-import-flow](../nl-import-flow)
- [nl-otp-migrate](../nl-otp-migrate)
- [nl-info](../nl-info)
- [nl-info-extension](../nl-info-extension)
- [nl-signin-read-only](../nl-signin-read-only)
- [nl-signin-bunker-url](../nl-signin-bunker-url)
- [nl-welcome-signin](../nl-welcome-signin)
- [nl-welcome-signup](../nl-welcome-signup)
- [nl-signin-connection-string](../nl-signin-connection-string)
- [nl-connect](../nl-connect)
- [nl-previously-logged](../nl-previously-logged)
- [nl-iframe](../nl-iframe)
- [nl-loading](../nl-loading)
### Graph
```mermaid
graph TD;
nl-auth --> nl-signin-otp
nl-auth --> nl-welcome
nl-auth --> nl-signin
nl-auth --> nl-signup
nl-auth --> nl-local-signup
nl-auth --> nl-confirm-logout
nl-auth --> nl-import-flow
nl-auth --> nl-otp-migrate
nl-auth --> nl-info
nl-auth --> nl-info-extension
nl-auth --> nl-signin-read-only
nl-auth --> nl-signin-bunker-url
nl-auth --> nl-welcome-signin
nl-auth --> nl-welcome-signup
nl-auth --> nl-signin-connection-string
nl-auth --> nl-connect
nl-auth --> nl-previously-logged
nl-auth --> nl-iframe
nl-auth --> nl-loading
nl-signin-otp --> button-base
nl-welcome --> button-base
nl-signin --> button-base
nl-signup --> nl-select
nl-signup --> button-base
nl-local-signup --> button-base
nl-confirm-logout --> button-base
nl-import-flow --> button-base
nl-import-flow --> nl-select
nl-otp-migrate --> nl-select
nl-otp-migrate --> button-base
nl-signin-read-only --> button-base
nl-signin-bunker-url --> button-base
nl-welcome-signin --> button-base
nl-welcome-signup --> button-base
nl-connect --> button-base
nl-previously-logged --> nl-login-status
nl-loading --> button-base
style nl-auth fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,4 @@
:host {
display: block;
}

View File

@@ -0,0 +1,345 @@
import { Component, Event, EventEmitter, Fragment, h, Prop, State, Watch } from '@stencil/core';
import { BannerNotify, BannerNotifyMode, Info, METHOD_MODULE, NlTheme } from '@/types';
@Component({
tag: 'nl-banner',
styleUrl: 'nl-banner.css',
shadow: true,
})
export class NlBanner {
@Prop({ mutable: true }) theme: NlTheme = 'default';
@Prop({ mutable: true }) darkMode: boolean = false;
@Prop({ mutable: true }) hiddenMode: boolean = false;
@Prop() titleBanner: string = '';
@Prop({ mutable: true }) isOpen: boolean = false;
@Prop() isLoading: boolean = false;
@Prop() notify: BannerNotify | null = null;
@Prop() userInfo: Info | null = null;
@Prop({ mutable: true }) accounts: Info[] = [];
@State() isUserImgError = false;
@State() domain: string = '';
@State() mode: BannerNotifyMode = '';
@State() url: string = '';
@State() isOpenConfirm: boolean = false;
@Event() handleNotifyConfirmBanner: EventEmitter<string>;
@Event() handleNotifyConfirmBannerIframe: EventEmitter<string>;
@Event() handleLoginBanner: EventEmitter<string>;
@Event() handleLogoutBanner: EventEmitter<string>;
@Event() handleOpenWelcomeModal: EventEmitter<string>;
@Event() handleConfirmLogout: EventEmitter<string>;
@Event() handleImportModal: EventEmitter<string>;
@Watch('notify')
watchNotifyHandler(notify: BannerNotify) {
this.isOpen = true;
this.isOpenConfirm = true;
this.domain = this.userInfo?.domain || this.userInfo?.nip05?.split('@')?.[1] || '';
this.mode = notify.mode;
this.url = notify.url;
if (!this.mode) {
this.isOpenConfirm = false;
this.isOpen = false;
}
}
handleOpen() {
if (this.userInfo) {
this.isOpen = true;
} else {
this.handleOpenWelcomeModal.emit();
}
}
handleClose() {
this.isOpen = false;
}
handleLogin() {
this.handleLoginBanner.emit(METHOD_MODULE.LOGIN);
this.handleClose();
}
handleSignup() {
this.handleLoginBanner.emit(METHOD_MODULE.SIGNUP);
this.handleClose();
}
handleImport() {
this.handleImportModal.emit();
this.handleClose();
}
handleLogout() {
const isBackupKey = localStorage.getItem('backupKey');
if (isBackupKey) {
this.handleLogoutBanner.emit(METHOD_MODULE.LOGOUT);
this.handleClose();
localStorage.removeItem('backupKey');
return;
}
if (this.userInfo.authMethod === 'local') {
this.handleConfirmLogout.emit();
} else {
this.handleLogoutBanner.emit(METHOD_MODULE.LOGOUT);
}
this.handleClose();
}
handleConfirm() {
switch (this.mode) {
case 'authUrl':
this.handleNotifyConfirmBanner.emit(this.url);
break;
case 'iframeAuthUrl':
this.handleNotifyConfirmBannerIframe.emit(this.url);
break;
}
this.handleClose();
}
render() {
const isShowImg = Boolean(this.userInfo?.picture);
const userName = this.userInfo?.name || this.userInfo?.nip05?.split('@')?.[0] || this.userInfo?.pubkey || '';
const isShowUserName = Boolean(userName);
const isTemporary = this.userInfo && this.userInfo.authMethod === 'local';
const isBackupKey = localStorage.getItem('backupKey');
const defaultUserAvatar = (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
);
const content = (
<Fragment>
<div class="block w-[48px] h-[46px] relative z-10">
<div onClick={() => this.handleOpen()} class={`flex w-52 h-[46px] items-center pl-[11px]`}>
<span
class={`${this.isLoading ? 'w-5 h-5 border-[2px] mr-3.5 ml-[2px] opacity-1' : 'w-0 h-0 border-[0px] mr-0 opacity-0 ml-0'} animate-spin transition-all duration-300 ease-in-out inline-block border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full`}
role="status"
aria-label="loading"
></span>
{this.userInfo ? (
<div class={`uppercase font-bold w-6 h-6 mr-2 rounded-full border border-gray-200 flex justify-center items-center`}>
{isShowImg ? (
this.isUserImgError ? (
defaultUserAvatar
) : (
<img class="w-full rounded-full" src={this.userInfo.picture} alt="Logo" onError={() => (this.isUserImgError = true)} />
)
) : isShowUserName ? (
userName[0]
) : (
defaultUserAvatar
)}
</div>
) : (
<div class="flex justify-center items-center">
<svg class="w-6 h-6" width="225" height="224" viewBox="0 0 225 224" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="224.047" height="224" rx="64" fill={this.theme === 'laan' ? (this.darkMode ? 'white' : 'black') : '#6951FA'} />
<path
d="M162.441 135.941V88.0593C170.359 85.1674 176 77.5348 176 68.6696C176 57.2919 166.708 48 155.33 48C143.953 48 134.661 57.2444 134.661 68.6696C134.661 77.5822 140.302 85.1674 148.219 88.0593V135.941C147.698 136.13 147.176 136.367 146.655 136.604L87.3956 77.3452C88.6282 74.6904 89.2919 71.7511 89.2919 68.6696C89.2919 57.2444 80.0474 48 68.6696 48C57.2919 48 48 57.2444 48 68.6696C48 77.5822 53.6415 85.1674 61.5585 88.0593V135.941C53.6415 138.833 48 146.465 48 155.33C48 166.708 57.2444 176 68.6696 176C80.0948 176 89.3393 166.708 89.3393 155.33C89.3393 146.418 83.6978 138.833 75.7807 135.941V88.0593C76.3022 87.8696 76.8237 87.6326 77.3452 87.3956L136.604 146.655C135.372 149.31 134.708 152.249 134.708 155.33C134.708 166.708 143.953 176 155.378 176C166.803 176 176.047 166.708 176.047 155.33C176.047 146.418 170.406 138.833 162.489 135.941H162.441Z"
fill={this.theme === 'laan' ? (this.darkMode ? 'black' : 'white') : 'white'}
/>
</svg>
{this.isOpen && (
<span class="px-2">
<b>Nostr</b> Login
</span>
)}
</div>
)}
{this.isOpen && isShowUserName && <div class="show-slow truncate w-16 text-xs">{userName}</div>}
{this.isOpen && isShowUserName && <nl-login-status info={this.userInfo} />}
</div>
</div>
<button
onClick={() => this.handleClose()}
type="button"
class={`${this.isOpen ? 'z-20' : 'z-0'} nl-action-button absolute right-2 top-2 z-0 show-slow grid place-items-center w-7 h-7 text-sm font-semibold rounded-full border border-transparent`}
>
<span class="sr-only">Close</span>
<svg
class="flex-shrink-0 w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
<div class="p-3 show-slow">
{this.isOpenConfirm ? (
<div>
<div class="w-8 h-8 p-1/2 rounded-full border border-gray-200 bg-white mb-2 mt-2 show-slow m-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#5a68ff" class="w-full">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</div>
<p class="mb-2 text-center max-w-40 min-w-40 mx-auto">
{this.mode === 'timeout' ? 'Keys not responding, check your key storage app' : `Confirmation required at ${this.domain}`}
</p>
{this.mode === 'timeout' ? (
<a
onClick={() => this.handleClose()}
href={`https://${this.domain}`}
target="_blank"
class="nl-button text-nowrap py-2.5 px-3 w-full inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg disabled:opacity-50 disabled:pointer-events-none dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600"
>
Go to {this.domain}
</a>
) : this.mode === 'rebind' ? (
<iframe src={this.url} width={'180'} height={'80'} frameBorder={'0'}></iframe>
) : (
<button-base onClick={() => this.handleConfirm()} titleBtn="Confirm" />
)}
</div>
) : (
<div>
<div>
{this.titleBanner && <p class="mb-2 text-center show-slow max-w-40 min-w-40 mx-auto">{this.titleBanner}</p>}
{isTemporary && (
<Fragment>
{!isBackupKey && <p class="mb-2 text-center show-slow text-red-400 max-w-40 min-w-40 mx-auto">Your profile may be lost if you close this tab</p>}
<div class="mb-2">
<button-base onClick={() => this.handleImport()} theme="lemonade" titleBtn="Back up profile" />
</div>
</Fragment>
)}
<div class="mb-2">
<nl-change-account currentAccount={this.userInfo} accounts={this.accounts} />
</div>
{/* {Boolean(this.listNotifies.length) && (
<div
onClick={() => this.handleRetryConfirm()}
class="show-slow border border-yellow-600 text-yellow-600 bg-yellow-100 p-2 rounded-lg mb-2 cursor-pointer w-44 text-xs m-auto text-center"
>
Requests: {this.listNotifies.length}
</div>
)} */}
{!this.userInfo ? (
<div>
<button-base onClick={() => this.handleLogin()} titleBtn="Log in">
<svg
style={{ display: 'none' }}
slot="icon-start"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="flex-shrink-0 w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"
/>
</svg>
</button-base>
<button-base onClick={() => this.handleSignup()} titleBtn="Sign up">
<svg
style={{ display: 'none' }}
slot="icon-start"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="flex-shrink-0 w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
/>
</svg>
</button-base>
</div>
) : (
<button-base onClick={() => this.handleLogout()} titleBtn="Log out" />
)}
</div>
</div>
)}
</div>
</Fragment>
);
// https://gist.github.com/Haprog/848fc451c25da00b540e6d34c301e96a
function deepQuerySelectorAll(selector: string, root?: Element) {
root = root || document.body;
const results = Array.from(root.querySelectorAll(selector));
const pushNestedResults = function (root) {
deepQuerySelectorAll(selector, root).forEach(elem => {
if (!results.includes(elem)) {
results.push(elem);
}
});
};
if (root.shadowRoot) {
pushNestedResults(root.shadowRoot);
}
for (const elem of Array.from(root.querySelectorAll('*'))) {
if (elem.shadowRoot) {
pushNestedResults(elem.shadowRoot);
}
}
return results;
}
const dialogs = deepQuerySelectorAll('dialog');
const needDialog = !!dialogs.find(d => (d as HTMLDialogElement).open && !d.classList.contains('nl-banner-dialog'));
return (
<div class={`theme-${this.theme} ${!this.isOpen && this.hiddenMode ? 'hidden' : ''}`}>
<div class={this.darkMode && 'dark'} dir="ltr">
{this.isOpenConfirm && needDialog ? (
<nl-dialog>
<div
class={`nl-banner ${this.isOpen ? 'w-52 h-auto right-2 rounded-r-lg isOpen ' : 'rounded-r-none hover:rounded-r-lg cursor-pointer'} z-50 w-12 h-12 fixed top-52 right-0 inline-block gap-x-2 text-sm font-medium rounded-lg hover:right-2 transition-all duration-300 ease-in-out`}
>
{content}
</div>
</nl-dialog>
) : (
<div
class={`nl-banner ${this.isOpen ? 'w-52 h-auto right-2 rounded-r-lg isOpen' : 'rounded-r-none hover:rounded-r-lg cursor-pointer'} z-50 w-12 h-12 fixed top-52 right-0 inline-block gap-x-2 text-sm font-medium rounded-lg hover:right-2 transition-all duration-300 ease-in-out`}
>
{content}
</div>
)}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,56 @@
# nl-banner
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | -------------- | ----------- | -------------------------------------------------------------------- | ----------- |
| `accounts` | -- | | `Info[]` | `[]` |
| `darkMode` | `dark-mode` | | `boolean` | `false` |
| `hiddenMode` | `hidden-mode` | | `boolean` | `false` |
| `isLoading` | `is-loading` | | `boolean` | `false` |
| `isOpen` | `is-open` | | `boolean` | `false` |
| `notify` | -- | | `BannerNotify` | `null` |
| `theme` | `theme` | | `"crab" \| "default" \| "laan" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
| `titleBanner` | `title-banner` | | `string` | `''` |
| `userInfo` | -- | | `Info` | `null` |
## Events
| Event | Description | Type |
| --------------------------------- | ----------- | --------------------- |
| `handleConfirmLogout` | | `CustomEvent<string>` |
| `handleImportModal` | | `CustomEvent<string>` |
| `handleLoginBanner` | | `CustomEvent<string>` |
| `handleLogoutBanner` | | `CustomEvent<string>` |
| `handleNotifyConfirmBanner` | | `CustomEvent<string>` |
| `handleNotifyConfirmBannerIframe` | | `CustomEvent<string>` |
| `handleOpenWelcomeModal` | | `CustomEvent<string>` |
## Dependencies
### Depends on
- [nl-login-status](../nl-login-status)
- [button-base](../button-base)
- [nl-change-account](../nl-change-account)
- [nl-dialog](../nl-dialog)
### Graph
```mermaid
graph TD;
nl-banner --> nl-login-status
nl-banner --> button-base
nl-banner --> nl-change-account
nl-banner --> nl-dialog
nl-change-account --> nl-login-status
style nl-banner fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,18 @@
import { Component, h, Prop } from '@stencil/core';
import { NlTheme } from '@/types';
import { IButton } from '@/types/button';
@Component({
tag: 'nl-button',
shadow: true,
})
export class NlButton implements IButton {
@Prop() theme: NlTheme = 'default';
@Prop() darkMode: boolean = false;
@Prop() titleBtn = 'Open modal';
@Prop() disabled = false;
render() {
return <button-base theme={this.theme} darkMode={this.darkMode} titleBtn={this.titleBtn} disabled={this.disabled} />;
}
}

View File

@@ -0,0 +1,31 @@
# nl-button
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ---------- | ----------- | ----------- | -------------------------------------------------------------------- | -------------- |
| `darkMode` | `dark-mode` | | `boolean` | `false` |
| `disabled` | `disabled` | | `boolean` | `false` |
| `theme` | `theme` | | `"crab" \| "default" \| "laan" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
| `titleBtn` | `title-btn` | | `string` | `'Open modal'` |
## Dependencies
### Depends on
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-button --> button-base
style nl-button fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,154 @@
import { Component, h, Listen, Prop, State, Watch, Element, Event, EventEmitter } from '@stencil/core';
import { Info } from '@/types';
@Component({
tag: 'nl-change-account',
styleUrl: 'nl-change-account.css',
shadow: false,
})
export class NLChangeAccount {
@State() isOpen: boolean = false;
@State() options: Info[] = [];
@Prop() accounts: Info[] = [];
@Prop() currentAccount: Info = null;
@Element() element: HTMLElement;
@Event() handleOpenWelcomeModal: EventEmitter<string>;
@Event() handleSwitchAccount: EventEmitter<Info>;
buttonRef: HTMLButtonElement;
ulRef: HTMLUListElement;
wrapperRef: HTMLDivElement;
@Listen('click', { target: 'window' })
handleWindowClick() {
if (this.wrapperRef.querySelector('.listClass')) {
this.isOpen = false;
}
}
toggleDropdown() {
this.isOpen = !this.isOpen;
this.calculateDropdownPosition();
}
@State() mode: boolean = false;
@Prop() darkMode: boolean = false;
@State() themeState: 'default' | 'ocean' | 'lemonade' | 'purple' = 'default';
@Prop() theme: 'default' | 'ocean' | 'lemonade' | 'purple' = 'default';
@Watch('theme')
watchPropHandler(newValue: 'default' | 'ocean' | 'lemonade' | 'purple') {
this.themeState = newValue;
}
@Watch('darkMode')
watchModeHandler(newValue: boolean) {
this.mode = newValue;
}
@Watch('accounts')
watchAccountsHandler(newValue: Info[]) {
this.options = newValue;
}
connectedCallback() {
this.themeState = this.theme;
this.mode = this.darkMode;
}
calculateDropdownPosition() {
if (this.isOpen && this.buttonRef) {
const buttonRect = this.buttonRef.getBoundingClientRect();
this.ulRef.style.top = `${buttonRect.height}px`;
}
}
handleChange(el: Info) {
this.handleSwitchAccount.emit(el);
}
handleOpenModal() {
this.handleOpenWelcomeModal.emit();
}
render() {
const listClass = `${this.isOpen ? 'listClass flex flex-col gap-2' : 'hidden'} w-full nl-select-list absolute z-10 left-0 shadow-md rounded-lg p-2 mt-1 after:h-4 after:absolute after:-bottom-4 after:start-0 after:w-full before:h-4 before:absolute before:-top-4 before:start-0 before:w-full`;
const arrowClass = `${this.isOpen ? 'rotate-180' : 'rotate-0'} duration-300 flex-shrink-0 w-4 h-4 text-gray-500`;
const filteredOptions =
this.options && this.currentAccount ? this.options.filter(el => el.pubkey !== this.currentAccount.pubkey || el.authMethod !== this.currentAccount.authMethod) : [];
return (
<div class={`theme-${this.themeState}`}>
<div class="relative" ref={el => (this.wrapperRef = el)}>
<button
ref={el => (this.buttonRef = el)}
onClick={() => this.toggleDropdown()}
type="button"
class="nl-select peer py-3 px-4 flex items-center w-full justify-between border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
>
<span class="text-gray-500">Switch profile</span>
<svg
class={arrowClass}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
<ul ref={el => (this.ulRef = el)} class={listClass}>
{this.options &&
filteredOptions.map(el => {
const isShowImg = Boolean(el?.picture);
const userName = el.name || el.nip05 || el.pubkey;
const isShowUserName = Boolean(userName);
return (
<li onClick={() => this.handleChange(el)} class="nl-select-option flex cursor-pointer items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm">
<div class="uppercase font-bold w-full max-w-6 h-6 rounded-full border border-gray-400 flex justify-center items-center">
{isShowImg ? (
<img class="w-full rounded-full" src={el.picture} alt="Logo" />
) : isShowUserName ? (
userName[0]
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
)}
</div>
{/*<div class="truncate overflow-hidden w-full">{userName}</div>*/}
<div class="overflow-hidden flex flex-col w-full">
<div class="truncate overflow-hidden">{userName}</div>
<nl-login-status info={el} />
</div>
</li>
);
})}
<li class="first:pt-0 pt-2 border-t-[1px] first:border-none border-gray-300">
<div onClick={() => this.handleOpenModal()} class="nl-select-option flex cursor-pointer items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm">
<div class="uppercase font-bold w-6 h-6 rounded-full border border-gray-400 flex justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</div>
Add profile
</div>
</li>
</ul>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,44 @@
# nl-change-account
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ---------------- | ----------- | ----------- | ------------------------------------------------ | ----------- |
| `accounts` | -- | | `Info[]` | `[]` |
| `currentAccount` | -- | | `Info` | `null` |
| `darkMode` | `dark-mode` | | `boolean` | `false` |
| `theme` | `theme` | | `"default" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
## Events
| Event | Description | Type |
| ------------------------ | ----------- | --------------------- |
| `handleOpenWelcomeModal` | | `CustomEvent<string>` |
| `handleSwitchAccount` | | `CustomEvent<Info>` |
## Dependencies
### Used by
- [nl-banner](../nl-banner)
### Depends on
- [nl-login-status](../nl-login-status)
### Graph
```mermaid
graph TD;
nl-change-account --> nl-login-status
nl-banner --> nl-change-account
style nl-change-account fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,44 @@
import { Component, Event, EventEmitter, Prop, h } from '@stencil/core';
import { CURRENT_MODULE, METHOD_MODULE } from '@/types';
import { state } from '@/store';
@Component({
tag: 'nl-confirm-logout',
styleUrl: 'nl-confirm-logout.css',
shadow: false,
})
export class NlConfirmLogout {
@Prop() titleModal = "Delete keys?";
@Prop() description = "Your profile keys are stored in this browser tab and will be deleted if you log out, and your profile will be inaccessible.";
@Event() handleLogoutBanner: EventEmitter<string>;
@Event() handleBackUpModal: EventEmitter<string>;
@Event() nlCloseModal: EventEmitter;
handleLogout() {
this.handleLogoutBanner.emit(METHOD_MODULE.LOGOUT);
this.nlCloseModal.emit();
}
handleCancel() {
this.nlCloseModal.emit();
}
handleBackUp() {
state.path = [CURRENT_MODULE.IMPORT_FLOW];
}
render() {
return (
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-4xl">{this.titleModal}</h1>
<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">{this.description}</p>
<div class="mt-3 ml-auto mr-auto w-60 flex flex-col gap-2">
{/* <button-base onClick={() => this.handleCancel()} titleBtn="Cancel" /> */}
<button-base onClick={() => this.handleBackUp()} titleBtn="Backup keys" theme="lemonade" />
<button-base onClick={() => this.handleLogout()} theme="crab" titleBtn="Logout and delete keys" />
</div>
</div>
);
}
}

View File

@@ -0,0 +1,43 @@
# nl-confirm-logout
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | ------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `description` | `description` | | `string` | `"Your profile keys are stored in this browser tab and will be deleted if you log out, and your profile will be inaccessible."` |
| `titleModal` | `title-modal` | | `string` | `"Delete keys?"` |
## Events
| Event | Description | Type |
| -------------------- | ----------- | --------------------- |
| `handleBackUpModal` | | `CustomEvent<string>` |
| `handleLogoutBanner` | | `CustomEvent<string>` |
| `nlCloseModal` | | `CustomEvent<any>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-confirm-logout --> button-base
nl-auth --> nl-confirm-logout
style nl-confirm-logout fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,178 @@
import { Component, Event, EventEmitter, Fragment, h, Prop, State } from '@stencil/core';
import { AuthMethod, ConnectionString, CURRENT_MODULE } from '@/types';
import { state } from '@/store';
@Component({
tag: 'nl-connect',
styleUrl: 'nl-connect.css',
shadow: false,
})
export class NlConnect {
@Prop() titleWelcome = 'Connect to key store';
@Prop() authMethods: AuthMethod[] = [];
@Prop() hasOTP: boolean = false;
@Prop() connectionStringServices: ConnectionString[] = [];
@State() isOpenAdvancedLogin: boolean = false;
@Event() nlNostrConnect: EventEmitter<ConnectionString>;
handleChangeScreen(screen) {
state.path = [...state.path, screen];
}
handleOpenAdvanced() {
this.isOpenAdvancedLogin = !this.isOpenAdvancedLogin;
}
allowAuthMethod(m: AuthMethod) {
return !this.authMethods.length || this.authMethods.includes(m);
}
componentWillLoad() {}
handleOpenLink(e: Event, cs: ConnectionString) {
e.preventDefault();
this.nlNostrConnect.emit(cs);
}
handleConnectionString() {
this.handleChangeScreen(CURRENT_MODULE.CONNECTION_STRING)
}
render() {
const arrowClass = `${this.isOpenAdvancedLogin ? 'rotate-180' : 'rotate-0'} duration-300 flex-shrink-0 w-4 h-4 text-blue-500`;
return (
<Fragment>
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-3xl">{this.titleWelcome}</h1>
</div>
<div class="p-4">
{Boolean(this.connectionStringServices.length) && (
<div class="max-w-96 mx-auto pt-5">
<p class="nl-description font-medium text-sm pb-1.5">Select key store:</p>
<ul class="p-2 rounded-lg border border-gray-200 flex flex-col w-full gap-0.5">
{this.connectionStringServices.map(el => {
return (
<li>
<a
href={el.link}
target="_blank"
onClick={e => this.handleOpenLink(e, el)}
class="flex items-center gap-x-3.5 w-full hover:bg-gray-300 flex cursor-pointer items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm justify-between"
>
<div class="w-full max-w-7 h-7 flex relative">
<div class="uppercase font-bold w-full h-full rounded-full border border-gray-400 flex justify-center items-center">
{Boolean(el.img) ? (
<img class="w-full rounded-full" src={el.img} alt={el.name} />
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#9ca3af" class="w-4 h-4 block">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"
/>
</svg>
)}
</div>
</div>
<div class="overflow-hidden flex flex-col w-full">
<div class="nl-title truncate overflow-hidden">{el.name}</div>
</div>
</a>
</li>
);
})}
</ul>
</div>
)}
</div>
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
<div class="max-w-52 mx-auto pb-5">
{(this.allowAuthMethod('connect') || this.allowAuthMethod('readOnly')) && (
<div class="flex justify-center">
<div
onClick={() => this.handleOpenAdvanced()}
class="text-blue-500 mt-3 decoration-dashed cursor-pointer inline-flex gap-2 items-center pb-1 border-dashed border-b-[1px] border-blue-500 text-sm font-light"
>
Advanced
<svg
class={arrowClass}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>
</div>
</div>
)}
<div
class={`${this.isOpenAdvancedLogin ? 'max-h-[500px] mt-3 duration-300' : 'max-h-0 mt-0 duration-[0.25s]'} transition-max-height ease-in flex gap-3 flex-col overflow-hidden`}
>
{/* {this.hasExtension && !this.allowAuthMethod('extension') && this.renderSignInWithExtension()} */}
{this.allowAuthMethod('connect') && (
<button-base titleBtn="User name" onClick={() => this.handleChangeScreen(CURRENT_MODULE.LOGIN)}>
<svg
style={{ display: 'none' }}
slot="icon-start"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
// class="flex-shrink-0 w-4 h-4 text-gray-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
/>
</svg>
</button-base>
)}
{this.allowAuthMethod('connect') && (
<button-base titleBtn="Connection string" onClick={() => this.handleConnectionString()}>
<svg style={{ display: 'none' }} slot="icon-start" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 3.75 9.375v-4.5ZM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 0 1-1.125-1.125v-4.5ZM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0 1 13.5 9.375v-4.5Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 6.75h.75v.75h-.75v-.75ZM6.75 16.5h.75v.75h-.75v-.75ZM16.5 6.75h.75v.75h-.75v-.75ZM13.5 13.5h.75v.75h-.75v-.75ZM13.5 19.5h.75v.75h-.75v-.75ZM19.5 13.5h.75v.75h-.75v-.75ZM19.5 19.5h.75v.75h-.75v-.75ZM16.5 16.5h.75v.75h-.75v-.75Z"
/>
</svg>
</button-base>
)}
{this.allowAuthMethod('connect') && (
<button-base onClick={() => this.handleChangeScreen(CURRENT_MODULE.LOGIN_BUNKER_URL)} titleBtn="Bunker URL">
<svg style={{ display: 'none' }} slot="icon-start" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
/>
</svg>
</button-base>
)}
</div>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,43 @@
# nl-connect
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| -------------------------- | --------------- | ----------- | -------------------- | ------------------------ |
| `authMethods` | -- | | `AuthMethod[]` | `[]` |
| `connectionStringServices` | -- | | `ConnectionString[]` | `[]` |
| `hasOTP` | `has-o-t-p` | | `boolean` | `false` |
| `titleWelcome` | `title-welcome` | | `string` | `'Connect to key store'` |
## Events
| Event | Description | Type |
| ---------------- | ----------- | ------------------------------- |
| `nlNostrConnect` | | `CustomEvent<ConnectionString>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-connect --> button-base
nl-auth --> nl-connect
style nl-connect fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,26 @@
import { Component, h } from '@stencil/core';
@Component({
tag: 'nl-dialog',
styleUrl: 'nl-dialog.css',
shadow: true,
})
export class NlDialog {
private dialogElement?: HTMLDialogElement;
componentDidLoad() {
this.dialogElement?.showModal();
}
disconnectedCallback() {
this.dialogElement?.close();
}
render() {
return (
<dialog ref={el => (this.dialogElement = el as HTMLDialogElement)} class={'m-auto nl-banner-dialog'} style={{ border: '0', backgroundColor: 'transparent' }}>
<slot></slot>
</dialog>
);
}
}

View File

@@ -0,0 +1,23 @@
# nl-dialog
<!-- Auto Generated Below -->
## Dependencies
### Used by
- [nl-banner](../nl-banner)
### Graph
```mermaid
graph TD;
nl-banner --> nl-dialog
style nl-dialog fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,40 @@
import { Component, Event, EventEmitter, Prop, h } from '@stencil/core';
@Component({
tag: 'nl-iframe',
styleUrl: 'nl-iframe.css',
shadow: false,
})
export class NlConfirmLogout {
@Prop() titleModal = 'Confirm';
@Prop() description = 'Your profile keys are stored in this browser tab and will be deleted if you log out, and your profile will be inaccessible.';
@Prop() iframeUrl = '';
@Event() nlCloseModal: EventEmitter;
handleCancel() {
this.nlCloseModal.emit();
}
render() {
return (
<div class="p-4 overflow-y-auto">
{/* <h1 class="nl-title font-bold text-center text-4xl">{this.titleModal}</h1>
<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">{this.description}</p> */}
<div class="mt-3 flex flex-col gap-2">
{this.iframeUrl && (
<iframe
src={this.iframeUrl}
style={{
width: '100%',
height: '600px',
border: '1px solid #ccc',
borderRadius: '8px',
}}
></iframe>
)}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,39 @@
# nl-iframe
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | ------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `description` | `description` | | `string` | `'Your profile keys are stored in this browser tab and will be deleted if you log out, and your profile will be inaccessible.'` |
| `iframeUrl` | `iframe-url` | | `string` | `''` |
| `titleModal` | `title-modal` | | `string` | `'Confirm'` |
## Events
| Event | Description | Type |
| -------------- | ----------- | ------------------ |
| `nlCloseModal` | | `CustomEvent<any>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Graph
```mermaid
graph TD;
nl-auth --> nl-iframe
style nl-iframe fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,145 @@
import { Component, h, Fragment, State, Prop, Event, EventEmitter } from '@stencil/core';
import { state } from '@/store';
import { ConnectionString } from '@/types';
@Component({
tag: 'nl-import-flow',
styleUrl: 'nl-import-flow.css',
shadow: false,
})
export class NlImportFlow {
@Prop({ mutable: true }) titleInfo = 'Back up your keys';
@Prop() titleImport = 'Choose a service';
@Prop() services: ConnectionString[] = [];
@State() isContinued = false;
@State() isKeyBackup = false;
@State() isCopy = false;
@Event() nlImportAccount: EventEmitter<ConnectionString>;
@Event() nlExportKeys: EventEmitter<void>;
handleDomainSelect(event: CustomEvent<string>) {
const s = this.services.find(s => s.domain === event.detail);
state.nlImport = s;
}
handleCreateAccount(e: MouseEvent) {
e.preventDefault();
this.nlImportAccount.emit(state.nlImport);
}
handleContinue() {
this.isContinued = true;
}
handleContinueKeyBackup() {
this.isKeyBackup = true;
}
async copyToClipboard() {
this.nlExportKeys.emit();
this.isCopy = true;
setTimeout(() => {
this.isCopy = false;
}, 1500);
}
render() {
if (!this.isContinued && !this.isKeyBackup) {
return (
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">{this.titleInfo}</h1>
<p class="nl-description font-light text-sm pt-2 pb-2 max-w-96 mx-auto">
Nostr profiles are controlled by cryptographic keys.
<br />
<br />
Your keys are currently only stored in this browser tab, and may be lost if you close it.
<br />
<br />
You should backup your keys.
<br />
<br />
We recommend to import your keys into a key store service, to protect them and to use with other apps.
{/* <br />
<br />
You can also export your keys and save them in your password manager. */}
</p>
<div class="ml-auto mr-auto mb-2 w-72">
<button-base onClick={() => this.handleContinue()} titleBtn="Import to key store" />
</div>
<div class="ml-auto mr-auto w-72">
<button-base onClick={() => this.handleContinueKeyBackup()} titleBtn="Export keys" />
</div>
</div>
);
}
if (this.isKeyBackup) {
return (
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">Key export</h1>
<p class="nl-description font-light text-sm pt-2 pb-2 max-w-96 mx-auto">
Copy your keys and store them in a safe place, like a password manager.
<br />
<br />
You can sign into other Nostr apps by pasting your keys into them.
<br />
<br />
Your keys must be kept secret, never share them with anyone.
</p>
<div class="max-w-72 mx-auto">
<div class="ml-auto mr-auto mb-2 w-72">
<button-base onClick={() => this.copyToClipboard()} titleBtn={this.isCopy ? 'Copied!' : 'Copy to clipboard'} />
</div>
</div>
</div>
);
}
const options = this.services.filter(s => s.canImport).map(s => ({ name: s.domain!, value: s.domain! }));
return (
<Fragment>
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">{this.titleImport}</h1>
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">
Your Nostr keys will be imported into the service you choose. You will manage your keys on their website.
</p>
</div>
<div class="max-w-72 mx-auto mb-5">
<div class="mb-0.5">
<nl-select onSelectDomain={e => this.handleDomainSelect(e)} selected={0} options={options}></nl-select>
</div>
<p class="nl-title font-light text-sm mb-2">Default provider is a fine choice to start with.</p>
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
<button-base disabled={state.isLoading} onClick={e => this.handleCreateAccount(e)} titleBtn="Start importing">
{state.isLoading ? (
<span
slot="icon-start"
class="animate-spin-loading inline-block w-4 h-4 border-[3px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
) : (
<svg slot="icon-start" style={{ display: 'none' }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
/>
</svg>
)}
</button-base>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,45 @@
# nl-import-flow
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | -------------- | ----------- | -------------------- | --------------------- |
| `services` | -- | | `ConnectionString[]` | `[]` |
| `titleImport` | `title-import` | | `string` | `'Choose a service'` |
| `titleInfo` | `title-info` | | `string` | `'Back up your keys'` |
## Events
| Event | Description | Type |
| ----------------- | ----------- | ------------------------------- |
| `nlExportKeys` | | `CustomEvent<void>` |
| `nlImportAccount` | | `CustomEvent<ConnectionString>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [button-base](../button-base)
- [nl-select](../nl-select)
### Graph
```mermaid
graph TD;
nl-import-flow --> button-base
nl-import-flow --> nl-select
nl-auth --> nl-import-flow
style nl-import-flow fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,50 @@
import { Component, h } from '@stencil/core';
import { state } from '@/store';
@Component({
tag: 'nl-info-extension',
styleUrl: 'nl-info-extension.css',
shadow: false,
})
export class NlInfoExtension {
render() {
return (
<div class="p-4 overflow-y-auto">
{state.isLoadingExtension ? (
<div>
<h1 class="nl-title font-bold text-center text-4xl">Signing in...</h1>
<div class="mt-10 mb-10 ml-auto mr-auto w-20">
<span
slot="icon-start"
class="animate-spin-loading ml-auto mr-auto inline-block w-20 h-20 border-[4px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
</div>
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
</div>
) : (
<div>
<h1 class="nl-title font-bold text-center text-4xl">Install browser extension!</h1>
<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">
Try{' '}
<a href="https://getalby.com" target="_blank">
Alby
</a>
,{' '}
<a href="https://chromewebstore.google.com/detail/nos2x/kpgefcfmnafjgpblomihpgmejjdanjjp" target="_blank">
nos2x
</a>{' '}
or{' '}
<a href="https://apps.apple.com/us/app/nostore/id1666553677" target="_blank">
Nostore
</a>
</p>
</div>
)}
</div>
);
}
}

View File

@@ -0,0 +1,21 @@
# nl-info-extension
<!-- Auto Generated Below -->
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Graph
```mermaid
graph TD;
nl-auth --> nl-info-extension
style nl-info-extension fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,46 @@
import { Component, h, Prop } from '@stencil/core';
import { NlTheme } from '@/types';
@Component({
tag: 'nl-info',
styleUrl: 'nl-info.css',
shadow: false,
})
export class NlInfo {
@Prop() theme: NlTheme = 'default';
@Prop() darkMode: boolean = false;
render() {
return (
<div class="p-4 overflow-y-auto">
<svg class="w-12 h-12 mx-auto mb-2" width="225" height="224" viewBox="0 0 225 224" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="224.047" height="224" rx="64" fill={this.theme === 'laan' ? (this.darkMode ? 'white' : 'black') : '#6951FA'} />
<path
d="M162.441 135.941V88.0593C170.359 85.1674 176 77.5348 176 68.6696C176 57.2919 166.708 48 155.33 48C143.953 48 134.661 57.2444 134.661 68.6696C134.661 77.5822 140.302 85.1674 148.219 88.0593V135.941C147.698 136.13 147.176 136.367 146.655 136.604L87.3956 77.3452C88.6282 74.6904 89.2919 71.7511 89.2919 68.6696C89.2919 57.2444 80.0474 48 68.6696 48C57.2919 48 48 57.2444 48 68.6696C48 77.5822 53.6415 85.1674 61.5585 88.0593V135.941C53.6415 138.833 48 146.465 48 155.33C48 166.708 57.2444 176 68.6696 176C80.0948 176 89.3393 166.708 89.3393 155.33C89.3393 146.418 83.6978 138.833 75.7807 135.941V88.0593C76.3022 87.8696 76.8237 87.6326 77.3452 87.3956L136.604 146.655C135.372 149.31 134.708 152.249 134.708 155.33C134.708 166.708 143.953 176 155.378 176C166.803 176 176.047 166.708 176.047 155.33C176.047 146.418 170.406 138.833 162.489 135.941H162.441Z"
fill={this.theme === 'laan' ? (this.darkMode ? 'black' : 'white') : 'white'}
/>
</svg>
<h1 class="nl-title font-bold text-center text-4xl">
Nostr <span class="font-light">Login</span>
</h1>
<p class="text-green-800 dark:text-green-200 font-light text-center text-lg pt-2 max-w-96 mx-auto">Version: 1.7.11</p>
<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">
Learn more about Nostr{' '}
<a target="_blank" href="https://nostr.how">
here
</a>
.<br />
This is an{' '}
<a target="_blank" href="https://github.com/nostrband/nostr-login">
open-source
</a>{' '}
tool by{' '}
<a target="_blank" href="https://nostr.band">
Nostr.Band
</a>
.
</p>
</div>
);
}
}

View File

@@ -0,0 +1,29 @@
# nl-info
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ---------- | ----------- | ----------- | -------------------------------------------------------------------- | ----------- |
| `darkMode` | `dark-mode` | | `boolean` | `false` |
| `theme` | `theme` | | `"crab" \| "default" \| "laan" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Graph
```mermaid
graph TD;
nl-auth --> nl-info
style nl-info fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,93 @@
import { Component, Event, EventEmitter, Prop, h } from '@stencil/core';
import { state } from '@/store';
import { CURRENT_MODULE } from '@/types';
@Component({
tag: 'nl-loading',
styleUrl: 'nl-loading.css',
shadow: false,
})
export class NlLoading {
@Event() stopFetchHandler: EventEmitter<boolean>;
@Event() handleContinue: EventEmitter<boolean>;
@Prop() path: string;
handleStop(e) {
e.preventDefault();
this.stopFetchHandler.emit();
}
handleContinueClick(e) {
e.preventDefault();
// reset();
this.handleContinue.emit();
}
render() {
let title = 'Connecting...';
let text = 'Establishing connection to your key storage.';
if (state.njumpIframe) {
title = '';
text = '';
} else if (this.path === CURRENT_MODULE.LOCAL_SIGNUP) {
title = 'Creating...';
text = 'Publishing your profile on Nostr.';
} else if (state.authUrl) {
if (state.isLoading) {
title = 'Confirming...';
text = 'Please confirm the connection in your key storage app.';
} else {
title = 'Almost ready!';
text = 'Continue to confirm the connection to your key storage.';
}
}
const showButton = this.path !== CURRENT_MODULE.LOCAL_SIGNUP;
const showIframe = !state.isLoading && state.iframeUrl && state.authUrl;
const iframeUrl = state.iframeUrl ? `${state.iframeUrl}?connect=${encodeURIComponent(state.authUrl)}` : '';
return (
<div class="p-4 overflow-y-auto">
{title && (<h1 class="nl-title font-bold text-center text-4xl">{title}</h1>)}
{text && (<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">{text}</p>)}
{!state.njumpIframe && !state.authUrl && state.isLoading && (
<div class="mt-10 mb-10 ml-auto mr-auto w-20">
<span
slot="icon-start"
class="animate-spin-loading ml-auto mr-auto inline-block w-20 h-20 border-[4px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
</div>
)}
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
{iframeUrl && (
<div class="mt-3 ml-auto mr-auto w-72 flex justify-center">
<iframe src={iframeUrl} width="180px" height="80px" style={{ display: showIframe ? 'block' : 'none', border: '0' }}></iframe>
</div>
)}
{state.njumpIframe && (
<div class="mt-3 ml-auto mr-auto flex justify-center">
<iframe srcdoc={state.njumpIframe} width="600px" style={{ border: '0', height: "80vh", borderRadius: "8px" }}></iframe>
</div>
)}
{!showIframe && showButton && (
<div class="mt-3 ml-auto mr-auto w-72">
<button-base
onClick={e => {
if (state.authUrl && !state.isLoading) {
this.handleContinueClick(e);
} else {
this.handleStop(e);
}
}}
titleBtn={!state.isLoading ? 'Continue' : 'Cancel'}
/>
</div>
)}
</div>
);
}
}

View File

@@ -0,0 +1,41 @@
# nl-loading
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| -------- | --------- | ----------- | -------- | ----------- |
| `path` | `path` | | `string` | `undefined` |
## Events
| Event | Description | Type |
| ------------------ | ----------- | ---------------------- |
| `handleContinue` | | `CustomEvent<boolean>` |
| `stopFetchHandler` | | `CustomEvent<boolean>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-loading --> button-base
nl-auth --> nl-loading
style nl-loading fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,100 @@
import { Component, h, Fragment, State, Prop, Event, EventEmitter } from '@stencil/core';
import { state } from '@/store';
@Component({
tag: 'nl-local-signup',
styleUrl: 'nl-local-signup.css',
shadow: false,
})
export class NlLocalSignup {
@Prop() titleSignup = 'Create Nostr profile';
@Prop() description = 'Choose any username, you can always change it later.';
@Prop() descriptionNjump = 'Proceed to creating your Nostr profile in a new tab.';
@Prop() signupNjump = false;
@State() isAvailable = false;
@Event() nlLocalSignup: EventEmitter<string>;
@Event() nlSignupNjump: EventEmitter<void>;
// @Event() nlCheckSignup: EventEmitter<string>;
@Event() fetchHandler: EventEmitter<boolean>;
handleInputChange(event: Event) {
state.nlSignup.signupName = (event.target as HTMLInputElement).value;
// this.nlCheckSignup.emit(`${(event.target as HTMLInputElement).value}@${state.nlSignup.domain}`);
}
handleCreateAccount(e: MouseEvent) {
e.preventDefault();
if (this.signupNjump) {
this.nlSignupNjump.emit();
} else {
this.nlLocalSignup.emit(`${state.nlSignup.signupName}`);
}
}
render() {
return (
<Fragment>
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">{this.titleSignup}</h1>
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">{this.signupNjump ? this.descriptionNjump : this.description}</p>
</div>
<div class="max-w-72 mx-auto">
{!this.signupNjump && (
<div class="relative mb-2">
<input
onInput={e => this.handleInputChange(e)}
type="text"
class="nl-input peer py-3 px-4 ps-11 block w-full border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
placeholder="Enter username"
value={state.nlSignup.signupName}
/>
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4 peer-disabled:opacity-50 peer-disabled:pointer-events-none">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke={this.isAvailable ? '#00cc00' : 'currentColor'}
class="flex-shrink-0 w-4 h-4 text-gray-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
/>
</svg>
</div>
</div>
)}
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
<button-base disabled={state.isLoading} onClick={e => this.handleCreateAccount(e)} titleBtn={this.signupNjump ? 'Get started' : 'Create profile'}>
{state.isLoading ? (
<span
slot="icon-start"
class="animate-spin-loading inline-block w-4 h-4 border-[3px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
) : (
<svg slot="icon-start" style={{ display: 'none' }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
/>
</svg>
)}
</button-base>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,45 @@
# nl-local-signup
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------------ | ------------------- | ----------- | --------- | -------------------------------------------------------- |
| `description` | `description` | | `string` | `'Choose any username, you can always change it later.'` |
| `descriptionNjump` | `description-njump` | | `string` | `'Proceed to creating your Nostr profile in a new tab.'` |
| `signupNjump` | `signup-njump` | | `boolean` | `false` |
| `titleSignup` | `title-signup` | | `string` | `'Create Nostr profile'` |
## Events
| Event | Description | Type |
| --------------- | ----------- | ---------------------- |
| `fetchHandler` | | `CustomEvent<boolean>` |
| `nlLocalSignup` | | `CustomEvent<string>` |
| `nlSignupNjump` | | `CustomEvent<void>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-local-signup --> button-base
nl-auth --> nl-local-signup
style nl-local-signup fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,41 @@
import { Info, RecentType } from '@/types';
import { Component, h, Prop } from '@stencil/core';
@Component({
tag: 'nl-login-status',
// styleUrl: 'nl-login-status.css',
shadow: false,
})
export class NlLoginStatus {
@Prop() info: RecentType | Info | undefined;
render() {
let text = '';
let color = '';
if (this.info.authMethod === 'extension') {
text = 'Extension';
color = 'border-yellow-300 text-yellow-500 bg-yellow-100';
} else if (this.info.authMethod === 'readOnly') {
text = 'Read only';
color = 'border-gray-300 text-gray-400 bg-gray-100';
} else if (this.info.authMethod === 'connect') {
text = 'Connect';
color = 'border-teal-300 text-teal-600 bg-teal-100';
} else if (this.info.authMethod === 'local') {
text = 'Temporary';
color = 'border-red-300 text-red-600 bg-red-100';
} else if (this.info.authMethod === 'otp') {
text = 'Delegated';
color = 'border-orange-300 text-orange-600 bg-orange-100';
} else {
console.log('unknown auth method', this.info);
throw new Error('Unknown auth method');
}
return (
<div>
<span class={`${color} rounded-xl border w-auto text-[10px] px-1 `}>{text}</span>
</div>
);
}
}

View File

@@ -0,0 +1,32 @@
# nl-login-status
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| -------- | --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| `info` | -- | | `Info \| { name?: string; picture?: string; nip05?: string; pubkey: string; bunkerUrl?: string; authMethod: AuthMethod; domain?: string; signerPubkey?: string; }` | `undefined` |
## Dependencies
### Used by
- [nl-banner](../nl-banner)
- [nl-change-account](../nl-change-account)
- [nl-previously-logged](../nl-previously-logged)
### Graph
```mermaid
graph TD;
nl-banner --> nl-login-status
nl-change-account --> nl-login-status
nl-previously-logged --> nl-login-status
style nl-login-status fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,70 @@
import { Component, h, Fragment, Prop, Event, EventEmitter } from '@stencil/core';
import { state } from '@/store';
import { ConnectionString } from '@/types';
@Component({
tag: 'nl-otp-migrate',
styleUrl: 'nl-otp-migrate.css',
shadow: false,
})
export class NlImportFlow {
@Prop({ mutable: true }) titleInfo = 'Import keys to storage service';
@Prop() titleImport = 'Choose a service';
@Prop() textImport = 'You will be prompted to import keys to the chosen service, and this website will connect to your keys.';
@Prop() services: ConnectionString[] = [];
@Event() nlImportAccount: EventEmitter<ConnectionString>;
handleDomainSelect(event: CustomEvent<string>) {
const s = this.services.find(s => s.domain === event.detail);
state.nlImport = s;
}
handleCreateAccount(e: MouseEvent) {
e.preventDefault();
this.nlImportAccount.emit(state.nlImport);
}
render() {
const options = this.services.filter(s => s.canImport).map(s => ({ name: s.domain!, value: s.domain! }));
return (
<Fragment>
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">{this.titleImport}</h1>
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">{this.textImport}</p>
</div>
<div class="max-w-72 mx-auto mb-5">
<div class="mb-0.5">
<nl-select onSelectDomain={e => this.handleDomainSelect(e)} selected={0} options={options}></nl-select>
</div>
<p class="nl-title font-light text-sm mb-2">Default provider is a fine choice to start with.</p>
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
<button-base disabled={state.isLoading} onClick={e => this.handleCreateAccount(e)} titleBtn="Start importing">
{state.isLoading ? (
<span
slot="icon-start"
class="animate-spin-loading inline-block w-4 h-4 border-[3px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
) : (
<svg slot="icon-start" style={{ display: 'none' }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
/>
</svg>
)}
</button-base>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,47 @@
# nl-otp-migrate
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | -------------- | ----------- | -------------------- | ---------------------------------------------------------------------------------------------------------- |
| `services` | -- | | `ConnectionString[]` | `[]` |
| `textImport` | `text-import` | | `string` | `'You will be prompted to import keys to the chosen service, and this website will connect to your keys.'` |
| `titleImport` | `title-import` | | `string` | `'Choose a service'` |
| `titleInfo` | `title-info` | | `string` | `'Import keys to storage service'` |
## Events
| Event | Description | Type |
| ----------------- | ----------- | ------------------------------- |
| `nlImportAccount` | | `CustomEvent<ConnectionString>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [nl-select](../nl-select)
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-otp-migrate --> nl-select
nl-otp-migrate --> button-base
nl-auth --> nl-otp-migrate
style nl-otp-migrate fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,4 @@
:host {
display: block;
}

View File

@@ -0,0 +1,191 @@
import { Component, h, Fragment, Prop, Event, EventEmitter, Watch } from '@stencil/core';
import { CURRENT_MODULE, Info, RecentType } from '@/types';
import { state } from '@/store';
@Component({
tag: 'nl-previously-logged',
styleUrl: 'nl-previously-logged.css',
shadow: false,
})
export class NlPreviouslyLogged {
@Prop() titlePage = 'Your profiles';
@Prop() description = 'Switch between active profiles or choose a recent one for fast login.';
@Prop() accounts: Info[] = [];
@Prop() recents: RecentType[] = [];
@Event() nlSwitchAccount: EventEmitter<Info>;
@Event() nlLoginRecentAccount: EventEmitter<RecentType>;
@Event() nlRemoveRecent: EventEmitter<RecentType>;
handleGoToWelcome() {
state.path = [CURRENT_MODULE.WELCOME];
}
switchToWelcomeIfEmpty() {
if (!this.recents.length && !this.accounts.length) {
state.path = [CURRENT_MODULE.WELCOME];
}
}
@Watch('accounts')
watchAccounts() {
this.switchToWelcomeIfEmpty();
}
@Watch('recents')
watchRecents() {
this.switchToWelcomeIfEmpty();
}
handleRemoveRecent(user: Info) {
this.nlRemoveRecent.emit(user);
}
handleSwitch(el: Info) {
this.nlSwitchAccount.emit(el);
}
handleLoginRecentAccount(el: RecentType) {
this.nlLoginRecentAccount.emit(el);
}
render() {
return (
<Fragment>
<div class="p-4 pt-0 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-4xl">{this.titlePage}</h1>
<p class="nl-description font-light text-center text-lg pt-2 max-w-96 mx-auto">{this.description}</p>
</div>
<div class="p-4">
{Boolean(this.accounts.length) && (
<div class="max-w-96 mx-auto">
<p class="nl-description font-medium text-sm pb-1.5">Active profiles</p>
<ul class="p-2 rounded-lg border border-blue-200 flex flex-col w-full gap-0.5">
{this.accounts.map(el => {
const isShowImg = Boolean(el?.picture);
const userName = el.name || el.nip05 || el.pubkey;
const isShowUserName = Boolean(userName);
return (
<li onClick={() => this.handleSwitch(el)} class="group hover:bg-gray-400 flex cursor-pointer gap-x-3.5 py-2 px-3 rounded-lg text-sm items-center justify-between">
<div class="flex items-center gap-x-3.5 w-full">
<div class="w-full max-w-7 h-7 flex relative">
<div class="absolute top-[-2px] right-[-2px] bg-white border-2 border-white rounded-xl">
<div class="active h-1.5 w-1.5 bg-green-500 rounded-xl"></div>
</div>
<div class="group-hover:border-blue-400 uppercase font-bold w-full h-full rounded-full border border-gray-400 flex justify-center items-center">
{isShowImg ? (
<img class="w-full rounded-full" src={el.picture} alt="Logo" />
) : isShowUserName ? (
userName[0]
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
)}
</div>
</div>
<div class="overflow-hidden flex flex-col w-full">
<div class="nl-title truncate overflow-hidden">{userName}</div>
<nl-login-status info={el} />
</div>
</div>
{/* <div class="w-full max-w-6">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full hover:text-blue-600">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15M12 9l3 3m0 0-3 3m3-3H2.25"
/>
</svg>
</div> */}
</li>
);
})}
</ul>
</div>
)}
{Boolean(this.recents.length) && (
<div class="max-w-96 mx-auto pt-5">
<p class="nl-description font-medium text-sm pb-1.5">Recent profiles</p>
<ul class="p-2 rounded-lg border border-gray-200 flex flex-col w-full gap-0.5">
{this.recents.map(el => {
const isShowImg = Boolean(el?.picture);
const userName = el.name || el.nip05 || el.pubkey;
const isShowUserName = Boolean(userName);
return (
<li
onClick={() => this.handleLoginRecentAccount(el)}
class="flex items-center gap-x-3.5 w-full hover:bg-gray-400 flex cursor-pointer items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm justify-between"
>
<div class="w-full max-w-7 h-7 flex relative">
<div class="absolute top-[-3px] right-[-3px] bg-white border border-white rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</div>
<div class="uppercase font-bold w-full h-full rounded-full border border-gray-400 flex justify-center items-center">
{isShowImg ? (
<img class="w-full rounded-full" src={el.picture} alt="Logo" />
) : isShowUserName ? (
userName[0]
) : (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-full">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
)}
</div>
</div>
<div class="overflow-hidden flex flex-col w-full">
<div class="nl-title truncate overflow-hidden">{userName}</div>
<nl-login-status info={el} />
</div>
<svg
onClick={e => {
e.stopPropagation();
this.handleRemoveRecent(el);
}}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-full max-w-6 h-6 text-red-500 hover:text-red-600 ml-auto"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</li>
);
})}
</ul>
</div>
)}
</div>
<div class="p-4 overflow-y-auto">
<p class="nl-footer font-light text-center text-sm max-w-96 mx-auto">
You can also{' '}
<span onClick={() => this.handleGoToWelcome()} class="cursor-pointer pb-3 text-blue-500">
add another profile
</span>
</p>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,45 @@
# nl-previously-logged
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | ------------- | ----------- | -------------- | ------------------------------------------------------------------------- |
| `accounts` | -- | | `Info[]` | `[]` |
| `description` | `description` | | `string` | `'Switch between active profiles or choose a recent one for fast login.'` |
| `recents` | -- | | `RecentType[]` | `[]` |
| `titlePage` | `title-page` | | `string` | `'Your profiles'` |
## Events
| Event | Description | Type |
| ---------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nlLoginRecentAccount` | | `CustomEvent<{ name?: string; picture?: string; nip05?: string; pubkey: string; bunkerUrl?: string; authMethod: AuthMethod; domain?: string; signerPubkey?: string; }>` |
| `nlRemoveRecent` | | `CustomEvent<{ name?: string; picture?: string; nip05?: string; pubkey: string; bunkerUrl?: string; authMethod: AuthMethod; domain?: string; signerPubkey?: string; }>` |
| `nlSwitchAccount` | | `CustomEvent<Info>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [nl-login-status](../nl-login-status)
### Graph
```mermaid
graph TD;
nl-previously-logged --> nl-login-status
nl-auth --> nl-previously-logged
style nl-previously-logged fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,118 @@
import { Component, h, Listen, Prop, State, Watch, Element, Event, EventEmitter } from '@stencil/core';
export type OptionType = {
name: string;
value: string;
};
@Component({
tag: 'nl-select',
styleUrl: 'nl-select.css',
shadow: false,
})
export class NlSelect {
@State() isOpen: boolean = false;
@State() value: OptionType = null;
@Prop() options: OptionType[];
@Prop() selected: number;
@Element() element: HTMLElement;
@Event() selectDomain: EventEmitter<string>;
buttonRef: HTMLButtonElement;
ulRef: HTMLUListElement;
wrapperRef: HTMLDivElement;
@Listen('click', { target: 'window' })
handleWindowClick() {
if (this.wrapperRef.querySelector('.listClass')) {
this.isOpen = false;
}
}
toggleDropdown() {
this.isOpen = !this.isOpen;
this.calculateDropdownPosition();
}
@State() mode: boolean = false;
@Prop() darkMode: boolean = false;
@State() themeState: 'default' | 'ocean' | 'lemonade' | 'purple' = 'default';
@Prop() theme: 'default' | 'ocean' | 'lemonade' | 'purple' = 'default';
@Watch('theme')
watchPropHandler(newValue: 'default' | 'ocean' | 'lemonade' | 'purple') {
this.themeState = newValue;
}
@Watch('darkMode')
watchModeHandler(newValue: boolean) {
this.mode = newValue;
}
connectedCallback() {
this.themeState = this.theme;
this.mode = this.darkMode;
this.value = this.options[this.selected];
this.selectDomain.emit(this.value.value);
}
calculateDropdownPosition() {
if (this.isOpen && this.buttonRef) {
const buttonRect = this.buttonRef.getBoundingClientRect();
this.ulRef.style.top = `${buttonRect.height}px`;
}
}
handleChange(el: OptionType) {
this.value = el;
this.isOpen = false;
this.selectDomain.emit(this.value.value);
}
render() {
const listClass = `${this.isOpen ? 'listClass' : 'hidden'} min-w-[15rem] nl-select-list absolute left-0 shadow-md rounded-lg p-2 mt-1 after:h-4 after:absolute after:-bottom-4 after:start-0 after:w-full before:h-4 before:absolute before:-top-4 before:start-0 before:w-full`;
const arrowClass = `${this.isOpen ? 'rotate-180' : 'rotate-0'} duration-300 flex-shrink-0 w-4 h-4 text-gray-500`;
return (
<div class={`theme-${this.themeState}`}>
<div class="relative" ref={el => (this.wrapperRef = el)}>
<button
ref={el => (this.buttonRef = el)}
onClick={() => this.toggleDropdown()}
type="button"
class="nl-select peer py-3 px-4 flex items-center w-full justify-between border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
>
<span class="truncate overflow-hidden">{this.value.name}</span>
<svg
class={arrowClass}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
<ul ref={el => (this.ulRef = el)} class={listClass}>
{this.options.map(el => {
return (
<li onClick={() => this.handleChange(el)} class="nl-select-option flex cursor-pointer items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm">
{el.name}
</li>
);
})}
</ul>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,42 @@
# nl-select
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ---------- | ----------- | ----------- | ------------------------------------------------ | ----------- |
| `darkMode` | `dark-mode` | | `boolean` | `false` |
| `options` | -- | | `OptionType[]` | `undefined` |
| `selected` | `selected` | | `number` | `undefined` |
| `theme` | `theme` | | `"default" \| "lemonade" \| "ocean" \| "purple"` | `'default'` |
## Events
| Event | Description | Type |
| -------------- | ----------- | --------------------- |
| `selectDomain` | | `CustomEvent<string>` |
## Dependencies
### Used by
- [nl-import-flow](../nl-import-flow)
- [nl-otp-migrate](../nl-otp-migrate)
- [nl-signup](../nl-signup)
### Graph
```mermaid
graph TD;
nl-import-flow --> nl-select
nl-otp-migrate --> nl-select
nl-signup --> nl-select
style nl-select fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,90 @@
import { Component, h, State, Prop, Fragment, Event, EventEmitter } from '@stencil/core';
import { state } from '@/store';
@Component({
tag: 'nl-signin-bunker-url',
styleUrl: 'nl-signin-bunker-url.css',
shadow: false,
})
export class NlSigninBunkerUrl {
@Prop() titleLogin = 'Connect with bunker url';
@Prop() description = 'Please enter a bunker url provided by key store.';
@State() isGood = false;
@Event() nlLogin: EventEmitter<string>;
@Event() nlCheckLogin: EventEmitter<string>;
handleInputChange(event: Event) {
state.nlSigninBunkerUrl.loginName = (event.target as HTMLInputElement).value;
this.nlCheckLogin.emit((event.target as HTMLInputElement).value);
}
handleLogin(e: MouseEvent) {
e.preventDefault();
this.nlLogin.emit(state.nlSigninBunkerUrl.loginName);
}
render() {
return (
<Fragment>
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">{this.titleLogin}</h1>
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">{this.description}</p>
</div>
<div class="max-w-72 mx-auto">
<div class="relative mb-2">
<input
onInput={e => this.handleInputChange(e)}
type="text"
class="nl-input peer py-3 px-4 ps-11 block w-full border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
placeholder="bunker://..."
value={state.nlSigninBunkerUrl.loginName}
/>
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4 peer-disabled:opacity-50 peer-disabled:pointer-events-none">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke={this.isGood ? '#00cc00' : 'currentColor'}
class="flex-shrink-0 w-4 h-4 text-gray-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
/>
</svg>
</div>
</div>
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
<button-base titleBtn="Connect" disabled={state.isLoading} onClick={e => this.handleLogin(e)}>
{state.isLoading ? (
<span
slot="icon-start"
class="animate-spin-loading inline-block w-4 h-4 border-[3px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
) : (
<svg style={{ display: 'none' }} slot="icon-start" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15M12 9l3 3m0 0-3 3m3-3H2.25"
/>
</svg>
)}
</button-base>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,42 @@
# nl-signin-bunker-url
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | ------------- | ----------- | -------- | ---------------------------------------------------- |
| `description` | `description` | | `string` | `'Please enter a bunker url provided by key store.'` |
| `titleLogin` | `title-login` | | `string` | `'Connect with bunker url'` |
## Events
| Event | Description | Type |
| -------------- | ----------- | --------------------- |
| `nlCheckLogin` | | `CustomEvent<string>` |
| `nlLogin` | | `CustomEvent<string>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-signin-bunker-url --> button-base
nl-auth --> nl-signin-bunker-url
style nl-signin-bunker-url fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,111 @@
import { Component, h, Prop, Fragment, State, Event, EventEmitter } from '@stencil/core';
import QRCode from 'qrcode';
@Component({
tag: 'nl-signin-connection-string',
styleUrl: 'nl-signin-connection-string.css',
shadow: false,
})
export class NlSigninConnectionString {
@Prop() titleLogin = 'Connection string';
@Prop() description = 'Scan or copy the connection string with key store app';
@Prop() connectionString = '';
@State() isCopy = false;
@Event() nlNostrConnectDefault: EventEmitter<void>;
private canvasElement: HTMLCanvasElement;
componentDidLoad() {
this.nlNostrConnectDefault.emit();
this.generateQRCode();
}
async generateQRCode() {
if (this.connectionString && this.canvasElement) {
try {
await QRCode.toCanvas(this.canvasElement, this.connectionString);
} catch (error) {
console.error('Error generating QR Code:', error);
}
}
}
async copyToClipboard() {
try {
await navigator.clipboard.writeText(this.connectionString);
this.isCopy = true;
setTimeout(() => {
this.isCopy = false;
}, 1500);
} catch (err) {
console.error('Failed to copy connectionString: ', err);
}
}
render() {
return (
<Fragment>
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">{this.titleLogin}</h1>
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">{this.description}</p>
</div>
<canvas class="mx-auto mb-2" ref={el => (this.canvasElement = el as HTMLCanvasElement)}></canvas>
<div class="px-4">
<div class="max-w-72 mx-auto">
<div class="relative mb-2">
<input
type="text"
class="nl-input peer py-3 px-4 pe-11 ps-11 block w-full border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
placeholder="npub or name@domain"
value={this.connectionString}
disabled
/>
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4 peer-disabled:opacity-50 peer-disabled:pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-4 h-4 text-gray-500">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</div>
{this.isCopy ? (
<div class="absolute inset-y-0 end-0 flex items-center p-2 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="#00cc00" class="flex-shrink-0 w-4 h-4 text-gray-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</div>
) : (
<div class="absolute inset-y-0 end-0 flex items-center cursor-pointer p-2 rounded-lg" onClick={() => this.copyToClipboard()}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-4 h-4 text-gray-500">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 8.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v8.25A2.25 2.25 0 0 0 6 16.5h2.25m8.25-8.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-7.5A2.25 2.25 0 0 1 8.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 0 0-2.25 2.25v6"
/>
</svg>
</div>
)}
</div>
<div class="mt-10 justify-center items-center flex gap-2">
<span
slot="icon-start"
class="animate-spin-loading inline-block w-[20px] h-[20px] border-[2px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
<span class="nl-footer">Waiting for connection</span>
</div>
</div>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,37 @@
# nl-signin-connection-string
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------------ | ------------------- | ----------- | -------- | --------------------------------------------------------- |
| `connectionString` | `connection-string` | | `string` | `''` |
| `description` | `description` | | `string` | `'Scan or copy the connection string with key store app'` |
| `titleLogin` | `title-login` | | `string` | `'Connection string'` |
## Events
| Event | Description | Type |
| ----------------------- | ----------- | ------------------- |
| `nlNostrConnectDefault` | | `CustomEvent<void>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Graph
```mermaid
graph TD;
nl-auth --> nl-signin-connection-string
style nl-signin-connection-string fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,89 @@
import { Component, h, State, Prop, Fragment, Event, EventEmitter } from '@stencil/core';
import { state } from '@/store';
@Component({
tag: 'nl-signin-otp',
styleUrl: 'nl-signin-otp.css',
shadow: false,
})
export class NlSigninOtp {
@Prop() titleLogin = 'Log in with DM';
@Prop() description = 'Please enter your user name or npub, and we will send you a direct message with a one-time code.';
@Prop() titleLoginOTP = 'Enter the code';
@Prop() descriptionOTP = 'Please enter the one-time code we sent to you as a direct message on Nostr.';
@State() isGood = false;
@Event() nlLoginOTPUser: EventEmitter<string>;
@Event() nlLoginOTPCode: EventEmitter<string>;
@Event() nlCheckLogin: EventEmitter<string>;
handleInputChange(event: Event) {
if (!state.isOTP) {
state.nlSigninOTP.loginName = (event.target as HTMLInputElement).value;
this.nlCheckLogin.emit(state.nlSigninOTP.loginName);
} else {
state.nlSigninOTP.code = (event.target as HTMLInputElement).value;
}
}
handleLogin(e: MouseEvent) {
e.preventDefault();
if (state.isOTP) this.nlLoginOTPCode.emit(state.nlSigninOTP.code);
else this.nlLoginOTPUser.emit(state.nlSigninOTP.loginName);
}
render() {
return (
<Fragment>
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">{state.isOTP ? this.titleLoginOTP : this.titleLogin}</h1>
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">{state.isOTP ? this.descriptionOTP : this.description}</p>
</div>
<div class="max-w-72 mx-auto">
<div class="relative mb-2">
<input
onInput={e => this.handleInputChange(e)}
type="text"
class="nl-input peer py-3 px-4 ps-11 block w-full border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
placeholder={state.isOTP ? 'code from direct message' : 'npub or name@domain'}
value={state.isOTP ? state.nlSigninOTP.code : state.nlSigninOTP.loginName}
/>
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4 peer-disabled:opacity-50 peer-disabled:pointer-events-none">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke={this.isGood ? '#00cc00' : 'currentColor'}
class="flex-shrink-0 w-4 h-4 text-gray-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</div>
</div>
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
<button-base titleBtn="Log in" disabled={state.isLoading} onClick={e => this.handleLogin(e)}>
{state.isLoading && (
<span
slot="icon-start"
class="animate-spin-loading inline-block w-4 h-4 border-[3px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
)}
</button-base>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,45 @@
# nl-signin-otp
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ---------------- | ------------------- | ----------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `description` | `description` | | `string` | `'Please enter your user name or npub, and we will send you a direct message with a one-time code.'` |
| `descriptionOTP` | `description-o-t-p` | | `string` | `'Please enter the one-time code we sent to you as a direct message on Nostr.'` |
| `titleLogin` | `title-login` | | `string` | `'Log in with DM'` |
| `titleLoginOTP` | `title-login-o-t-p` | | `string` | `'Enter the code'` |
## Events
| Event | Description | Type |
| ---------------- | ----------- | --------------------- |
| `nlCheckLogin` | | `CustomEvent<string>` |
| `nlLoginOTPCode` | | `CustomEvent<string>` |
| `nlLoginOTPUser` | | `CustomEvent<string>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-signin-otp --> button-base
nl-auth --> nl-signin-otp
style nl-signin-otp fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,81 @@
import { Component, h, State, Prop, Fragment, Event, EventEmitter } from '@stencil/core';
import { state } from '@/store';
@Component({
tag: 'nl-signin-read-only',
styleUrl: 'nl-signin-read-only.css',
shadow: false,
})
export class NlSigninReadOnly {
@Prop() titleLogin = 'Log in to read only';
@Prop() description = 'Please enter the user name or npub of any Nostr user.';
@State() isGood = false;
@Event() nlLoginReadOnly: EventEmitter<string>;
@Event() nlCheckLogin: EventEmitter<string>;
handleInputChange(event: Event) {
state.nlSigninReadOnly.loginName = (event.target as HTMLInputElement).value;
this.nlCheckLogin.emit((event.target as HTMLInputElement).value); // .emit(this.loginName);
}
handleLogin(e: MouseEvent) {
e.preventDefault();
this.nlLoginReadOnly.emit(state.nlSigninReadOnly.loginName);
}
render() {
return (
<Fragment>
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">{this.titleLogin}</h1>
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">{this.description}</p>
</div>
<div class="max-w-72 mx-auto">
<div class="relative mb-2">
<input
onInput={e => this.handleInputChange(e)}
type="text"
class="nl-input peer py-3 px-4 ps-11 block w-full border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
placeholder="npub or name@domain"
value={state.nlSigninReadOnly.loginName}
/>
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4 peer-disabled:opacity-50 peer-disabled:pointer-events-none">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke={this.isGood ? '#00cc00' : 'currentColor'}
class="flex-shrink-0 w-4 h-4 text-gray-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</div>
</div>
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
<button-base titleBtn="Log in" disabled={state.isLoading} onClick={e => this.handleLogin(e)}>
{state.isLoading && (
<span
slot="icon-start"
class="animate-spin-loading inline-block w-4 h-4 border-[3px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
)}
</button-base>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,42 @@
# nl-signin-read-only
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | ------------- | ----------- | -------- | --------------------------------------------------------- |
| `description` | `description` | | `string` | `'Please enter the user name or npub of any Nostr user.'` |
| `titleLogin` | `title-login` | | `string` | `'Log in to read only'` |
## Events
| Event | Description | Type |
| ----------------- | ----------- | --------------------- |
| `nlCheckLogin` | | `CustomEvent<string>` |
| `nlLoginReadOnly` | | `CustomEvent<string>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-signin-read-only --> button-base
nl-auth --> nl-signin-read-only
style nl-signin-read-only fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,96 @@
import { Component, h, State, Prop, Fragment, Event, EventEmitter } from '@stencil/core';
import { state } from '@/store';
@Component({
tag: 'nl-signin',
styleUrl: 'nl-signin.css',
shadow: false,
})
export class NlSignin {
@Prop() titleLogin = 'Connect to key store';
@Prop() description = 'Please enter your user name.';
@State() isGood = false;
@Event() nlLogin: EventEmitter<string>;
@Event() nlCheckLogin: EventEmitter<string>;
handleInputChange(event: Event) {
state.nlSignin.loginName = (event.target as HTMLInputElement).value;
this.nlCheckLogin.emit((event.target as HTMLInputElement).value);
}
handleLogin(e: MouseEvent) {
e.preventDefault();
this.nlLogin.emit(state.nlSignin.loginName);
}
render() {
return (
<Fragment>
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">{this.titleLogin}</h1>
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">{this.description}</p>
</div>
<div class="max-w-72 mx-auto">
<div class="relative mb-2">
<input
onInput={e => this.handleInputChange(e)}
type="text"
class="nl-input peer py-3 px-4 ps-11 block w-full border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
placeholder="name@domain.com"
value={state.nlSignin.loginName}
/>
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4 peer-disabled:opacity-50 peer-disabled:pointer-events-none">
{/* <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-4 h-4 text-gray-500">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
/>
</svg> */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke={this.isGood ? '#00cc00' : 'currentColor'}
class="flex-shrink-0 w-4 h-4 text-gray-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
/>
</svg>
</div>
</div>
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
<button-base titleBtn="Connect" disabled={state.isLoading} onClick={e => this.handleLogin(e)}>
{state.isLoading ? (
<span
slot="icon-start"
class="animate-spin-loading inline-block w-4 h-4 border-[3px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
) : (
<svg style={{ display: 'none' }} slot="icon-start" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15M12 9l3 3m0 0-3 3m3-3H2.25"
/>
</svg>
)}
</button-base>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,42 @@
# nl-signin
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | ------------- | ----------- | -------- | -------------------------------- |
| `description` | `description` | | `string` | `'Please enter your user name.'` |
| `titleLogin` | `title-login` | | `string` | `'Connect to key store'` |
## Events
| Event | Description | Type |
| -------------- | ----------- | --------------------- |
| `nlCheckLogin` | | `CustomEvent<string>` |
| `nlLogin` | | `CustomEvent<string>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-signin --> button-base
nl-auth --> nl-signin
style nl-signin fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@@ -0,0 +1,125 @@
import { Component, h, Fragment, State, Prop, Event, EventEmitter, Watch } from '@stencil/core';
import { state } from '@/store';
@Component({
tag: 'nl-signup',
styleUrl: 'nl-signup.css',
shadow: false,
})
export class NlSignup {
@Prop() titleSignup = 'Create keys with key store';
@Prop() description = 'Choose some username and a key store service.';
@Prop() bunkers: string = 'nsec.app,highlighter.com';
@State() isAvailable = false;
@Event() nlSignup: EventEmitter<string>;
@Event() nlCheckSignup: EventEmitter<string>;
@Event() fetchHandler: EventEmitter<boolean>;
formatServers(bunkers: string) {
return bunkers.split(',').map(d => ({
name: '@' + d,
value: d,
}));
}
handleInputChange(event: Event) {
state.nlSignup.signupName = (event.target as HTMLInputElement).value;
this.nlCheckSignup.emit(`${(event.target as HTMLInputElement).value}@${state.nlSignup.domain}`);
}
handleDomainSelect(event: CustomEvent<string>) {
state.nlSignup.domain = event.detail;
this.nlCheckSignup.emit(`${state.nlSignup.signupName}@${event.detail}`);
}
handleCreateAccount(e: MouseEvent) {
e.preventDefault();
this.nlSignup.emit(`${state.nlSignup.signupName}@${state.nlSignup.domain}`);
}
@Watch('bunkers')
watchBunkersHandler(newValue: string) {
state.nlSignup.servers = this.formatServers(newValue);
}
componentWillLoad() {
state.nlSignup.servers = this.formatServers(this.bunkers);
}
render() {
return (
<Fragment>
<div class="p-4 overflow-y-auto">
<h1 class="nl-title font-bold text-center text-2xl">{this.titleSignup}</h1>
<p class="nl-description font-light text-center text-sm pt-2 max-w-96 mx-auto">{this.description}</p>
</div>
<div class="max-w-72 mx-auto">
<div class="relative mb-2">
<input
onInput={e => this.handleInputChange(e)}
type="text"
class="nl-input peer py-3 px-4 ps-11 block w-full border-transparent rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none dark:border-transparent"
placeholder="Name"
value={state.nlSignup.signupName}
/>
<div class="absolute inset-y-0 start-0 flex items-center pointer-events-none ps-4 peer-disabled:opacity-50 peer-disabled:pointer-events-none">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke={this.isAvailable ? '#00cc00' : 'currentColor'}
class="flex-shrink-0 w-4 h-4 text-gray-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
/>
</svg>
</div>
</div>
{/* {inputStatus && (
<p class={classError}>{textError}</p>
)} */}
<div class="mb-2">
{/*<select class="nl-select border-transparent py-3 px-4 pe-9 block w-full rounded-lg text-sm disabled:opacity-50 disabled:pointer-events-none">*/}
{/* <option selected value="@nsec.app">*/}
{/* @nsec.app*/}
{/* </option>*/}
{/*</select>*/}
<nl-select onSelectDomain={e => this.handleDomainSelect(e)} selected={0} options={state.nlSignup.servers}></nl-select>
</div>
{/* <p class="nl-title font-light text-sm mb-2">Choose a service to manage your Nostr keys.</p> */}
<div class="ps-4 pe-4 overflow-y-auto">
<p class="nl-error font-light text-center text-sm max-w-96 mx-auto">{state.error}</p>
</div>
<button-base disabled={state.isLoading} onClick={e => this.handleCreateAccount(e)} titleBtn="Create profile">
{state.isLoading ? (
<span
slot="icon-start"
class="animate-spin-loading inline-block w-4 h-4 border-[3px] border-current border-t-transparent text-slate-900 dark:text-gray-300 rounded-full"
role="status"
aria-label="loading"
></span>
) : (
<svg slot="icon-start" style={{ display: 'none' }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
/>
</svg>
)}
</button-base>
</div>
</Fragment>
);
}
}

View File

@@ -0,0 +1,46 @@
# nl-signup
<!-- Auto Generated Below -->
## Properties
| Property | Attribute | Description | Type | Default |
| ------------- | -------------- | ----------- | -------- | ------------------------------------------------- |
| `bunkers` | `bunkers` | | `string` | `'nsec.app,highlighter.com'` |
| `description` | `description` | | `string` | `'Choose some username and a key store service.'` |
| `titleSignup` | `title-signup` | | `string` | `'Create keys with key store'` |
## Events
| Event | Description | Type |
| --------------- | ----------- | ---------------------- |
| `fetchHandler` | | `CustomEvent<boolean>` |
| `nlCheckSignup` | | `CustomEvent<string>` |
| `nlSignup` | | `CustomEvent<string>` |
## Dependencies
### Used by
- [nl-auth](../nl-auth)
### Depends on
- [nl-select](../nl-select)
- [button-base](../button-base)
### Graph
```mermaid
graph TD;
nl-signup --> nl-select
nl-signup --> button-base
nl-auth --> nl-signup
style nl-signup fill:#f9f,stroke:#333,stroke-width:4px
```
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -0,0 +1,3 @@
:host {
display: block;
}

Some files were not shown because too many files have changed in this diff Show More