Check memebership status when logging in

This commit is contained in:
Bojan Mojsilovic 2024-03-08 14:05:41 +01:00
parent 2464df18f3
commit 7307bafbf5
5 changed files with 137 additions and 17 deletions

View File

@ -3,14 +3,14 @@ import { Progress } from '@kobalte/core';
import styles from './Uploader.module.scss'; import styles from './Uploader.module.scss';
import { uploadServer } from '../../uploadSocket'; 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 { NostrEOSE, NostrEvent, NostrEventContent, NostrEventType, NostrMediaUploaded } from '../../types/primal';
import { readUploadTime, saveUploadTime } from '../../lib/localStore'; import { readUploadTime, saveUploadTime } from '../../lib/localStore';
import { startTimes, uploadMediaCancel, uploadMediaChunk, uploadMediaConfirm } from '../../lib/media'; import { startTimes, uploadMediaCancel, uploadMediaChunk, uploadMediaConfirm } from '../../lib/media';
import { sha256, uuidv4 } from '../../utils'; import { sha256, uuidv4 } from '../../utils';
import { Kind, uploadLimit } from '../../constants'; import { Kind, uploadLimit } from '../../constants';
import ButtonGhost from '../Buttons/ButtonGhost'; import ButtonGhost from '../Buttons/ButtonGhost';
import { isAccountVerified } from '../../lib/profile'; import { useAccountContext } from '../../contexts/AccountContext';
const MB = 1024 * 1024; const MB = 1024 * 1024;
const maxParallelChunks = 5; const maxParallelChunks = 5;
@ -44,6 +44,7 @@ const Uploader: Component<{
onCancel?: () => void, onCancel?: () => void,
onSuccsess?: (url: string) => void, onSuccsess?: (url: string) => void,
}> = (props) => { }> = (props) => {
const account = useAccountContext();
const [uploadState, setUploadState] = createStore<UploadState>({ const [uploadState, setUploadState] = createStore<UploadState>({
isUploading: false, isUploading: false,
@ -113,7 +114,7 @@ const Uploader: Component<{
}); });
createEffect(() => { createEffect(() => {
calcUploadLimit(props.nip05); calcUploadLimit(account?.membershipStatus.tier);
}); });
onCleanup(() => { onCleanup(() => {
@ -127,21 +128,14 @@ const Uploader: Component<{
} }
}); });
const calcUploadLimit = (nip05: string | undefined) => { const calcUploadLimit = (membershipTier: string | undefined) => {
if (!nip05) { if (membershipTier === 'premium') {
setUploadState('uploadLimit', () => uploadLimit.regular);
return;
}
isAccountVerified(nip05).then(profile => {
if (profile && profile.pubkey === props.publicKey && nip05.endsWith && nip05.endsWith('primal.net')) {
setUploadState('uploadLimit', () => uploadLimit.premium); setUploadState('uploadLimit', () => uploadLimit.premium);
return; return;
} }
setUploadState('uploadLimit', () => uploadLimit.regular); setUploadState('uploadLimit', () => uploadLimit.regular);
});
}; };
const subTo = (socket: WebSocket, subId: string, cb: (type: NostrEventType, subId: string, content?: NostrEventContent) => void ) => { const subTo = (socket: WebSocket, subId: string, cb: (type: NostrEventType, subId: string, content?: NostrEventContent) => void ) => {

View File

@ -17,11 +17,12 @@ import {
NostrMutedContent, NostrMutedContent,
NostrRelays, NostrRelays,
NostrWindow, NostrWindow,
MembershipStatus,
PrimalNote, PrimalNote,
PrimalUser, PrimalUser,
} from '../types/primal'; } from '../types/primal';
import { Kind, pinEncodePrefix, relayConnectingTimeout } from "../constants"; 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"; import { sendContacts, sendLike, sendMuteList, triggerImportEvents } from "../lib/notes";
// @ts-ignore Bad types in nostr-tools // @ts-ignore Bad types in nostr-tools
import { generatePrivateKey, Relay, getPublicKey as nostrGetPubkey, nip19 } from "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 { useToastContext } from "../components/Toaster/Toaster";
import { useIntl } from "@cookbook/solid-intl"; import { useIntl } from "@cookbook/solid-intl";
import { account as tAccount } from "../translations"; import { account as tAccount } from "../translations";
import { getMembershipStatus } from "../lib/membership";
export type AccountContextStore = { export type AccountContextStore = {
likes: string[], likes: string[],
@ -68,6 +70,7 @@ export type AccountContextStore = {
showGettingStarted: boolean, showGettingStarted: boolean,
showLogin: boolean, showLogin: boolean,
emojiHistory: EmojiOption[], emojiHistory: EmojiOption[],
membershipStatus: MembershipStatus,
actions: { actions: {
showNewNoteForm: () => void, showNewNoteForm: () => void,
hideNewNoteForm: () => void, hideNewNoteForm: () => void,
@ -126,6 +129,7 @@ const initialData = {
showGettingStarted: false, showGettingStarted: false,
showLogin: false, showLogin: false,
emojiHistory: [], emojiHistory: [],
membershipStatus: {},
}; };
export const AccountContext = createContext<AccountContextStore>(); export const AccountContext = createContext<AccountContextStore>();
@ -143,6 +147,8 @@ export function AccountProvider(props: { children: JSXElement }) {
let connectedRelaysCopy: Relay[] = []; let connectedRelaysCopy: Relay[] = [];
let membershipSocket: WebSocket | undefined;
onMount(() => { onMount(() => {
setInterval(() => { setInterval(() => {
checkNostrChange(); 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 = () => { const showGetStarted = () => {
updateStore('showGettingStarted', () => true); updateStore('showGettingStarted', () => true);
} }
@ -227,8 +276,17 @@ export function AccountProvider(props: { children: JSXElement }) {
} }
const setPublicKey = (pubkey: string | undefined) => { const setPublicKey = (pubkey: string | undefined) => {
if(pubkey && pubkey.length > 0) {
updateStore('publicKey', () => pubkey); updateStore('publicKey', () => pubkey);
pubkey ? localStorage.setItem('pubkey', pubkey) : localStorage.removeItem('pubkey'); localStorage.setItem('pubkey', pubkey);
checkMembershipStatus();
}
else {
updateStore('publicKey', () => undefined);
localStorage.removeItem('pubkey');
}
updateStore('isKeyLookupDone', () => true); updateStore('isKeyLookupDone', () => true);
}; };

38
src/lib/membership.ts Normal file
View File

@ -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;
}
}

View File

@ -124,3 +124,21 @@ export const subscribeTo = (subId: string, cb: (type: NostrEventType, subId: str
socket()?.removeEventListener('message', listener); 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);
};
};

12
src/types/primal.d.ts vendored
View File

@ -663,3 +663,15 @@ export type ContactsData = {
tags: string[][], tags: string[][],
following: 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,
};