feat: add delete alias functionality & code cleanup (settings)

This commit is contained in:
Danny Morabito 2024-12-04 13:17:09 +01:00
parent c9b6f02fd3
commit e959809a0e
Signed by: dannym
GPG key ID: 7CC8056A5A04557E
10 changed files with 553 additions and 271 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -15,17 +15,17 @@
"@iconify/svelte": "^4.0.2", "@iconify/svelte": "^4.0.2",
"@sveltejs/adapter-node": "^5.2.9", "@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/adapter-auto": "^3.3.1", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.8.5", "@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^4.0.2", "@sveltejs/vite-plugin-svelte": "^4.0.2",
"prettier": "^3.4.1", "prettier": "^3.4.1",
"prettier-plugin-svelte": "^3.3.2", "prettier-plugin-svelte": "^3.3.2",
"svelte": "^5.2.10", "svelte": "^5.2.11",
"svelte-check": "^4.1.0", "svelte-check": "^4.1.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^5.4.11" "vite": "^5.4.11"
}, },
"dependencies": { "dependencies": {
"@arx/utils": "git+ssh://git@git.arx-ccn.com:222/Arx/ts-utils", "@arx/utils": "git+ssh://git@git.arx-ccn.com:222/Arx/ts-utils#03163e9f4a07b011a28a8f97c90852ecfc806ddd",
"@gandlaf21/bc-ur": "^1.1.12", "@gandlaf21/bc-ur": "^1.1.12",
"@nostr-dev-kit/ndk": "^2.10.7", "@nostr-dev-kit/ndk": "^2.10.7",
"@nostr-dev-kit/ndk-cache-dexie": "^2.5.8", "@nostr-dev-kit/ndk-cache-dexie": "^2.5.8",

View file

@ -111,7 +111,7 @@
<Icon icon="ph:user" /> <Icon icon="ph:user" />
{/if} {/if}
</div> </div>
{#if npub !== displayString && !emailAddress} {#if npub !== displayString}
<Tooltip position="bottom" content={npub}> <Tooltip position="bottom" content={npub}>
<span class="user-text">{displayString}</span> <span class="user-text">{displayString}</span>
</Tooltip> </Tooltip>

244
src/lib/random.ts Normal file
View file

@ -0,0 +1,244 @@
export const adjectives = [
// Colors
'crimson',
'azure',
'golden',
'silver',
'emerald',
'violet',
'cobalt',
'scarlet',
'obsidian',
'jade',
'amber',
'coral',
'indigo',
'sapphire',
'ruby',
'onyx',
// Elements
'frost',
'flame',
'storm',
'thunder',
'crystal',
'shadow',
'lunar',
'solar',
'plasma',
'terra',
'aether',
'void',
'cosmic',
'astral',
'nebula',
'nova',
// Power
'mega',
'ultra',
'hyper',
'super',
'prime',
'apex',
'elite',
'omega',
'alpha',
'delta',
'sigma',
'gamma',
'beta',
'epsilon',
'zeta',
'theta',
// Tech
'cyber',
'techno',
'digital',
'binary',
'neural',
'crypto',
'matrix',
'vector',
'quantum',
'nano',
'laser',
'cyber',
'data',
'pixel',
'sonic',
'hyper',
// Mystical
'mystic',
'arcane',
'ethereal',
'divine',
'phantom',
'spirit',
'ancient',
'chaos',
'astral',
'eldritch',
'occult',
'mythic',
'sacred',
'cursed',
'blessed',
'doom',
// Nature
'savage',
'primal',
'feral',
'wild',
'fierce',
'rapid',
'swift',
'silent',
'deadly',
'stealth',
'shadow',
'night',
'dark',
'light',
'bright',
'dawn',
// Epic
'epic',
'legendary',
'mythic',
'eternal',
'immortal',
'infinite',
'supreme',
'ultimate',
'grand',
'mighty',
'noble',
'royal',
'heroic',
'valor',
'glory',
'honor'
];
export const animals = [
// Classic Predators
'wolf',
'tiger',
'lion',
'eagle',
'hawk',
'bear',
'panther',
'falcon',
'jaguar',
'leopard',
'lynx',
'cobra',
'viper',
'python',
'raptor',
'shark',
// Mythical Dragons
'dragon',
'wyvern',
'drake',
'wyrm',
'hydra',
'basilisk',
'tiamat',
'ryuu',
'fafnir',
'bahamut',
'ryu',
'draco',
'naga',
'ouroboros',
'lindworm',
'lung',
// Fantasy
'phoenix',
'griffin',
'unicorn',
'pegasus',
'chimera',
'manticore',
'sphinx',
'kraken',
'behemoth',
'leviathan',
'titan',
'giant',
'colossus',
'golem',
'gargoyle',
'djinn',
// Norse
'fenrir',
'jormungandr',
'sleipnir',
'valkyrie',
'einherjar',
'huginn',
'muninn',
'garmr',
'nidhogg',
'ratatoskr',
'hraesvelgr',
'gullinkambi',
'eikthyrnir',
'duneyrr',
'dvalinn',
'dainn',
// Eastern
'kitsune',
'kirin',
'byakko',
'suzaku',
'genbu',
'seiryu',
'oni',
'tengu',
'raiju',
'baku',
'nekomata',
'tanuki',
'kappa',
'tsukumogami',
'yokai',
'orochi',
// Ancient
'cyclops',
'minotaur',
'cerberus',
'scylla',
'typhon',
'charybdis',
'medusa',
'harpy',
'siren',
'gorgon',
'centaur',
'satyr',
'triton',
'echidna',
'lamia',
'sphinx',
// Cosmic
'nova',
'pulsar',
'quasar',
'nebula',
'vortex',
'cosmic',
'astral',
'celestial',
'starborn',
'solaris',
'lunaris',
'eclipse',
'meteor',
'comet',
'galaxy',
'cosmos'
];

View file

@ -9,18 +9,18 @@ const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'npub-email-cache' });
const nip07signer = new NDKNip07Signer(); const nip07signer = new NDKNip07Signer();
export const _ndk = new NDKSvelte({ export const _ndk = new NDKSvelte({
explicitRelayUrls: [ explicitRelayUrls: [
'wss://relay.primal.net', 'wss://relay.primal.net',
'wss://relay.damus.io', 'wss://relay.damus.io',
'wss://relay.nostr.band', 'wss://relay.nostr.band',
'wss://offchain.pub', 'wss://offchain.pub',
'wss://relay.snort.social' 'wss://relay.snort.social'
], ],
autoConnectUserRelays: true, autoConnectUserRelays: false,
relayAuthDefaultPolicy: async (r) => true, relayAuthDefaultPolicy: async (r) => true,
enableOutboxModel: true, enableOutboxModel: true,
// signer: nip07signer, // signer: nip07signer,
cacheAdapter: dexieAdapter cacheAdapter: dexieAdapter
}); });
if (browser && localStorage.getItem('useNip07')) _ndk.signer = nip07signer; if (browser && localStorage.getItem('useNip07')) _ndk.signer = nip07signer;
@ -33,42 +33,42 @@ if (browser && _ndk.activeUser) activeUser.set(_ndk.activeUser);
export const validSortOptions = ['stamps', 'date', 'sender', 'subject']; export const validSortOptions = ['stamps', 'date', 'sender', 'subject'];
export const baseHue = writable( export const baseHue = writable(
browser ? parseInt(<string>localStorage.getItem('baseHue')) || 200 : 200 browser ? parseInt(<string>localStorage.getItem('baseHue')) || 200 : 200
); );
export const groupByStamps = writable( export const groupByStamps = writable(
browser ? localStorage.getItem('groupByStamps') === 'true' : false browser ? localStorage.getItem('groupByStamps') === 'true' : false
); );
export const sortBy = writable(browser ? localStorage.getItem('sortBy') || 'date' : 'date'); export const sortBy = writable(browser ? localStorage.getItem('sortBy') || 'date' : 'date');
export const dateFormat = writable( export const dateFormat = writable(
browser ? localStorage.getItem('dateFormat') || 'y-m-d' : 'y-m-d' browser ? localStorage.getItem('dateFormat') || 'y-m-d' : 'y-m-d'
); );
export const timeFormat = writable( export const timeFormat = writable(
browser ? localStorage.getItem('timeFormat') || 'h:m:s' : 'h:m:s' browser ? localStorage.getItem('timeFormat') || 'h:m:s' : 'h:m:s'
); );
export let pageTitle = writable('loading'); export let pageTitle = writable('loading');
export let pageIcon = writable(''); export let pageIcon = writable('');
if (browser) { if (browser) {
baseHue.subscribe((value) => { baseHue.subscribe((value) => {
document.documentElement.style.setProperty('--base-hue', value.toString()); document.documentElement.style.setProperty('--base-hue', value.toString());
localStorage.setItem('baseHue', value.toString()); localStorage.setItem('baseHue', value.toString());
}); });
groupByStamps.subscribe((value) => { groupByStamps.subscribe((value) => {
localStorage.setItem('groupByStamps', value.toString()); localStorage.setItem('groupByStamps', value.toString());
}); });
sortBy.subscribe((value) => { sortBy.subscribe((value) => {
if (!validSortOptions.includes(value)) value = 'date'; if (!validSortOptions.includes(value)) value = 'date';
localStorage.setItem('sortBy', value); localStorage.setItem('sortBy', value);
}); });
dateFormat.subscribe((value) => { dateFormat.subscribe((value) => {
localStorage.setItem('dateFormat', value); localStorage.setItem('dateFormat', value);
}); });
timeFormat.subscribe((value) => { timeFormat.subscribe((value) => {
localStorage.setItem('timeFormat', value); localStorage.setItem('timeFormat', value);
}); });
} }

View file

@ -3,182 +3,22 @@
activeUser, activeUser,
baseHue, baseHue,
groupByStamps, groupByStamps,
ndk,
pageIcon, pageIcon,
pageTitle, pageTitle,
sortBy, sortBy,
validSortOptions validSortOptions
} from '$lib/stores.svelte'; } from '$lib/stores.svelte';
import ColorPicker from '../../components/ColorPicker.svelte';
import SettingsLine from '../../components/SettingsLine.svelte';
import Checkbox from '../../components/Checkbox.svelte';
import Select from '../../components/Select.svelte';
import DateFormatBuilder from '../../components/DateFormatBuilder.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import * as nip98 from 'nostr-tools/nip98'; import Checkbox from '../../components/Checkbox.svelte';
import { NDKEvent, type NostrEvent } from '@nostr-dev-kit/ndk'; import ColorPicker from '../../components/ColorPicker.svelte';
import { PUBLIC_API_BASE_URL } from '$env/static/public'; import DateFormatBuilder from '../../components/DateFormatBuilder.svelte';
import TimeCountdown from '../../components/TimeCountdown.svelte'; import Select from '../../components/Select.svelte';
import { TokenInfoWithMailSubscriptionDuration } from '@arx/utils'; import SettingsLine from './SettingsLine.svelte';
import SubscriptionSettings from './SubscriptionSettings.svelte';
let subscribed = $state(false);
let hasUnlimitedSubscription = $state(false);
let subscriptionTill = $state(new Date(0));
let aliases = $state([]);
let newAlias = $state('');
let cashuTokenForBuy = $state('');
let tokenInfo = $state();
let tokenInfoError = $state('');
let buyTimeForNpub = $state('');
$effect(() => {
try {
tokenInfo = new TokenInfoWithMailSubscriptionDuration(cashuTokenForBuy);
tokenInfoError = '';
} catch (e) {
tokenInfoError = (e as Error).message;
}
});
async function buyTime() {
if (!tokenInfo) return;
if (tokenInfoError) return;
if (!buyTimeForNpub) return;
try {
const response = await fetch(PUBLIC_API_BASE_URL + '/addTime/' + buyTimeForNpub, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tokenString: cashuTokenForBuy
})
});
if (!response.ok) throw new Error(await response.json().then(r => r.message));
reloadSubscriptionStatus();
} catch (e) {
alert(e);
}
}
async function reloadSubscriptionStatus() {
const subscriptionStatusURL = PUBLIC_API_BASE_URL + '/subscription/' + $ndk.activeUser.npub;
const subscriptionStatus = await fetch(subscriptionStatusURL).then(r => r.json<{
subscribed: boolean,
subscribedUntil: number | null
}>());
if (subscriptionStatus.subscribed) {
subscribed = true;
if (subscriptionStatus.subscribedUntil == null)
hasUnlimitedSubscription = true;
else {
subscriptionTill = new Date(subscriptionStatus.subscribedUntil * 1000);
}
}
}
async function reloadAliases() {
const aliasesURL = PUBLIC_API_BASE_URL + '/aliases/' + $ndk.activeUser.npub;
const auth = await nip98.getToken(aliasesURL, 'post', async (e: NostrEvent) => {
const event = new NDKEvent($ndk, e);
await event.sign();
return event.rawEvent();
},
true
);
aliases = await fetch(aliasesURL, {
headers: {
Authorization: auth
}
}).then(r => r.json());
}
async function addAlias() {
const addAliasURL = PUBLIC_API_BASE_URL + '/addAlias';
const auth = await nip98.getToken(addAliasURL, 'post', async (e: NostrEvent) => {
const event = new NDKEvent($ndk, e);
await event.sign();
return event.rawEvent();
},
true
);
await fetch(addAliasURL, {
method: 'POST',
headers: {
Authorization: auth,
'Content-Type': 'application/json'
},
body: JSON.stringify({
alias: newAlias
})
}).then(r => r.json()).then(console.log);
newAlias = '';
await reloadAliases();
alert('Alias added');
}
function randomAlias() {
const adjectives = [
// Colors
'crimson', 'azure', 'golden', 'silver', 'emerald', 'violet', 'cobalt', 'scarlet',
'obsidian', 'jade', 'amber', 'coral', 'indigo', 'sapphire', 'ruby', 'onyx',
// Elements
'frost', 'flame', 'storm', 'thunder', 'crystal', 'shadow', 'lunar', 'solar',
'plasma', 'terra', 'aether', 'void', 'cosmic', 'astral', 'nebula', 'nova',
// Power
'mega', 'ultra', 'hyper', 'super', 'prime', 'apex', 'elite', 'omega',
'alpha', 'delta', 'sigma', 'gamma', 'beta', 'epsilon', 'zeta', 'theta',
// Tech
'cyber', 'techno', 'digital', 'binary', 'neural', 'crypto', 'matrix', 'vector',
'quantum', 'nano', 'laser', 'cyber', 'data', 'pixel', 'sonic', 'hyper',
// Mystical
'mystic', 'arcane', 'ethereal', 'divine', 'phantom', 'spirit', 'ancient', 'chaos',
'astral', 'eldritch', 'occult', 'mythic', 'sacred', 'cursed', 'blessed', 'doom',
// Nature
'savage', 'primal', 'feral', 'wild', 'fierce', 'rapid', 'swift', 'silent',
'deadly', 'stealth', 'shadow', 'night', 'dark', 'light', 'bright', 'dawn',
// Epic
'epic', 'legendary', 'mythic', 'eternal', 'immortal', 'infinite', 'supreme', 'ultimate',
'grand', 'mighty', 'noble', 'royal', 'heroic', 'valor', 'glory', 'honor'
];
const animals = [
// Classic Predators
'wolf', 'tiger', 'lion', 'eagle', 'hawk', 'bear', 'panther', 'falcon',
'jaguar', 'leopard', 'lynx', 'cobra', 'viper', 'python', 'raptor', 'shark',
// Mythical Dragons
'dragon', 'wyvern', 'drake', 'wyrm', 'hydra', 'basilisk', 'tiamat', 'ryuu',
'fafnir', 'bahamut', 'ryu', 'draco', 'naga', 'ouroboros', 'lindworm', 'lung',
// Fantasy
'phoenix', 'griffin', 'unicorn', 'pegasus', 'chimera', 'manticore', 'sphinx', 'kraken',
'behemoth', 'leviathan', 'titan', 'giant', 'colossus', 'golem', 'gargoyle', 'djinn',
// Norse
'fenrir', 'jormungandr', 'sleipnir', 'valkyrie', 'einherjar', 'huginn', 'muninn', 'garmr',
'nidhogg', 'ratatoskr', 'hraesvelgr', 'gullinkambi', 'eikthyrnir', 'duneyrr', 'dvalinn', 'dainn',
// Eastern
'kitsune', 'kirin', 'byakko', 'suzaku', 'genbu', 'seiryu', 'oni', 'tengu',
'raiju', 'baku', 'nekomata', 'tanuki', 'kappa', 'tsukumogami', 'yokai', 'orochi',
// Ancient
'cyclops', 'minotaur', 'cerberus', 'scylla', 'typhon', 'charybdis', 'medusa', 'harpy',
'siren', 'gorgon', 'centaur', 'satyr', 'triton', 'echidna', 'lamia', 'sphinx',
// Cosmic
'nova', 'pulsar', 'quasar', 'nebula', 'vortex', 'cosmic', 'astral', 'celestial',
'starborn', 'solaris', 'lunaris', 'eclipse', 'meteor', 'comet', 'galaxy', 'cosmos'
];
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const animal = animals[Math.floor(Math.random() * animals.length)];
const digits = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
newAlias = `${adjective}-${animal}${digits}`;
}
onMount(async () => { onMount(async () => {
$pageTitle = 'Settings'; $pageTitle = 'Settings';
$pageIcon = 'si:settings-cute-line'; $pageIcon = 'si:settings-cute-line';
await reloadAliases();
await reloadSubscriptionStatus();
}); });
</script> </script>
@ -187,78 +27,19 @@
</SettingsLine> </SettingsLine>
<SettingsLine title="Email Alias Time Blocks"> <SettingsLine title="Email Alias Time Blocks">
<p> <SubscriptionSettings />
npub.email offers email aliases that connect to your nostr account, converting incoming emails into Letters.<br />
These aliases can also serve as your nip 05 identifier.
</p>
<h3>Pricing:</h3>
<ul>
<li><b>210 sats</b> per day</li>
<li>Minimum purchase: <b>21 sats</b> (2.4 hours)</li>
<li>Flexible duration: Purchase any length of time you need</li>
</ul>
<p>
Purchase time blocks to activate your email alias service for yourself or gift them to another user. Once the time
expires, you'll need to purchase additional time to continue using the service. Note: emails received while the
service is inactive will not be processed.
</p>
<SettingsLine title="Available time">
{#if hasUnlimitedSubscription}
<h4 style:text-align="center" style:width="100%">You are officially awesome!</h4>
<p>
The Arx team has granted you an unlimited subscription to npub.email for your valuable contributions to Arx,
nostr or bitcoin. <br />
Keep up your great work and thank you!
</p>
{:else if subscribed && subscriptionTill.getTime() > 1000}
Your subscription will end in: <br />
<TimeCountdown bind:time={subscriptionTill} />
{:else}
You are not currently subscribed to npub.email
{/if}
</SettingsLine>
<SettingsLine title="Buy time">
{#if cashuTokenForBuy !== ''}
{#if tokenInfoError}
<p class="error">{tokenInfoError}</p>
{:else}
<h4>{tokenInfo.amount} sats</h4>
<TimeCountdown time={new Date(Date.now() + tokenInfo.duration * 1000)} isStopped />
{/if}
{/if}
<input bind:value={cashuTokenForBuy} placeholder="Enter cashu token" type="text" />
<h4>Buy for:</h4>
<input bind:value={buyTimeForNpub} placeholder="Enter npub" type="text" />
<button onclick={() => buyTimeForNpub = $ndk.activeUser.npub}>Set to your own</button>
<button onclick={buyTime}>Buy</button>
</SettingsLine>
{#if subscribed}
<SettingsLine title="Your aliases">
{#each aliases as alias}
<code>{alias}@npub.email</code>
{:else}
<p>No aliases yet</p>
{/each}
<input bind:value={newAlias} placeholder="Enter alias" type="text" />
<button onclick={randomAlias}>Random</button>
<button onclick={addAlias}>Add</button>
</SettingsLine>
{/if}
</SettingsLine> </SettingsLine>
<SettingsLine title="Sorting and Grouping"> <SettingsLine title="Sorting and Grouping">
<Checkbox bind:checked={$groupByStamps} label="Group by stamps" /> <Checkbox bind:checked={$groupByStamps} label="Group by stamps" />
<Select bind:value={$sortBy} label="Sort by" <Select
options={validSortOptions.map(o => ({ value: o, label: o.charAt(0).toUpperCase() + o.slice(1).toLowerCase() }))} /> bind:value={$sortBy}
label="Sort by"
options={validSortOptions.map((o) => ({
value: o,
label: o.charAt(0).toUpperCase() + o.slice(1).toLowerCase()
}))}
/>
</SettingsLine> </SettingsLine>
<SettingsLine title="Date and Time"> <SettingsLine title="Date and Time">

View file

@ -0,0 +1,124 @@
<script lang="ts">
import { PUBLIC_API_BASE_URL } from '$env/static/public';
import * as nip98 from 'nostr-tools/nip98';
import { NDKEvent, type NostrEvent } from '@nostr-dev-kit/ndk';
import { adjectives, animals } from '$lib/random';
import { onMount } from 'svelte';
import { ndk } from '$lib/stores.svelte';
import SettingsLine from './SettingsLine.svelte';
import IconButton from '../../components/IconButton.svelte';
async function reloadAliases() {
const aliasesURL = PUBLIC_API_BASE_URL + '/aliases/' + $ndk.activeUser.npub;
const auth = await nip98.getToken(
aliasesURL,
'post',
async (e: NostrEvent) => {
const event = new NDKEvent($ndk, e);
await event.sign();
return event.rawEvent();
},
true
);
aliases = await fetch(aliasesURL, {
headers: {
Authorization: auth
}
}).then((r) => r.json());
}
async function addAlias() {
const addAliasURL = PUBLIC_API_BASE_URL + '/addAlias';
const auth = await nip98.getToken(
addAliasURL,
'post',
async (e: NostrEvent) => {
const event = new NDKEvent($ndk, e);
await event.sign();
return event.rawEvent();
},
true
);
await fetch(addAliasURL, {
method: 'POST',
headers: {
Authorization: auth,
'Content-Type': 'application/json'
},
body: JSON.stringify({
alias: newAlias
})
})
.then((r) => r.json())
.then(console.log);
newAlias = '';
await reloadAliases();
alert('Alias added');
}
async function deleteAlias(alias: string) {
if (!confirm(`Delete ${alias}?`)) return;
const deleteAliasURL = PUBLIC_API_BASE_URL + '/alias/' + alias;
const auth = await nip98.getToken(
deleteAliasURL,
'DELETE',
async (e: NostrEvent) => {
const event = new NDKEvent($ndk, e);
await event.sign();
return event.rawEvent();
},
true
);
await fetch(deleteAliasURL, {
method: 'DELETE',
headers: {
Authorization: auth
}
})
.then((r) => r.json())
.then(console.log);
await reloadAliases();
}
function randomAlias() {
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const animal = animals[Math.floor(Math.random() * animals.length)];
const digits = Math.floor(Math.random() * 10000)
.toString()
.padStart(4, '0');
newAlias = `${adjective}-${animal}${digits}`;
}
onMount(reloadAliases);
let aliases = $state([]);
let newAlias = $state('');
</script>
<SettingsLine title="Your aliases">
<table>
<tbody>
{#each aliases as alias}
<tr>
<td>
<code>{alias}@npub.email</code>
</td>
<td>
<IconButton
icon="icon-park-twotone:delete"
text="Delete"
onclick={() => deleteAlias(alias)}
/>
</td>
</tr>
{:else}
<tr><td>No aliases yet</td></tr>
{/each}
</tbody>
</table>
<input bind:value={newAlias} placeholder="Enter alias" type="text" />
<button onclick={randomAlias}>Random</button>
<button onclick={addAlias}>Add</button>
</SettingsLine>

View file

@ -0,0 +1,62 @@
<script lang="ts">
import { PUBLIC_API_BASE_URL } from '$env/static/public';
import { TokenInfoWithMailSubscriptionDuration } from '@arx/utils';
import TimeCountdown from '../../components/TimeCountdown.svelte';
import { ndk } from '$lib/stores.svelte';
let { onReloadNeeded } = $props<{
onReloadNeeded: () => void;
}>();
let cashuTokenForBuy = $state('');
let tokenInfo = $state<TokenInfoWithMailSubscriptionDuration | null>(null);
let tokenInfoError = $state('');
let buyTimeForNpub = $state('');
async function buyTime() {
if (!tokenInfo) return;
if (tokenInfoError) return;
if (!buyTimeForNpub) return;
try {
const response = await fetch(PUBLIC_API_BASE_URL + '/addTime/' + buyTimeForNpub, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tokenString: cashuTokenForBuy
})
});
if (!response.ok) throw new Error(await response.json().then((r) => r.message));
onReloadNeeded();
alert(`Time added!`);
} catch (e) {
alert(e);
}
}
$effect(() => {
try {
tokenInfo = new TokenInfoWithMailSubscriptionDuration(cashuTokenForBuy);
tokenInfoError = '';
} catch (e) {
tokenInfoError = (e as Error).message;
}
});
</script>
{#if cashuTokenForBuy !== ''}
{#if tokenInfoError}
<p class="error">{tokenInfoError}</p>
{:else if tokenInfo}
<h4>{tokenInfo.amount} sats</h4>
<TimeCountdown time={new Date(Date.now() + tokenInfo.duration * 1000)} isStopped />
{/if}
{/if}
<input bind:value={cashuTokenForBuy} placeholder="Enter cashu token" type="text" />
<h4>Buy for:</h4>
<input bind:value={buyTimeForNpub} placeholder="Enter npub" type="text" />
<button onclick={() => (buyTimeForNpub = $ndk.activeUser.npub)}>Set to your own</button>
<button onclick={buyTime}>Buy</button>

View file

@ -0,0 +1,71 @@
<script lang="ts">
import { onMount } from 'svelte';
import SettingsLine from './SettingsLine.svelte';
import TimeCountdown from '../../components/TimeCountdown.svelte';
import AliasSettings from './AliasSettings.svelte';
import BuyTime from './BuyTime.svelte';
import { PUBLIC_API_BASE_URL } from '$env/static/public';
import { ndk } from '$lib/stores.svelte';
let subscribed = $state(false);
let hasUnlimitedSubscription = $state(false);
let subscriptionTill = $state(new Date(0));
async function reloadSubscriptionStatus() {
const subscriptionStatusURL = PUBLIC_API_BASE_URL + '/subscription/' + $ndk.activeUser.npub;
const subscriptionStatus = await fetch(subscriptionStatusURL).then((r) => r.json());
if (subscriptionStatus.subscribed) {
subscribed = true;
if (subscriptionStatus.subscribedUntil == null) hasUnlimitedSubscription = true;
else {
subscriptionTill = new Date(subscriptionStatus.subscribedUntil * 1000);
}
}
}
onMount(reloadSubscriptionStatus);
</script>
<p>
npub.email offers email aliases that connect to your nostr account, converting incoming emails
into Letters.<br />
These aliases can also serve as your nip 05 identifier.
</p>
<h3>Pricing:</h3>
<ul>
<li><b>210 sats</b> per day</li>
<li>Minimum purchase: <b>21 sats</b> (2.4 hours)</li>
<li>Flexible duration: Purchase any length of time you need</li>
</ul>
<p>
Purchase time blocks to activate your email alias service for yourself or gift them to another
user. Once the time expires, you'll need to purchase additional time to continue using the
service. Note: emails received while the service is inactive will not be processed.
</p>
<SettingsLine title="Available time">
{#if hasUnlimitedSubscription}
<h4 style:text-align="center" style:width="100%">You are officially awesome!</h4>
<p>
The Arx team has granted you an unlimited subscription to npub.email for your valuable
contributions to Arx, nostr or bitcoin. <br />
Keep up your great work and thank you!
</p>
{:else if subscribed && subscriptionTill.getTime() > 1000}
Your subscription will end in: <br />
<TimeCountdown bind:time={subscriptionTill} />
{:else}
You are not currently subscribed to npub.email
{/if}
</SettingsLine>
<SettingsLine title="Buy time">
<BuyTime onReloadNeeded={reloadSubscriptionStatus} />
</SettingsLine>
{#if subscribed}
<AliasSettings />
{/if}