From f309b2bfc03eaa5b4584d1ff3551e3c5c73ab309 Mon Sep 17 00:00:00 2001 From: Bojan Mojsilovic Date: Mon, 10 Jun 2024 13:22:57 +0200 Subject: [PATCH] Handle USD subscriptions --- .../AuthorSubscribe/AuthorSubscribe.tsx | 31 +++++- .../SubscribeToAuthorModal.tsx | 105 ++++++++++++++++-- src/lib/membership.ts | 36 ++++++ src/lib/zap.ts | 7 +- 4 files changed, 169 insertions(+), 10 deletions(-) diff --git a/src/components/AuthorSubscribe/AuthorSubscribe.tsx b/src/components/AuthorSubscribe/AuthorSubscribe.tsx index 641876d..94da6c8 100644 --- a/src/components/AuthorSubscribe/AuthorSubscribe.tsx +++ b/src/components/AuthorSubscribe/AuthorSubscribe.tsx @@ -53,11 +53,13 @@ const AuthoreSubscribe: Component<{ getAuthorData(); }); - const doSubscription = async (tier: Tier, cost: TierCost) => { + const doSubscription = async (tier: Tier, cost: TierCost, exchangeRate?: Record>) => { const a = author(); if (!a || !account || !cost) return; + if (cost.unit === 'USD' && (!exchangeRate || !exchangeRate['USD'])) return; + const subEvent = { kind: Kind.Subscribe, content: '', @@ -72,13 +74,38 @@ 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); }; diff --git a/src/components/SubscribeToAuthorModal/SubscribeToAuthorModal.tsx b/src/components/SubscribeToAuthorModal/SubscribeToAuthorModal.tsx index d2b4276..4f3cfe2 100644 --- a/src/components/SubscribeToAuthorModal/SubscribeToAuthorModal.tsx +++ b/src/components/SubscribeToAuthorModal/SubscribeToAuthorModal.tsx @@ -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>, } -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>) => void, }> = (props) => { + const account = useAccountContext(); + const [store, updateStore] = createStore({ 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 (
@@ -277,7 +368,7 @@ const SubscribeToAuthorModal: Component<{
store.selectedTier && store.selectedCost && props.onSubscribe(store.selectedTier, store.selectedCost)} + onClick={() => store.selectedTier && store.selectedCost && props.onSubscribe(store.selectedTier, store.selectedCost, store.exchangeRate)} > subscribe diff --git a/src/lib/membership.ts b/src/lib/membership.ts index 497e585..279f127 100644 --- a/src/lib/membership.ts +++ b/src/lib/membership.ts @@ -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'); + } +} diff --git a/src/lib/zap.ts b/src/lib/zap.ts index 47f956e..73b88de 100644 --- a/src/lib/zap.ts +++ b/src/lib/zap.ts @@ -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>) => { 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,