Handle USD subscriptions

This commit is contained in:
Bojan Mojsilovic 2024-06-10 13:22:57 +02:00
parent 40bc2ec8a7
commit f309b2bfc0
4 changed files with 169 additions and 10 deletions

View File

@ -53,11 +53,13 @@ const AuthoreSubscribe: Component<{
getAuthorData();
});
const doSubscription = async (tier: Tier, cost: TierCost) => {
const doSubscription = async (tier: Tier, cost: TierCost, exchangeRate?: Record<string, Record<string, number>>) => {
const a = author();
if (!a || !account || !cost) return;
if (cost.unit === 'USD' && (!exchangeRate || !exchangeRate['USD'])) return;
const subEvent = {
kind: Kind.Subscribe,
content: '',
@ -72,12 +74,37 @@ const AuthoreSubscribe: Component<{
],
}
const { success, note } = await sendEvent(subEvent, account.relays, account.relaySettings);
if (success && note) {
await zapSubscription(note, a, account.publicKey, account.relays);
const isZapped = await zapSubscription(note, a, account.publicKey, account.relays, exchangeRate);
if (!isZapped) {
unsubscribe(note.id);
}
}
}
const unsubscribe = async (eventId: string) => {
const a = author();
if (!a || !account) return;
const unsubEvent = {
kind: Kind.Unsubscribe,
content: '',
created_at: Math.floor((new Date()).getTime() / 1_000),
tags: [
['p', a.pubkey],
['e', eventId],
],
};
await sendEvent(unsubEvent, account.relays, account.relaySettings);
}
const openSubscribe = () => {
app?.actions.openAuthorSubscribeModal(author(), doSubscription);

View File

@ -13,11 +13,17 @@ import { userName } from '../../stores/profile';
import Avatar from '../Avatar/Avatar';
import VerificationCheck from '../VerificationCheck/VerificationCheck';
import { APP_ID } from '../../App';
import { subsTo } from '../../sockets';
import { subsTo, subTo } from '../../sockets';
import { getAuthorSubscriptionTiers } from '../../lib/feed';
import ButtonSecondary from '../Buttons/ButtonSecondary';
import { Select } from '@kobalte/core';
import Loader from '../Loader/Loader';
import { logInfo } from '../../lib/logger';
import { getExchangeRate, getMembershipStatus } from '../../lib/membership';
import { useAccountContext } from '../../contexts/AccountContext';
export const satsInBTC = 100_000_000;
export type TierCost = {
amount: string,
@ -42,23 +48,29 @@ export type TierStore = {
selectedTier: Tier | undefined,
selectedCost: TierCost | undefined,
isFetchingTiers: boolean,
exchangeRate: Record<string, Record<string, number>>,
}
export const payUnits = ['sats', 'msat', ''];
export const payUnits = ['sats', 'sat', 'msat', 'msats', 'USD', 'usd', ''];
const SubscribeToAuthorModal: Component<{
id?: string,
author: PrimalUser | undefined,
onClose: () => void,
onSubscribe: (tier: Tier, cost: TierCost) => void,
onSubscribe: (tier: Tier, cost: TierCost, exchangeRate?: Record<string, Record<string, number>>) => void,
}> = (props) => {
const account = useAccountContext();
const [store, updateStore] = createStore<TierStore>({
tiers: [],
selectedTier: undefined,
selectedCost: undefined,
isFetchingTiers: false,
})
exchangeRate: {},
});
let walletSocket: WebSocket | undefined;
createEffect(() => {
const author = props.author;
@ -68,6 +80,55 @@ const SubscribeToAuthorModal: Component<{
}
});
createEffect(() => {
if (props.author && (!walletSocket || walletSocket.readyState === WebSocket.CLOSED)) {
openWalletSocket(() => {
if (!walletSocket || walletSocket.readyState !== WebSocket.OPEN) return;
const subId = `er_${APP_ID}`;
const unsub = subTo(walletSocket, subId, (type, _, content) => {
if (type === 'EVENT') {
const response: { rate: string } = JSON.parse(content?.content || '{ "rate": 1 }');
const BTCForTarget = parseFloat(response.rate) || 1;
const satsToTarget = BTCForTarget / satsInBTC;
const targetToBTC = 1 / BTCForTarget;
const targetToSats = 1 / satsToTarget;
updateStore('exchangeRate', () => ({
USD: {
sats: targetToSats,
BTC: targetToBTC,
USD: 1,
},
sats: {
sats: 1,
USD: satsToTarget,
BTC: 1 / satsInBTC,
},
BTC: {
sats: satsInBTC,
USD: BTCForTarget,
BTC: 1,
}
}));
}
if (type === 'EOSE') {
unsub();
walletSocket?.close();
}
});
getExchangeRate(account?.publicKey, subId, "USD", walletSocket);
});
} else {
walletSocket?.close();
}
})
const getTiers = (author: PrimalUser) => {
if (!author) return;
@ -84,7 +145,7 @@ const SubscribeToAuthorModal: Component<{
if (content.kind === Kind.Tier) {
const t = content as NostrTier;
const costs = t.tags?.filter((t: string[]) => t[0] === 'amount').map((t: string[]) => (
let costs = t.tags?.filter((t: string[]) => t[0] === 'amount').map((t: string[]) => (
{
amount: t[1],
unit: t[2],
@ -137,9 +198,39 @@ const SubscribeToAuthorModal: Component<{
}
const displayCost = (cost: TierCost | undefined) => {
return `${cost?.unit === 'msat' ? Math.ceil(parseInt(cost?.amount || '0') / 1_000) : cost?.amount} sats`;
let text = '';
switch(cost?.unit) {
case 'msat':
case 'msats':
case '':
text = `${Math.ceil(parseInt(cost?.amount || '0') / 1_000)} sats`;
break;
case 'sats':
case 'sat':
text = `${cost.amount} sats`;
break;
case 'USD':
case 'usd':
text = `${cost.amount} USD`;
}
return text;
};
const openWalletSocket = (onOpen: () => void) => {
walletSocket = new WebSocket('wss://wallet.primal.net/v1');
walletSocket.addEventListener('close', () => {
logInfo('WALLET SOCKET CLOSED');
});
walletSocket.addEventListener('open', () => {
logInfo('WALLET SOCKET OPENED');
onOpen();
});
}
return (
<Modal open={props.author !== undefined} onClose={props.onClose}>
<div id={props.id} class={styles.subscribeToAuthor}>
@ -277,7 +368,7 @@ const SubscribeToAuthorModal: Component<{
<Show when={store.selectedTier}>
<div class={styles.payAction}>
<ButtonPrimary
onClick={() => store.selectedTier && store.selectedCost && props.onSubscribe(store.selectedTier, store.selectedCost)}
onClick={() => store.selectedTier && store.selectedCost && props.onSubscribe(store.selectedTier, store.selectedCost, store.exchangeRate)}
>
subscribe
</ButtonPrimary>

View File

@ -36,3 +36,39 @@ export const getMembershipStatus = async (pubkey: string | undefined, subId: str
return false;
}
}
export const getExchangeRate = async (pubkey: string | undefined, subId: string, currency: string, socket: WebSocket) => {
if (!pubkey) {
return;
}
const content = JSON.stringify(
["exchange_rate", { target_currency: currency }],
);
const event = {
content,
kind: Kind.WALLET_OPERATION,
created_at: Math.ceil((new Date()).getTime() / 1000),
tags: [],
};
const signedEvent = await signEvent(event);
const message = JSON.stringify([
"REQ",
subId,
{cache: ["wallet", { operation_event: signedEvent }]},
]);
if (socket) {
const e = new CustomEvent('send', { detail: { message, ws: socket }});
socket.send(message);
socket.dispatchEvent(e);
} else {
throw('no_socket');
}
}

View File

@ -140,7 +140,7 @@ export const zapProfile = async (profile: PrimalUser, sender: string | undefined
}
}
export const zapSubscription = async (subEvent: NostrRelaySignedEvent, recipient: PrimalUser, sender: string | undefined, relays: Relay[]) => {
export const zapSubscription = async (subEvent: NostrRelaySignedEvent, recipient: PrimalUser, sender: string | undefined, relays: Relay[], exchangeRate?: Record<string, Record<string, number>>) => {
if (!sender || !recipient) {
return false;
}
@ -164,6 +164,11 @@ export const zapSubscription = async (subEvent: NostrRelaySignedEvent, recipient
sats = parseInt(costTag[1]);
}
if (costTag[2] === 'USD' && exchangeRate && exchangeRate['USD']) {
let usd = parseFloat(costTag[1]);
sats = Math.ceil(exchangeRate['USD'].sats * usd * 1_000);
}
let payload = {
profile: recipient.pubkey,
event: subEvent.id,