From c7b483fd1f7d7ca585811d6f064a669329849ab2 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 5 Mar 2026 22:10:57 +0200 Subject: [PATCH 1/6] fix subscription status --- .../ResourceList/ResourceListDisplay.tsx | 41 ++++- src/hooks/useResources.tsx | 4 +- src/hooks/useSubscriptionCheck.tsx | 22 ++- src/utils/checkGroupHasSubscription.ts | 8 +- src/utils/checkSubscriptionStatus.ts | 153 +++++++++++++++++- 5 files changed, 206 insertions(+), 22 deletions(-) diff --git a/src/components/ResourceList/ResourceListDisplay.tsx b/src/components/ResourceList/ResourceListDisplay.tsx index d36ba03..a670886 100644 --- a/src/components/ResourceList/ResourceListDisplay.tsx +++ b/src/components/ResourceList/ResourceListDisplay.tsx @@ -85,6 +85,7 @@ interface BaseProps { scrollerRef?: React.RefObject | null; filterDuplicateIdentifiers?: FilterDuplicateIdentifiersOptions; secondaryDataSources?: SecondaryDataSource[]; + isLoading?: boolean; } const defaultStyles = { @@ -104,7 +105,39 @@ interface NonVirtualizedProps extends BaseProps { type PropsResourceListDisplay = VirtualizedProps | NonVirtualizedProps; -export const MemorizedComponent = ({ +/** When initialIsLoading is true, only this shell mounts — no hooks, effects, or list logic run. */ +export const MemorizedComponent = (props: PropsResourceListDisplay) => { + const { isLoading: initialIsLoading = false } = props; + if (initialIsLoading) { + return ( +
+ + {null} + +
+ ); + } + return ; +}; + +function ResourceListDisplayContent({ search, listItem, styles = defaultStyles, @@ -129,7 +162,7 @@ export const MemorizedComponent = ({ scrollerRef, filterDuplicateIdentifiers, secondaryDataSources, -}: PropsResourceListDisplay) => { +}: PropsResourceListDisplay) { const { identifierOperations, lists } = useGlobal(); const [generatedIdentifier, setGeneratedIdentifier] = useState(''); const [memoizedParams, setMemorizedParams] = useState(''); @@ -303,7 +336,7 @@ export const MemorizedComponent = ({ ); const resetSearch = useCallback(async () => { - resetScroll() + resetScroll(); lists.deleteList(listName); getResourceList(); }, [listName, getResourceList, resetScroll]); @@ -336,7 +369,6 @@ export const MemorizedComponent = ({ getResourceList(); }, [getResourceList, listName]); // Runs when dependencies change - useScrollTrackerRef(listName, list?.length > 0, scrollerRef); const setSearchCacheExpiryDuration = useCacheStore( (s) => s.setSearchCacheExpiryDuration @@ -561,6 +593,7 @@ function arePropsEqual( prevProps.disableVirtualization === nextProps.disableVirtualization && prevProps.direction === nextProps.direction && prevProps.onSeenLastItem === nextProps.onSeenLastItem && + prevProps.isLoading === nextProps.isLoading && JSON.stringify(prevProps.filterDuplicateIdentifiers) === JSON.stringify(nextProps.filterDuplicateIdentifiers) && JSON.stringify(prevProps.search) === JSON.stringify(nextProps.search) && diff --git a/src/hooks/useResources.tsx b/src/hooks/useResources.tsx index ac98550..f579231 100644 --- a/src/hooks/useResources.tsx +++ b/src/hooks/useResources.tsx @@ -18,8 +18,8 @@ export const requestQueueProductPublishes = new RequestQueueWithPromise(12); export const requestQueueProductPublishesBackup = new RequestQueueWithPromise( 6 ); -export const requestQueueResourcesResultsOnly = new RequestQueueWithPromise(4); -export const requestQueueSearchResources = new RequestQueueWithPromise(8); +export const requestQueueResourcesResultsOnly = new RequestQueueWithPromise(6); +export const requestQueueSearchResources = new RequestQueueWithPromise(12); export interface Resource { qortalMetadata: QortalMetadata; diff --git a/src/hooks/useSubscriptionCheck.tsx b/src/hooks/useSubscriptionCheck.tsx index 926de20..e9f4db2 100644 --- a/src/hooks/useSubscriptionCheck.tsx +++ b/src/hooks/useSubscriptionCheck.tsx @@ -15,6 +15,9 @@ export type SubscriptionStatus = | 'owner' | 'no-subscription'; +export const subscriptionAppPublicSalt = + 'gnRp+Pao85XZlExcqynLS0+GaKCL3ia9E1sEm9XPaOA='; + export interface CheckSubscriptionStatusResult { /** * The subscription status @@ -73,6 +76,7 @@ export interface CheckSubscriptionStatusResult { } export interface UseCheckSubscriptionStatusParams { + name: string | null; /** * The address to check subscription status for */ @@ -148,8 +152,8 @@ export interface UseCheckSubscriptionStatusReturn export function useCheckSubscriptionStatus( params: UseCheckSubscriptionStatusParams ): UseCheckSubscriptionStatusReturn { - const { address, groupId, enabled = true } = params; - const { identifierOperations } = useGlobal(); + const { address, name, groupId, enabled = true } = params; + const { identifierOperations, lists } = useGlobal(); const [result, setResult] = useState({ status: 'no-subscription', isSubscribed: false, @@ -172,7 +176,7 @@ export function useCheckSubscriptionStatus( return; } - if (!address || !groupId || !identifierOperations) { + if (!address || !groupId || !identifierOperations || !lists || !name) { console.log('[useCheckSubscriptionStatus] Missing required parameters'); setLoading(false); return; @@ -190,8 +194,10 @@ export function useCheckSubscriptionStatus( try { const statusResult = await checkSubscriptionStatus({ address, + name: name!, groupId, identifierOperations, + lists, }); if (!cancelled) { @@ -219,7 +225,15 @@ export function useCheckSubscriptionStatus( return () => { cancelled = true; }; - }, [address, groupId, identifierOperations, enabled, refreshKey]); + }, [ + address, + name, + groupId, + identifierOperations, + enabled, + refreshKey, + lists, + ]); return { ...result, diff --git a/src/utils/checkGroupHasSubscription.ts b/src/utils/checkGroupHasSubscription.ts index fc071c1..7db9c17 100644 --- a/src/utils/checkGroupHasSubscription.ts +++ b/src/utils/checkGroupHasSubscription.ts @@ -61,10 +61,10 @@ async function buildDetailsIdentifier( groupId: number, identifierOperations: any ): Promise { - const subscriptionId = `test-subscription-${groupId.toString()}`; + const subscriptionId = `subscription-${groupId.toString()}`; const typeHash = await identifierOperations.hashString( - 'test-subscription_details', + 'subscription_details', EnumCollisionStrength.HIGH, subscriptionAppPublicSalt ); @@ -89,10 +89,10 @@ async function buildIndexIdentifierBase( groupId: number, identifierOperations: any ): Promise { - const subscriptionId = `test-subscription-${groupId.toString()}`; + const subscriptionId = `subscription-${groupId.toString()}`; const typeHash = await identifierOperations.hashString( - 'test-subscription_index', + 'subscription_index', EnumCollisionStrength.HIGH, subscriptionAppPublicSalt ); diff --git a/src/utils/checkSubscriptionStatus.ts b/src/utils/checkSubscriptionStatus.ts index 120ae4c..442fd3e 100644 --- a/src/utils/checkSubscriptionStatus.ts +++ b/src/utils/checkSubscriptionStatus.ts @@ -3,9 +3,8 @@ * This is a standalone function that can be used in any app without React dependencies */ +import { subscriptionAppPublicSalt } from '../hooks/useSubscriptionCheck'; import { EnumCollisionStrength } from './encryption'; -export const subscriptionAppPublicSalt = - 'gnRp+Pao85XZlExcqynLS0+GaKCL3ia9E1sEm9XPaOA='; export type SubscriptionStatus = | 'not-subscribed' @@ -20,6 +19,10 @@ export interface CheckSubscriptionStatusParams { */ address: string; + /** + * The name to check subscription status for + */ + name: string; /** * The group ID to check membership in */ @@ -30,6 +33,16 @@ export interface CheckSubscriptionStatusParams { * Get this from useGlobal() hook: const { identifierOperations } = useGlobal(); */ identifierOperations: any; + + lists: any; +} + +/** Subscription pricing state (historical) */ +interface SubscriptionState { + version: number; + price: number; + interval: 'DAY' | 'WEEK' | 'MONTH' | 'YEAR'; + effectiveFrom: number; } /** @@ -60,7 +73,7 @@ async function buildDetailsIdentifier( groupId: number, identifierOperations: any ): Promise { - const subscriptionId = `test-subscription-${groupId.toString()}`; + const subscriptionId = `subscription-${groupId.toString()}`; console.log( '[buildDetailsIdentifier] Building identifier for subscriptionId:', @@ -69,7 +82,7 @@ async function buildDetailsIdentifier( // Hash the type and ID using identifierOperations (same as in subscriptionPublishing.ts) const typeHash = await identifierOperations.hashString( - 'test-subscription_details', + 'subscription_details', EnumCollisionStrength.HIGH, subscriptionAppPublicSalt ); @@ -184,7 +197,7 @@ export interface CheckSubscriptionStatusResult { export async function checkSubscriptionStatus( params: CheckSubscriptionStatusParams ): Promise { - const { address, groupId, identifierOperations } = params; + const { address, name, groupId, identifierOperations, lists } = params; const defaultResult: CheckSubscriptionStatusResult = { status: 'no-subscription', @@ -217,7 +230,7 @@ export async function checkSubscriptionStatus( } const groupData = await groupResponse.json(); - const groupOwner = groupData?.owner || groupData?.ownerAddress; + const groupOwner = groupData?.owner; console.log('[checkSubscriptionStatus] Group owner:', groupOwner); if (groupOwner === address) { @@ -239,7 +252,7 @@ export async function checkSubscriptionStatus( ); // Get the owner's primary name - const ownerName = await getPrimaryName(groupOwner); + const ownerName = groupData?.ownerPrimaryName; if (!ownerName) { console.log( '[checkSubscriptionStatus] Owner has no primary name - subscription likely not enabled' @@ -348,7 +361,7 @@ export async function checkSubscriptionStatus( } // Step 3: Get the primary name for this address - const name = await getPrimaryName(address); + console.log('[checkSubscriptionStatus] Primary name:', name); if (!name) { @@ -379,6 +392,8 @@ export async function checkSubscriptionStatus( `identifier=${encodeURIComponent(subscriptionIdentifier)}&` + `name=${encodeURIComponent(name)}&` + `exactmatchnames=true&` + + `reverse=true&` + + `prefix=true&` + `limit=1`; console.log( @@ -553,6 +568,76 @@ export async function checkSubscriptionStatus( }; } + // Validate amount matches the price that was active at payment time (historical pricing) + const paymentTimestamp = txData?.timestamp; + if (paymentTimestamp != null) { + const paymentRecord = resources[0]; + + let recordData: any = null; + try { + const dataResponse = await fetch( + `/arbitrary/PRODUCT/${paymentRecord.name}/${paymentRecord.identifier}` + ); + if (dataResponse.ok) { + recordData = await dataResponse.json(); + } + } catch (error) { + console.error( + `Failed to fetch subscription record for ${name}:`, + error + ); + } + + const indexData = await fetchSubscriptionIndexPrice( + ownerName!, + recordData.si + ); + const expectedPrice = indexData?.priceQort; + const intervalDaysAtPayment = indexData?.intervalDays; + + const amountPaid = parseFloat(txData?.amount) ?? 0; + if (Math.abs(amountPaid - expectedPrice!) > 0.00001) { + console.error( + '[checkSubscriptionStatus] Payment amount does not match expected price:', + amountPaid, + 'expected', + expectedPrice + ); + return { + status: 'subscribed-unpaid', + isSubscribed: true, + needsPayment: true, + isOwner: false, + isMember: true, + hasPaymentRecord: true, + paymentTxSignature, + isPaymentTxValid: false, + hasSubscriptionEnabled: true, + paymentValidationError: `Payment amount ${amountPaid} doesn't match expected price ${expectedPrice} (price at time of payment)`, + }; + } + + const subscriptionEndsAt = + paymentTimestamp + intervalDaysAtPayment! * 24 * 60 * 60 * 1000; + + const now = Date.now(); + if (now < subscriptionEndsAt) { + } else { + return { + status: 'subscribed-unpaid', + isSubscribed: true, + needsPayment: true, + isOwner: false, + isMember: true, + hasPaymentRecord: true, + paymentTxSignature, + isPaymentTxValid: false, + hasSubscriptionEnabled: true, + paymentValidationError: `Subscription has expired`, + }; + } + } + // All validations passed! console.log('[checkSubscriptionStatus] Payment validated successfully'); return { @@ -656,3 +741,55 @@ export async function isGroupOwner( return false; } } + +export function parseOnChainIndexData( + data: string +): { priceQort: number; intervalDays: number } | null { + if (!data || typeof data !== 'string') return null; + const decoded = + data.length > 0 && !data.includes('|') + ? (() => { + try { + return atob(data); + } catch { + return data; + } + })() + : data; + const parts = decoded.trim().split('|'); + if (parts.length < 5 || parts[0] !== 'qsub1') return null; + const amt = parseFloat(parts[2]); + let intervalDays = parseFloat(parts[3]); + if (Number.isNaN(amt) || Number.isNaN(intervalDays) || intervalDays < 0) + return null; + if (intervalDays === 0) intervalDays = 1 / 24; // 0 stored for hourly + return { priceQort: amt, intervalDays }; +} + +export async function fetchSubscriptionIndexPrice( + ownerName: string, + indexIdentifier: string +): Promise<{ priceQort: number; intervalDays: number } | null> { + const res = await fetch( + `/arbitrary/DOCUMENT/${encodeURIComponent(ownerName)}/${encodeURIComponent(indexIdentifier)}` + ); + if (!res.ok) return null; + let dataStr = await res.text(); + try { + const parsed = JSON.parse(dataStr); + if (parsed && typeof parsed === 'object') { + const raw = parsed.resource?.data ?? parsed.data; + if (raw != null) dataStr = typeof raw === 'string' ? raw : String(raw); + } + } catch { + // not JSON + } + if (!dataStr.includes('|')) { + try { + dataStr = atob(dataStr); + } catch { + return null; + } + } + return parseOnChainIndexData(dataStr); +} From 5db6b4a6a81973cccb1b919dd9a1d1cf65ffdd05 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Fri, 6 Mar 2026 17:29:20 +0200 Subject: [PATCH 2/6] fix encrypted audio and video streaming --- .../AudioPlayer/AudioPlayerControls.tsx | 13 +++++++++++-- src/components/VideoPlayer/VideoPlayer.tsx | 19 ++++++++++++++++--- src/index.ts | 2 ++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/components/AudioPlayer/AudioPlayerControls.tsx b/src/components/AudioPlayer/AudioPlayerControls.tsx index 0611984..89d798e 100644 --- a/src/components/AudioPlayer/AudioPlayerControls.tsx +++ b/src/components/AudioPlayer/AudioPlayerControls.tsx @@ -335,8 +335,17 @@ const AudioPlayerComponent = forwardRef( useEffect(() => { const audioElement = audioRef.current; - if (!audioElement || !isReady || !resourceUrl) return; + if (!audioElement || !isReady || !resourceUrl || !activeTrack) return; + const copiedActiveTrack = structuredClone(activeTrack); + if (copiedActiveTrack.name) { + copiedActiveTrack.name = encodeURIComponent(copiedActiveTrack.name); + } + if (copiedActiveTrack.identifier) { + copiedActiveTrack.identifier = encodeURIComponent( + copiedActiveTrack.identifier + ); + } // Generate unique track identifier const trackId = `${activeTrack?.service}-${activeTrack?.name}-${activeTrack?.identifier}-${encryption?.key || 'none'}`; @@ -371,7 +380,7 @@ const AudioPlayerComponent = forwardRef( const qortalStreamUrl = await playEncryptedAudioWithQortalRequest({ keyBytes, ivBytes, - qortalAudioResource: activeTrack, + qortalAudioResource: copiedActiveTrack, }); if (abortController.signal.aborted) return; diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index 4f4873f..c8f4013 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -1273,7 +1273,13 @@ export const VideoPlayer = ({ return; const resource = JSON.parse(videoLocactionStringified); - + const copiedResource = structuredClone(resource); + if (copiedResource.name) { + copiedResource.name = encodeURIComponent(copiedResource.name); + } + if (copiedResource.identifier) { + copiedResource.identifier = encodeURIComponent(copiedResource.identifier); + } try { const setupPlayer = async () => { const ref = videoRef as any; @@ -1303,9 +1309,14 @@ export const VideoPlayer = ({ player: playerRef.current, keyBytes: keyBytes, ivBytes: ivBytes, - qortalVideoResource: resource, + qortalVideoResource: copiedResource, } ); + await new Promise((res) => { + setTimeout(() => { + res(null); + }, 250); + }); if (qortalStreamUrl) { const options = { @@ -1641,7 +1652,9 @@ export const VideoPlayer = ({ player.off('ratechange', handleRateChange); }; }, [isPlayerInitialized]); - const hideTimeoutRef = useRef | null>(null); + const hideTimeoutRef = useRef | null>( + null + ); const resetHideTimeout = () => { setShowControlsMobile(true); if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); diff --git a/src/index.ts b/src/index.ts index 47b99f2..6f824e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,3 +74,5 @@ export { ListItem } from './state/cache'; export { SymmetricKeys } from './utils/encryption'; export { LoaderListStatus } from './common/ListLoader'; export { Coin } from './types/qortalRequests/types'; +export { useIdentifiers } from './hooks/useIdentifiers'; +export { useResources } from './hooks/useResources'; From 532dbfe4c5f348857aa3c82e7de31f0f85b39987 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sun, 15 Mar 2026 17:50:52 +0200 Subject: [PATCH 3/6] fix subscription status --- src/utils/checkSubscriptionStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/checkSubscriptionStatus.ts b/src/utils/checkSubscriptionStatus.ts index 442fd3e..388686a 100644 --- a/src/utils/checkSubscriptionStatus.ts +++ b/src/utils/checkSubscriptionStatus.ts @@ -596,7 +596,7 @@ export async function checkSubscriptionStatus( const intervalDaysAtPayment = indexData?.intervalDays; const amountPaid = parseFloat(txData?.amount) ?? 0; - if (Math.abs(amountPaid - expectedPrice!) > 0.00001) { + if (amountPaid < (expectedPrice ?? 0) - 0.00001) { console.error( '[checkSubscriptionStatus] Payment amount does not match expected price:', amountPaid, From efecb39efb9f80fe51993b4829023fc06aa5d179 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 19 Mar 2026 14:45:27 +0200 Subject: [PATCH 4/6] added new qortal request interfaces --- src/common/ImagePicker.tsx | 38 ++++++++++++++++++ src/global.ts | 12 +++++- src/types/qortalRequests/interfaces.ts | 53 ++++++++++++++++++++++++++ src/utils/checkSubscriptionStatus.ts | 46 ++++++++++++++++++++-- 4 files changed, 144 insertions(+), 5 deletions(-) diff --git a/src/common/ImagePicker.tsx b/src/common/ImagePicker.tsx index 516b1dd..8dbb9e2 100644 --- a/src/common/ImagePicker.tsx +++ b/src/common/ImagePicker.tsx @@ -8,6 +8,38 @@ import { import Compressor from 'compressorjs'; import { fileToBase64 } from '../utils/base64'; +async function isWebPAnimated(file: File): Promise { + const buffer = await file.slice(0, 65536).arrayBuffer(); + const bytes = new Uint8Array(buffer); + + if ( + bytes.length < 12 || + bytes[0] !== 0x52 || + bytes[1] !== 0x49 || + bytes[2] !== 0x46 || + bytes[3] !== 0x46 || + bytes[8] !== 0x57 || + bytes[9] !== 0x45 || + bytes[10] !== 0x42 || + bytes[11] !== 0x50 + ) { + return false; + } + + for (let i = 12; i < bytes.length - 4; i++) { + if ( + bytes[i] === 0x41 && + bytes[i + 1] === 0x4e && + bytes[i + 2] === 0x49 && + bytes[i + 3] === 0x4d + ) { + return true; + } + } + + return false; +} + type Mode = 'single' | 'multi'; interface ImageResult { @@ -60,6 +92,12 @@ export const ImagePicker: React.FC = ({ continue; } fileToConvert = image; + } else if ( + image.type === 'image/webp' && + image.size <= 150 * 1024 && + (await isWebPAnimated(image)) + ) { + fileToConvert = image; } else { fileToConvert = await new Promise((resolve, reject) => { new Compressor(image, { diff --git a/src/global.ts b/src/global.ts index 89b8d5a..429b938 100644 --- a/src/global.ts +++ b/src/global.ts @@ -93,6 +93,11 @@ import { VoteOnPollQortalRequest, whichUIQortalRequest, ChromecastCastQortalRequest, + NotificationPermissionQortalRequest, + NotificationAddQortalRequest, + NotificationGetQortalRequest, + NotificationMarkSeenQortalRequest, + NotificationRemoveQortalRequest, } from './types/qortalRequests/interfaces'; declare global { @@ -190,7 +195,12 @@ declare global { | lockTabQortalRequest | reencryptGroupKeysQortalRequest | sessionPermissionsQortalRequest - | ChromecastCastQortalRequest; + | ChromecastCastQortalRequest + | NotificationPermissionQortalRequest + | NotificationAddQortalRequest + | NotificationGetQortalRequest + | NotificationMarkSeenQortalRequest + | NotificationRemoveQortalRequest; function qortalRequest(options: QortalRequestOptions): Promise; diff --git a/src/types/qortalRequests/interfaces.ts b/src/types/qortalRequests/interfaces.ts index cd7ad77..9b9e54c 100644 --- a/src/types/qortalRequests/interfaces.ts +++ b/src/types/qortalRequests/interfaces.ts @@ -759,6 +759,59 @@ export interface sessionPermissionsQortalRequest extends BaseRequest { permissions: SessionPermissions[]; } +// Notification Permission Request Types +export interface NotificationPermissionQortalRequest extends BaseRequest { + action: 'NOTIFICATION_PERMISSION'; +} + +export interface NotificationFilter { + service?: string; + query?: string; + identifier?: string; + names?: string[]; + title?: string; + description?: string; + keywords?: string[]; + prefix?: boolean; + defaultResource?: boolean; + followedOnly?: boolean; + excludeBlocked?: boolean; + after?: number; + before?: number; + mode?: 'ALL' | 'ANY'; +} + +export interface NotificationItem { + notificationId: string; + link: string; + filters: NotificationFilter; + message: { en: string; [key: string]: string }; + image?: string; +} + +export interface NotificationAddQortalRequest extends BaseRequest { + action: 'NOTIFICATION_ADD'; + notifications: NotificationItem[]; +} + +export interface NotificationGetQortalRequest extends BaseRequest { + action: 'NOTIFICATION_GET'; +} + +export type NotificationSeenItem = + | string + | { notificationId: string; identifier?: string }; + +export interface NotificationMarkSeenQortalRequest extends BaseRequest { + action: 'NOTIFICATION_MARK_SEEN'; + notificationIds: NotificationSeenItem[]; +} + +export interface NotificationRemoveQortalRequest extends BaseRequest { + action: 'NOTIFICATION_REMOVE'; + notificationIds?: string[]; +} + // Chromecast Request Types export interface ChromecastCastQortalRequest extends BaseRequest { action: 'CHROMECAST_CAST'; diff --git a/src/utils/checkSubscriptionStatus.ts b/src/utils/checkSubscriptionStatus.ts index 388686a..c002c32 100644 --- a/src/utils/checkSubscriptionStatus.ts +++ b/src/utils/checkSubscriptionStatus.ts @@ -162,6 +162,37 @@ export interface CheckSubscriptionStatusResult { subscriptionDisabledReason?: string; } +const AMOUNT_TOLERANCE = 0.00001; + +function amountMatches(a: number, b: number): boolean { + return Math.abs(a - b) <= AMOUNT_TOLERANCE; +} + +export function getPaidIntervalsFromAmount( + paidAmount: number, + unitPrice: number +): number { + if ( + !Number.isFinite(paidAmount) || + !Number.isFinite(unitPrice) || + unitPrice <= 0 + ) { + return 0; + } + const raw = paidAmount / unitPrice; + if (!Number.isFinite(raw) || raw <= 0) return 0; + return Math.floor(raw + AMOUNT_TOLERANCE); +} + +export function isMultipleOfUnitPrice( + paidAmount: number, + unitPrice: number +): boolean { + const intervals = getPaidIntervalsFromAmount(paidAmount, unitPrice); + if (intervals < 1) return false; + return amountMatches(paidAmount, unitPrice * intervals); +} + /** * Check an address's subscription status for a private group * @@ -593,10 +624,13 @@ export async function checkSubscriptionStatus( recordData.si ); const expectedPrice = indexData?.priceQort; - const intervalDaysAtPayment = indexData?.intervalDays; + const intervalDaysAtPayment = indexData?.intervalDays ?? 30; const amountPaid = parseFloat(txData?.amount) ?? 0; - if (amountPaid < (expectedPrice ?? 0) - 0.00001) { + if ( + expectedPrice == null || + !isMultipleOfUnitPrice(amountPaid, expectedPrice) + ) { console.error( '[checkSubscriptionStatus] Payment amount does not match expected price:', amountPaid, @@ -616,9 +650,13 @@ export async function checkSubscriptionStatus( paymentValidationError: `Payment amount ${amountPaid} doesn't match expected price ${expectedPrice} (price at time of payment)`, }; } - + const paidIntervals = getPaidIntervalsFromAmount( + amountPaid, + expectedPrice + ); const subscriptionEndsAt = - paymentTimestamp + intervalDaysAtPayment! * 24 * 60 * 60 * 1000; + paymentTimestamp + + paidIntervals * intervalDaysAtPayment! * 24 * 60 * 60 * 1000; const now = Date.now(); if (now < subscriptionEndsAt) { From b637d60ec2aa20e665c5396d41cb184bc4147916 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 19 Mar 2026 14:51:20 +0200 Subject: [PATCH 5/6] remove logs --- src/state/publishes.ts | 43 ++++-------- src/utils/checkGroupHasSubscription.ts | 4 +- src/utils/checkSubscriptionStatus.ts | 93 ++------------------------ 3 files changed, 18 insertions(+), 122 deletions(-) diff --git a/src/state/publishes.ts b/src/state/publishes.ts index fdeeed1..a55064f 100644 --- a/src/state/publishes.ts +++ b/src/state/publishes.ts @@ -278,8 +278,7 @@ export const usePublishStore = create((set, get) => ({ if (chunkHistory.length > 6) { chunkHistory = chunkHistory.slice(-6); } - console.log('chunkHistory (after slice)', chunkHistory); - + // Need at least 2 data points to calculate speed if (chunkHistory.length < 2) { return null; @@ -289,9 +288,7 @@ export const usePublishStore = create((set, get) => ({ const lastPoint = chunkHistory[chunkHistory.length - 1]; const chunkDiff = lastPoint.chunks - firstPoint.chunks; const timeDiff = (lastPoint.timestamp - firstPoint.timestamp) / 1000; // in seconds - - console.log('Speed calc:', { chunkDiff, timeDiff, firstChunks: firstPoint.chunks, lastChunks: lastPoint.chunks }); - + if (timeDiff <= 0) { return null; } @@ -301,7 +298,6 @@ export const usePublishStore = create((set, get) => ({ if (chunkDiff <= 0) { // If we have enough history and no progress, it's stalled if (chunkHistory.length >= 4) { - console.log('STALLED DETECTED: chunkDiff <= 0 with enough history'); return 0; // Return 0 to indicate stalled, not null } return null; @@ -312,11 +308,9 @@ export const usePublishStore = create((set, get) => ({ // Set baseline speed on first valid measurement (after we have enough data) if (baselineSpeed === null && chunkHistory.length >= 4) { baselineSpeed = speed; - console.log('Baseline speed set:', baselineSpeed); return speed; } - - console.log('Current speed:', speed, 'Baseline:', baselineSpeed); + return speed; }; @@ -325,37 +319,30 @@ export const usePublishStore = create((set, get) => ({ if (numberOfPeers > maxPeersSeen) { maxPeersSeen = numberOfPeers; } - console.log('checkForSlowdown:', { maxPeersSeen, hasDetectedSlowdown, currentChunks, totalChunks }); - + // Only check for slowdown if we had more than 1 peer at some point if (maxPeersSeen <= 1) { - console.log('Skipping: maxPeersSeen <= 1'); return false; } - + // Don't restart multiple times if (hasDetectedSlowdown) { - console.log('Skipping: already detected slowdown'); return false; } - + // Need at least some progress to detect slowdown if (currentChunks === 0 || totalChunks === 0) { - console.log('Skipping: no chunks or total'); return false; } - + const currentSpeed = calculateChunkSpeed(currentChunks, totalChunks); - console.log('currentSpeed:', currentSpeed, 'baselineSpeed:', baselineSpeed); - + // Handle stalled downloads (speed === 0) - this is definitely a slowdown if (currentSpeed === 0 && baselineSpeed !== null && chunkHistory.length >= 4) { - console.log('Download stalled - no chunk progress detected'); return true; } - + if (currentSpeed === null || baselineSpeed === null) { - console.log('Skipping: currentSpeed or baselineSpeed is null'); return false; } @@ -363,9 +350,7 @@ export const usePublishStore = create((set, get) => ({ // If we have many chunks, we expect slower absolute speed, so normalize const normalizedBaseline = baselineSpeed / totalChunks; // baseline speed as fraction of total per second const normalizedCurrent = currentSpeed / totalChunks; // current speed as fraction of total per second - - console.log('Normalized speeds:', { normalizedBaseline, normalizedCurrent }); - + // Detect slowdown: current speed is less than 50% of baseline // This means download has slowed down significantly const slowdownThreshold = 0.5; @@ -375,9 +360,7 @@ export const usePublishStore = create((set, get) => ({ // If we're downloading less than 0.1% of total chunks per second, consider it slow const absoluteSlowThreshold = 0.001; // 0.1% of total chunks per second const isVerySlow = normalizedCurrent < absoluteSlowThreshold; - - console.log('Slowdown checks:', { hasSlowdown, isVerySlow }); - + return hasSlowdown || isVerySlow; }; @@ -468,11 +451,9 @@ export const usePublishStore = create((set, get) => ({ currentStatus.totalChunkCount, numberOfPeers ); - console.log('shouldRequestAsync', shouldRequestAsync); if (shouldRequestAsync) { hasDetectedSlowdown = true; - console.log(`Download slowdown detected. Requesting async fetch for ${resourceId}`); - + // Call the async fetch request const url = `/arbitrary/${service}/${name}/${identifier}?async=true`; requestQueueBuildFile.enqueue(() => diff --git a/src/utils/checkGroupHasSubscription.ts b/src/utils/checkGroupHasSubscription.ts index 7db9c17..735fa21 100644 --- a/src/utils/checkGroupHasSubscription.ts +++ b/src/utils/checkGroupHasSubscription.ts @@ -24,9 +24,9 @@ * const result = await checkGroupHasSubscription(12345, identifierOperations); * * if (result.exists) { - * console.log(`Group ${result.groupId}: ${result.title}`); + * // use result.groupId, result.title * } else { - * console.log('No subscription found'); + * // no subscription for this group * } * ``` */ diff --git a/src/utils/checkSubscriptionStatus.ts b/src/utils/checkSubscriptionStatus.ts index c002c32..fe16f07 100644 --- a/src/utils/checkSubscriptionStatus.ts +++ b/src/utils/checkSubscriptionStatus.ts @@ -52,12 +52,10 @@ async function getPrimaryName(address: string): Promise { try { const response = await fetch(`/names/primary/${address}`); if (!response.ok) { - console.log('[getPrimaryName] No primary name found for:', address); return null; } const nameData = await response.json(); const name = nameData?.name || null; - console.log('[getPrimaryName] Found name:', name, 'for address:', address); return name; } catch (error) { console.error('[getPrimaryName] Failed to fetch primary name:', error); @@ -75,11 +73,6 @@ async function buildDetailsIdentifier( ): Promise { const subscriptionId = `subscription-${groupId.toString()}`; - console.log( - '[buildDetailsIdentifier] Building identifier for subscriptionId:', - subscriptionId - ); - // Hash the type and ID using identifierOperations (same as in subscriptionPublishing.ts) const typeHash = await identifierOperations.hashString( 'subscription_details', @@ -92,15 +85,11 @@ async function buildDetailsIdentifier( subscriptionAppPublicSalt ); - console.log('[buildDetailsIdentifier] typeHash:', typeHash); - console.log('[buildDetailsIdentifier] idHash:', idHash); - if (!typeHash || !idHash) { throw new Error('Failed to create subscription identifier'); } const identifier = typeHash + idHash; - console.log('[buildDetailsIdentifier] Final identifier:', identifier); return identifier; } @@ -220,9 +209,8 @@ export function isMultipleOfUnitPrice( * identifierOperations * }); * - * console.log(result.status); // 'subscribed-paid' | 'subscribed-unpaid' | 'not-subscribed' | 'owner' - * console.log(result.isSubscribed); // true/false - * console.log(result.needsPayment); // true/false + * // result.status: 'subscribed-paid' | 'subscribed-unpaid' | 'not-subscribed' | 'owner' + * // result.isSubscribed, result.needsPayment * ``` */ export async function checkSubscriptionStatus( @@ -241,13 +229,10 @@ export async function checkSubscriptionStatus( }; if (!address || !groupId || !identifierOperations) { - console.log('[checkSubscriptionStatus] Missing required parameters'); return defaultResult; } try { - console.log('[checkSubscriptionStatus] Checking:', { address, groupId }); - // Step 1: Check if the address is the group owner const groupResponse = await fetch(`/groups/${groupId}`); if (!groupResponse.ok) { @@ -262,10 +247,8 @@ export async function checkSubscriptionStatus( const groupData = await groupResponse.json(); const groupOwner = groupData?.owner; - console.log('[checkSubscriptionStatus] Group owner:', groupOwner); if (groupOwner === address) { - console.log('[checkSubscriptionStatus] User is the group owner'); return { status: 'owner', isSubscribed: false, @@ -278,16 +261,9 @@ export async function checkSubscriptionStatus( } // Step 1.5: Check if the owner has enabled a subscription for this group - console.log( - '[checkSubscriptionStatus] Checking if subscription is enabled...' - ); - // Get the owner's primary name const ownerName = groupData?.ownerPrimaryName; if (!ownerName) { - console.log( - '[checkSubscriptionStatus] Owner has no primary name - subscription likely not enabled' - ); return { status: 'no-subscription', isSubscribed: false, @@ -306,23 +282,12 @@ export async function checkSubscriptionStatus( identifierOperations ); - console.log( - '[checkSubscriptionStatus] Checking for subscription details:', - { - ownerName, - subscriptionIdentifier, - } - ); - // Check if subscription details exist (DOCUMENT service) const subscriptionDetailsResponse = await fetch( `/arbitrary/DOCUMENT/${encodeURIComponent(ownerName)}/${encodeURIComponent(subscriptionIdentifier)}` ); if (!subscriptionDetailsResponse.ok) { - console.log( - '[checkSubscriptionStatus] No subscription details found for this group' - ); return { status: 'no-subscription', isSubscribed: false, @@ -336,14 +301,9 @@ export async function checkSubscriptionStatus( } const subscriptionDetails = await subscriptionDetailsResponse.json(); - console.log( - '[checkSubscriptionStatus] Subscription details:', - subscriptionDetails - ); // Check if subscription is disabled if (subscriptionDetails?.status === 'disabled') { - console.log('[checkSubscriptionStatus] Subscription is disabled'); return { status: 'no-subscription', isSubscribed: false, @@ -357,8 +317,6 @@ export async function checkSubscriptionStatus( }; } - console.log('[checkSubscriptionStatus] Subscription is enabled'); - // Step 2: Check if the address is a member of the group const memberResponse = await fetch(`/groups/member/${address}`); if (!memberResponse.ok) { @@ -371,19 +329,12 @@ export async function checkSubscriptionStatus( } const groups = await memberResponse.json(); - console.log('[checkSubscriptionStatus] User groups:', groups); const isMember = Array.isArray(groups) && groups.some((group: any) => group.groupId === groupId); - console.log('[checkSubscriptionStatus] Is member:', isMember); - if (!isMember) { - console.log( - '[checkSubscriptionStatus] User is not a member of group', - groupId - ); return { ...defaultResult, status: 'not-subscribed', @@ -393,11 +344,8 @@ export async function checkSubscriptionStatus( // Step 3: Get the primary name for this address - console.log('[checkSubscriptionStatus] Primary name:', name); - if (!name) { // If no name is registered, can't check for payment records - console.log('[checkSubscriptionStatus] No name registered for address'); return { status: 'subscribed-unpaid', isSubscribed: true, @@ -410,11 +358,6 @@ export async function checkSubscriptionStatus( } // Step 4: Build the details identifier for this subscription (reuse from earlier) - console.log( - '[checkSubscriptionStatus] Details identifier:', - subscriptionIdentifier - ); - // Step 5: Check if there's a payment record for THIS specific subscription // Search for PRODUCT resource with the specific subscriptionIdentifier const searchUrl = @@ -427,11 +370,6 @@ export async function checkSubscriptionStatus( `prefix=true&` + `limit=1`; - console.log( - '[checkSubscriptionStatus] Searching for payment record:', - searchUrl - ); - const resourcesResponse = await fetch(searchUrl); if (!resourcesResponse.ok) { @@ -452,20 +390,10 @@ export async function checkSubscriptionStatus( } const resources = await resourcesResponse.json(); - console.log( - '[checkSubscriptionStatus] Payment resources found:', - resources - ); const hasPaymentRecord = Array.isArray(resources) && resources.length > 0; - console.log( - '[checkSubscriptionStatus] Has payment record:', - hasPaymentRecord - ); - if (!hasPaymentRecord) { - console.log('[checkSubscriptionStatus] No payment record found'); return { status: 'subscribed-unpaid', isSubscribed: true, @@ -479,8 +407,6 @@ export async function checkSubscriptionStatus( } // Step 6: Fetch and validate the payment transaction - console.log('[checkSubscriptionStatus] Fetching PRODUCT record data...'); - let recordData: any = null; try { const dataResponse = await fetch( @@ -488,10 +414,6 @@ export async function checkSubscriptionStatus( ); if (dataResponse.ok) { recordData = await dataResponse.json(); - console.log( - '[checkSubscriptionStatus] PRODUCT record data:', - recordData - ); } else { console.warn( '[checkSubscriptionStatus] Failed to fetch PRODUCT record data:', @@ -511,9 +433,6 @@ export async function checkSubscriptionStatus( : undefined; if (!paymentTxSignature) { - console.log( - '[checkSubscriptionStatus] No tx signature in PRODUCT record' - ); return { status: 'subscribed-unpaid', isSubscribed: true, @@ -529,8 +448,6 @@ export async function checkSubscriptionStatus( } // Step 7: Validate the transaction on-chain - console.log('[checkSubscriptionStatus] Validating tx:', paymentTxSignature); - try { const txResponse = await fetch( `/transactions/signature/${paymentTxSignature}` @@ -554,7 +471,6 @@ export async function checkSubscriptionStatus( } const txData = await txResponse.json(); - console.log('[checkSubscriptionStatus] Transaction data:', txData); // Validate it's a PAYMENT transaction const typeOk = txData?.type === 'PAYMENT' || txData?.type === 2; @@ -677,7 +593,6 @@ export async function checkSubscriptionStatus( } // All validations passed! - console.log('[checkSubscriptionStatus] Payment validated successfully'); return { status: 'subscribed-paid', isSubscribed: true, @@ -724,7 +639,7 @@ export async function checkSubscriptionStatus( * @example * ```typescript * const isMember = await isGroupMember('QUserAddress123', 12345); - * console.log(isMember); // true/false + * // isMember is true/false * ``` */ export async function isGroupMember( @@ -758,7 +673,7 @@ export async function isGroupMember( * @example * ```typescript * const isOwner = await isGroupOwner('QOwnerAddress123', 12345); - * console.log(isOwner); // true/false + * // isOwner is true/false * ``` */ export async function isGroupOwner( From 4e653dfb80c97d87ec99a85de8d3b42685a10b0a Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 19 Mar 2026 14:57:31 +0200 Subject: [PATCH 6/6] added qortalRequest type --- src/global.ts | 2 ++ src/types/qortalRequests/interfaces.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/global.ts b/src/global.ts index 429b938..6effce3 100644 --- a/src/global.ts +++ b/src/global.ts @@ -94,6 +94,7 @@ import { whichUIQortalRequest, ChromecastCastQortalRequest, NotificationPermissionQortalRequest, + NotificationHasPermissionQortalRequest, NotificationAddQortalRequest, NotificationGetQortalRequest, NotificationMarkSeenQortalRequest, @@ -197,6 +198,7 @@ declare global { | sessionPermissionsQortalRequest | ChromecastCastQortalRequest | NotificationPermissionQortalRequest + | NotificationHasPermissionQortalRequest | NotificationAddQortalRequest | NotificationGetQortalRequest | NotificationMarkSeenQortalRequest diff --git a/src/types/qortalRequests/interfaces.ts b/src/types/qortalRequests/interfaces.ts index 9b9e54c..8248d62 100644 --- a/src/types/qortalRequests/interfaces.ts +++ b/src/types/qortalRequests/interfaces.ts @@ -764,6 +764,11 @@ export interface NotificationPermissionQortalRequest extends BaseRequest { action: 'NOTIFICATION_PERMISSION'; } +/** Resolves to `true` or `false` depending on whether the app has notification permission. */ +export interface NotificationHasPermissionQortalRequest extends BaseRequest { + action: 'NOTIFICATION_HAS_PERMISSION'; +} + export interface NotificationFilter { service?: string; query?: string;