From 7307bafbf5bf6fabd4d7937cb4f97b86ec769d70 Mon Sep 17 00:00:00 2001 From: Bojan Mojsilovic Date: Fri, 8 Mar 2024 14:05:41 +0100 Subject: [PATCH] Check memebership status when logging in --- src/components/Uploader/Uploader.tsx | 22 ++++------ src/contexts/AccountContext.tsx | 64 ++++++++++++++++++++++++++-- src/lib/membership.ts | 38 +++++++++++++++++ src/sockets.tsx | 18 ++++++++ src/types/primal.d.ts | 12 ++++++ 5 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 src/lib/membership.ts diff --git a/src/components/Uploader/Uploader.tsx b/src/components/Uploader/Uploader.tsx index d534638..a274dd9 100644 --- a/src/components/Uploader/Uploader.tsx +++ b/src/components/Uploader/Uploader.tsx @@ -3,14 +3,14 @@ import { Progress } from '@kobalte/core'; import styles from './Uploader.module.scss'; import { uploadServer } from '../../uploadSocket'; -import { createStore, reconcile } from 'solid-js/store'; +import { createStore } from 'solid-js/store'; import { NostrEOSE, NostrEvent, NostrEventContent, NostrEventType, NostrMediaUploaded } from '../../types/primal'; import { readUploadTime, saveUploadTime } from '../../lib/localStore'; import { startTimes, uploadMediaCancel, uploadMediaChunk, uploadMediaConfirm } from '../../lib/media'; import { sha256, uuidv4 } from '../../utils'; import { Kind, uploadLimit } from '../../constants'; import ButtonGhost from '../Buttons/ButtonGhost'; -import { isAccountVerified } from '../../lib/profile'; +import { useAccountContext } from '../../contexts/AccountContext'; const MB = 1024 * 1024; const maxParallelChunks = 5; @@ -44,6 +44,7 @@ const Uploader: Component<{ onCancel?: () => void, onSuccsess?: (url: string) => void, }> = (props) => { + const account = useAccountContext(); const [uploadState, setUploadState] = createStore({ isUploading: false, @@ -113,7 +114,7 @@ const Uploader: Component<{ }); createEffect(() => { - calcUploadLimit(props.nip05); + calcUploadLimit(account?.membershipStatus.tier); }); onCleanup(() => { @@ -127,21 +128,14 @@ const Uploader: Component<{ } }); - const calcUploadLimit = (nip05: string | undefined) => { + const calcUploadLimit = (membershipTier: string | undefined) => { - if (!nip05) { - setUploadState('uploadLimit', () => uploadLimit.regular); + if (membershipTier === 'premium') { + setUploadState('uploadLimit', () => uploadLimit.premium); return; } - isAccountVerified(nip05).then(profile => { - if (profile && profile.pubkey === props.publicKey && nip05.endsWith && nip05.endsWith('primal.net')) { - setUploadState('uploadLimit', () => uploadLimit.premium); - return; - } - - setUploadState('uploadLimit', () => uploadLimit.regular); - }); + setUploadState('uploadLimit', () => uploadLimit.regular); }; const subTo = (socket: WebSocket, subId: string, cb: (type: NostrEventType, subId: string, content?: NostrEventContent) => void ) => { diff --git a/src/contexts/AccountContext.tsx b/src/contexts/AccountContext.tsx index 67a976e..abfc980 100644 --- a/src/contexts/AccountContext.tsx +++ b/src/contexts/AccountContext.tsx @@ -17,11 +17,12 @@ import { NostrMutedContent, NostrRelays, NostrWindow, + MembershipStatus, PrimalNote, PrimalUser, } from '../types/primal'; import { Kind, pinEncodePrefix, relayConnectingTimeout } from "../constants"; -import { isConnected, refreshSocketListeners, removeSocketListeners, socket, subscribeTo, reset } from "../sockets"; +import { isConnected, refreshSocketListeners, removeSocketListeners, socket, subscribeTo, reset, subTo } from "../sockets"; import { sendContacts, sendLike, sendMuteList, triggerImportEvents } from "../lib/notes"; // @ts-ignore Bad types in nostr-tools import { generatePrivateKey, Relay, getPublicKey as nostrGetPubkey, nip19 } from "nostr-tools"; @@ -38,6 +39,7 @@ import { logError, logInfo, logWarning } from "../lib/logger"; import { useToastContext } from "../components/Toaster/Toaster"; import { useIntl } from "@cookbook/solid-intl"; import { account as tAccount } from "../translations"; +import { getMembershipStatus } from "../lib/membership"; export type AccountContextStore = { likes: string[], @@ -68,6 +70,7 @@ export type AccountContextStore = { showGettingStarted: boolean, showLogin: boolean, emojiHistory: EmojiOption[], + membershipStatus: MembershipStatus, actions: { showNewNoteForm: () => void, hideNewNoteForm: () => void, @@ -126,6 +129,7 @@ const initialData = { showGettingStarted: false, showLogin: false, emojiHistory: [], + membershipStatus: {}, }; export const AccountContext = createContext(); @@ -143,6 +147,8 @@ export function AccountProvider(props: { children: JSXElement }) { let connectedRelaysCopy: Relay[] = []; + let membershipSocket: WebSocket | undefined; + onMount(() => { setInterval(() => { checkNostrChange(); @@ -185,6 +191,49 @@ export function AccountProvider(props: { children: JSXElement }) { } }; + const openMembershipSocket = (onOpen: () => void) => { + membershipSocket = new WebSocket('wss://wallet.primal.net/v1'); + + membershipSocket.addEventListener('close', () => { + console.log('PREMIUM SOCKET CLOSED'); + }); + + membershipSocket.addEventListener('open', () => { + console.log('PREMIUM SOCKET OPENED'); + onOpen(); + }); + } + + const checkMembershipStatus = () => { + openMembershipSocket(() => { + if (!membershipSocket || membershipSocket.readyState !== WebSocket.OPEN) return; + + const subId = `ps_${APP_ID}`; + + let gotEvent = false; + + const unsub = subTo(membershipSocket, subId, (type, _, content) => { + if (type === 'EVENT') { + const status: MembershipStatus = JSON.parse(content?.content || '{}'); + + gotEvent = true; + updateStore('membershipStatus', () => ({ ...status })); + } + + if (type === 'EOSE') { + unsub(); + membershipSocket?.close(); + + if (!gotEvent) { + updateStore('membershipStatus', () => ({ tier: 'none' })); + } + } + }); + + getMembershipStatus(store.publicKey, subId, membershipSocket); + }); + }; + const showGetStarted = () => { updateStore('showGettingStarted', () => true); } @@ -227,8 +276,17 @@ export function AccountProvider(props: { children: JSXElement }) { } const setPublicKey = (pubkey: string | undefined) => { - updateStore('publicKey', () => pubkey); - pubkey ? localStorage.setItem('pubkey', pubkey) : localStorage.removeItem('pubkey'); + + if(pubkey && pubkey.length > 0) { + updateStore('publicKey', () => pubkey); + localStorage.setItem('pubkey', pubkey); + checkMembershipStatus(); + } + else { + updateStore('publicKey', () => undefined); + localStorage.removeItem('pubkey'); + } + updateStore('isKeyLookupDone', () => true); }; diff --git a/src/lib/membership.ts b/src/lib/membership.ts new file mode 100644 index 0000000..497e585 --- /dev/null +++ b/src/lib/membership.ts @@ -0,0 +1,38 @@ +import { Kind } from "../constants"; +import { signEvent } from "./nostrAPI"; + +export const getMembershipStatus = async (pubkey: string | undefined, subId: string, socket: WebSocket) => { + if (!pubkey) return; + + const event = { + kind: Kind.Settings, + tags: [['p', pubkey]], + created_at: Math.floor((new Date()).getTime() / 1000), + content: JSON.stringify({}), + }; + + try { + const signedNote = await signEvent(event); + + const message = JSON.stringify([ + "REQ", + subId, + {cache: ["membership_status", { event_from_user: signedNote }]}, + ]); + + if (socket) { + const e = new CustomEvent('send', { detail: { message, ws: socket }}); + + socket.send(message); + socket.dispatchEvent(e); + } else { + throw('no_socket'); + } + + + return true; + } catch (reason) { + console.error('Failed to upload: ', reason); + return false; + } +} diff --git a/src/sockets.tsx b/src/sockets.tsx index c753eb6..40c174b 100644 --- a/src/sockets.tsx +++ b/src/sockets.tsx @@ -124,3 +124,21 @@ export const subscribeTo = (subId: string, cb: (type: NostrEventType, subId: str socket()?.removeEventListener('message', listener); }; }; + +export const subTo = (socket: WebSocket, subId: string, cb: (type: NostrEventType, subId: string, content?: NostrEventContent) => void ) => { + const listener = (event: MessageEvent) => { + const message: NostrEvent | NostrEOSE = JSON.parse(event.data); + const [type, subscriptionId, content] = message; + + if (subId === subscriptionId) { + cb(type, subscriptionId, content); + } + + }; + + socket.addEventListener('message', listener); + + return () => { + socket.removeEventListener('message', listener); + }; +}; diff --git a/src/types/primal.d.ts b/src/types/primal.d.ts index fd40d94..3ae43a1 100644 --- a/src/types/primal.d.ts +++ b/src/types/primal.d.ts @@ -663,3 +663,15 @@ export type ContactsData = { tags: string[][], following: string[], } + +export type MembershipStatus = { + pubkey?: string, + tier?: string, + name?: string, + rename?: string, + nostr_address?: string, + lightning_address?: string, + primal_vip_profile?: string, + used_storage?: number, + expires_on?: number, +};