From 69704ccef2940c8d1ed4fcf98ec6e42508d4f737 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 21 Nov 2025 13:37:11 +1100 Subject: [PATCH 1/3] feat: schedule pro details fetch jobs based on proof and access expiry times --- .../debug/playgrounds/CTAPlaygroundPage.tsx | 69 +++++++++++++++++++ ts/components/leftpane/ActionsPanel.tsx | 8 +++ ts/state/ducks/proBackendData.ts | 67 ++++++++++++++++-- 3 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 ts/components/dialog/debug/playgrounds/CTAPlaygroundPage.tsx diff --git a/ts/components/dialog/debug/playgrounds/CTAPlaygroundPage.tsx b/ts/components/dialog/debug/playgrounds/CTAPlaygroundPage.tsx new file mode 100644 index 000000000..fbf9c14af --- /dev/null +++ b/ts/components/dialog/debug/playgrounds/CTAPlaygroundPage.tsx @@ -0,0 +1,69 @@ +import useUpdate from 'react-use/lib/useUpdate'; +import { ProDebugSection } from '../FeatureFlags'; +import { SpacerLG } from '../../../basic/Text'; +import { useShowSessionCTACbWithVariant } from '../../SessionCTA'; +import { Flex } from '../../../basic/Flex'; +import { LucideIcon } from '../../../icon/LucideIcon'; +import { LUCIDE_ICONS_UNICODE } from '../../../icon/lucide'; +import { DebugButton } from '../components'; +import { DebugMenuPageProps, DebugMenuSection } from '../DebugMenuModal'; +import { CTAVariant } from '../../cta/types'; + +export function CTAPlaygroundPage(props: DebugMenuPageProps) { + const forceUpdate = useUpdate(); + const handleClick = useShowSessionCTACbWithVariant(); + + return ( + <> + +

Call to Actions (CTAs)

+ + {' '} + {'Pro CTAs only work if pro is available, toggle it above!'} + + + +

Feature CTAs

+ handleClick(CTAVariant.PRO_GENERIC)}>Generic + handleClick(CTAVariant.PRO_MESSAGE_CHARACTER_LIMIT)}> + Character Limit + + handleClick(CTAVariant.PRO_PINNED_CONVERSATION_LIMIT)}> + Pinned Conversations + + handleClick(CTAVariant.PRO_PINNED_CONVERSATION_LIMIT_GRANDFATHERED)} + > + Pinned Conversations (Grandfathered) + + handleClick(CTAVariant.PRO_ANIMATED_DISPLAY_PICTURE)}> + Animated Profile Picture + + handleClick(CTAVariant.PRO_ANIMATED_DISPLAY_PICTURE_ACTIVATED)}> + Animated Profile Picture (Has pro) + +

+ Pro Group CTAs WIP +

+ handleClick(CTAVariant.PRO_GROUP_ACTIVATED)}> + Group Activated + + handleClick(CTAVariant.PRO_GROUP_NON_ADMIN)}> + Group (Non-Admin) + + handleClick(CTAVariant.PRO_GROUP_ADMIN)}> + Group (Admin) + +

Special CTAs

+ handleClick(CTAVariant.PRO_EXPIRING_SOON)}> + Expiring Soon + + handleClick(CTAVariant.PRO_EXPIRED)}>Expired +
+ + ); +} diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index 1f0a9663e..e30543a13 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -64,6 +64,8 @@ import { useDebugKey } from '../../hooks/useDebugKey'; import { UpdateProRevocationList } from '../../session/utils/job_runners/jobs/UpdateProRevocationListJob'; import { CTAVariant } from '../dialog/cta/types'; import { getUrlInteractionsForUrl, URLInteraction } from '../../util/urlHistory'; +import { proBackendDataActions } from '../../state/ducks/proBackendData'; +import { handleTriggeredProCTAs } from '../dialog/SessionCTA'; const StyledContainerAvatar = styled.div` padding: var(--margins-lg); @@ -117,6 +119,12 @@ const doAppStartUp = async () => { void SnodePool.getFreshSwarmFor(UserUtils.getOurPubKeyStrFromCache()).then(() => { // trigger any other actions that need to be done after the swarm is ready window.inboxStore?.dispatch(networkDataActions.fetchInfoFromSeshServer() as any); + window.inboxStore?.dispatch( + proBackendDataActions.refreshGetProDetailsFromProBackend({}) as any + ); + if (window.inboxStore) { + void handleTriggeredProCTAs(window.inboxStore.dispatch); + } }); // refresh our swarm on start to speed up the first message fetching event void Data.cleanupOrphanedAttachments(); diff --git a/ts/state/ducks/proBackendData.ts b/ts/state/ducks/proBackendData.ts index e59959e6b..1ca4ba11f 100644 --- a/ts/state/ducks/proBackendData.ts +++ b/ts/state/ducks/proBackendData.ts @@ -173,7 +173,7 @@ async function putProDetailsInStorage(details: ProDetailsResultType) { await Storage.put(SettingsKey.proDetails, details); } -async function handleNewProProof(rotatingPrivKeyHex: string) { +async function handleNewProProof(rotatingPrivKeyHex: string): Promise { const masterPrivKeyHex = await getProMasterKeyHex(); const response = await ProBackendAPI.generateProProof({ masterPrivKeyHex, @@ -188,13 +188,15 @@ async function handleNewProProof(rotatingPrivKeyHex: string) { signatureHex: response.result.sig, } satisfies ProProof; await UserConfigWrapperActions.setProConfig({ proProof, rotatingPrivKeyHex }); - } else { - window?.log?.error('failed to get new pro proof: ', response); + return proProof; } + window?.log?.error('failed to get new pro proof: ', response); + return null; } async function handleClearProProof() { - // TODO: remove pro proof from user config + await UserConfigWrapperActions.removeProConfig(); + // TODO: remove access expiry timestamp from synced user config } async function handleExpiryCTAs( @@ -231,6 +233,20 @@ async function handleExpiryCTAs( } } +let scheduledProofExpiryTaskTimestamp: number | null = null; +let scheduledProofExpiryTaskId: ReturnType | null = null; +let scheduledAccessExpiryTaskTimestamp: number | null = null; +let scheduledAccessExpiryTaskId: ReturnType | null = null; + +function scheduleRefresh(timestampMs: number) { + window?.log?.info(`Scheduling a pro details refresh for ${timestampMs}`); + return setTimeout(() => { + window?.inboxStore?.dispatch( + proBackendDataActions.refreshGetProDetailsFromProBackend({}) as any + ); + }, timestampMs - NetworkTime.now()); +} + async function handleProProof(accessExpiryTsMs: number, autoRenewing: boolean, status: ProStatus) { if (status !== ProStatus.Active) { return; @@ -238,10 +254,22 @@ async function handleProProof(accessExpiryTsMs: number, autoRenewing: boolean, s const proConfig = await UserConfigWrapperActions.getProConfig(); + // TODO: if the user config access expiry timestamp is different, set it and sync the user config + + let proofExpiry: number | null = null; + if (!proConfig || !proConfig.proProof) { - const rotatingPrivKeyHex = await UserUtils.getProRotatingPrivateKeyHex(); - await handleNewProProof(rotatingPrivKeyHex); + try { + const rotatingPrivKeyHex = await UserUtils.getProRotatingPrivateKeyHex(); + const newProof = await handleNewProProof(rotatingPrivKeyHex); + if (newProof) { + proofExpiry = newProof.expiryMs; + } + } catch (e) { + window?.log?.error(e); + } } else { + proofExpiry = proConfig.proProof.expiryMs; const sixtyMinutesBeforeAccessExpiry = accessExpiryTsMs - DURATION.HOURS; const sixtyMinutesBeforeProofExpiry = proConfig.proProof.expiryMs - DURATION.HOURS; const now = NetworkTime.now(); @@ -251,8 +279,33 @@ async function handleProProof(accessExpiryTsMs: number, autoRenewing: boolean, s autoRenewing ) { const rotatingPrivKeyHex = proConfig.rotatingPrivKeyHex; - await handleNewProProof(rotatingPrivKeyHex); + const newProof = await handleNewProProof(rotatingPrivKeyHex); + if (newProof) { + proofExpiry = newProof.expiryMs; + } + } + } + + const accessExpiryRefreshTimestamp = accessExpiryTsMs + 30 * DURATION.SECONDS; + if (accessExpiryRefreshTimestamp !== scheduledAccessExpiryTaskTimestamp) { + if (scheduledAccessExpiryTaskId) { + clearTimeout(scheduledAccessExpiryTaskId); + } + scheduledAccessExpiryTaskTimestamp = accessExpiryRefreshTimestamp; + scheduledAccessExpiryTaskId = scheduleRefresh(scheduledAccessExpiryTaskTimestamp); + } + + if ( + proofExpiry && + (!scheduledProofExpiryTaskTimestamp || proofExpiry > scheduledProofExpiryTaskTimestamp) + ) { + if (scheduledProofExpiryTaskId) { + clearTimeout(scheduledProofExpiryTaskId); } + // Random number of minutes between 10 and 60 + const minutes = Math.floor(Math.random() * 51) + 10; + scheduledProofExpiryTaskTimestamp = proofExpiry - minutes * DURATION.MINUTES; + scheduledProofExpiryTaskId = scheduleRefresh(scheduledProofExpiryTaskTimestamp); } } From 910240f8e426b90fdd5aa83fe7848a702c3f2b56 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 21 Nov 2025 14:35:09 +1100 Subject: [PATCH 2/3] fix: move all cta time triggering logic to the same function --- ts/components/dialog/SessionCTA.tsx | 25 +++++++++++++++++++++---- ts/components/leftpane/ActionsPanel.tsx | 16 +--------------- ts/data/data.ts | 7 ++++++- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/ts/components/dialog/SessionCTA.tsx b/ts/components/dialog/SessionCTA.tsx index 33e401791..276ec1e40 100644 --- a/ts/components/dialog/SessionCTA.tsx +++ b/ts/components/dialog/SessionCTA.tsx @@ -40,7 +40,9 @@ import { } from './cta/types'; import { getFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes'; import { showLinkVisitWarningDialog } from './OpenUrlModal'; -import { APP_URL } from '../../session/constants'; +import { APP_URL, DURATION } from '../../session/constants'; +import { Data } from '../../data/data'; +import { getUrlInteractionsForUrl, URLInteraction } from '../../util/urlHistory'; function useIsProCTAVariant(v: CTAVariant): v is ProCTAVariant { return useMemo(() => isProCTAVariant(v), [v]); @@ -426,11 +428,12 @@ export const useShowSessionCTACbWithVariant = () => { }; export async function handleTriggeredProCTAs(dispatch: Dispatch) { - if (!getFeatureFlag('proAvailable')) { - return; - } + const proAvailable = !getFeatureFlag('proAvailable'); if (Storage.get(SettingsKey.proExpiringSoonCTA)) { + if (!proAvailable) { + return; + } dispatch( updateSessionCTA({ variant: CTAVariant.PRO_EXPIRING_SOON, @@ -438,11 +441,25 @@ export async function handleTriggeredProCTAs(dispatch: Dispatch) { ); await Storage.put(SettingsKey.proExpiringSoonCTA, false); } else if (Storage.get(SettingsKey.proExpiredCTA)) { + if (!proAvailable) { + return; + } dispatch( updateSessionCTA({ variant: CTAVariant.PRO_EXPIRED, }) ); await Storage.put(SettingsKey.proExpiredCTA, false); + } else { + const dbCreationTimestampMs = await Data.getDBCreationTimestampMs(); + if (dbCreationTimestampMs && dbCreationTimestampMs + 7 * DURATION.DAYS < Date.now()) { + const donateInteractions = getUrlInteractionsForUrl(APP_URL.DONATE); + if ( + !donateInteractions.includes(URLInteraction.COPY) && + !donateInteractions.includes(URLInteraction.OPEN) + ) { + dispatch(updateSessionCTA({ variant: CTAVariant.DONATE_GENERIC })); + } + } } } diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index e30543a13..edc162209 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -19,12 +19,11 @@ import { getOurNumber } from '../../state/selectors/user'; import { DecryptedAttachmentsManager } from '../../session/crypto/DecryptedAttachmentsManager'; -import { APP_URL, DURATION } from '../../session/constants'; +import { DURATION } from '../../session/constants'; import { onionPathModal, updateDebugMenuModal, - updateSessionCTA, userSettingsModal, } from '../../state/ducks/modalDialog'; @@ -62,8 +61,6 @@ import { useDebugMenuModal } from '../../state/selectors/modal'; import { useFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes'; import { useDebugKey } from '../../hooks/useDebugKey'; import { UpdateProRevocationList } from '../../session/utils/job_runners/jobs/UpdateProRevocationListJob'; -import { CTAVariant } from '../dialog/cta/types'; -import { getUrlInteractionsForUrl, URLInteraction } from '../../util/urlHistory'; import { proBackendDataActions } from '../../state/ducks/proBackendData'; import { handleTriggeredProCTAs } from '../dialog/SessionCTA'; @@ -155,17 +152,6 @@ const doAppStartUp = async () => { }, 1 * DURATION.MINUTES); void regenerateLastMessagesGroupsCommunities(); - - const dbCreationTimestampMs = await Data.getDBCreationTimestampMs(); - if (dbCreationTimestampMs && dbCreationTimestampMs + 7 * DURATION.DAYS < Date.now()) { - const donateInteractions = getUrlInteractionsForUrl(APP_URL.DONATE); - if ( - !donateInteractions.includes(URLInteraction.COPY) && - !donateInteractions.includes(URLInteraction.OPEN) - ) { - window.inboxStore?.dispatch(updateSessionCTA({ variant: CTAVariant.DONATE_GENERIC })); - } - } }; function useUpdateBadgeCount() { diff --git a/ts/data/data.ts b/ts/data/data.ts index 522dfecfd..1ba273c31 100644 --- a/ts/data/data.ts +++ b/ts/data/data.ts @@ -60,8 +60,13 @@ async function getPasswordHash(): Promise { return channels.getPasswordHash(); } +// Note: once we have this timestamp there is no reason it should change +let cachedDBCreationTimestampMs: null | number = null; async function getDBCreationTimestampMs(): Promise { - return channels.getDBCreationTimestampMs(); + if (!cachedDBCreationTimestampMs) { + cachedDBCreationTimestampMs = await channels.getDBCreationTimestampMs(); + } + return cachedDBCreationTimestampMs; } // Guard Nodes From 57e6d83101998d2b3f6045a13520e458e31d5ea6 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 21 Nov 2025 14:57:51 +1100 Subject: [PATCH 3/3] fix: clamp auto refresh task scheduler --- ts/components/dialog/SessionCTA.tsx | 2 +- ts/state/ducks/proBackendData.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ts/components/dialog/SessionCTA.tsx b/ts/components/dialog/SessionCTA.tsx index 276ec1e40..519af3226 100644 --- a/ts/components/dialog/SessionCTA.tsx +++ b/ts/components/dialog/SessionCTA.tsx @@ -428,7 +428,7 @@ export const useShowSessionCTACbWithVariant = () => { }; export async function handleTriggeredProCTAs(dispatch: Dispatch) { - const proAvailable = !getFeatureFlag('proAvailable'); + const proAvailable = getFeatureFlag('proAvailable'); if (Storage.get(SettingsKey.proExpiringSoonCTA)) { if (!proAvailable) { diff --git a/ts/state/ducks/proBackendData.ts b/ts/state/ducks/proBackendData.ts index 1ca4ba11f..ede5accff 100644 --- a/ts/state/ducks/proBackendData.ts +++ b/ts/state/ducks/proBackendData.ts @@ -233,18 +233,20 @@ async function handleExpiryCTAs( } } +let lastKnownProofExpiryTimestamp: number | null = null; let scheduledProofExpiryTaskTimestamp: number | null = null; let scheduledProofExpiryTaskId: ReturnType | null = null; let scheduledAccessExpiryTaskTimestamp: number | null = null; let scheduledAccessExpiryTaskId: ReturnType | null = null; function scheduleRefresh(timestampMs: number) { - window?.log?.info(`Scheduling a pro details refresh for ${timestampMs}`); + const delay = Math.max(timestampMs - NetworkTime.now(), 15 * DURATION.SECONDS); + window?.log?.info(`Scheduling a pro details refresh in ${delay}ms for ${timestampMs}`); return setTimeout(() => { window?.inboxStore?.dispatch( proBackendDataActions.refreshGetProDetailsFromProBackend({}) as any ); - }, timestampMs - NetworkTime.now()); + }, delay); } async function handleProProof(accessExpiryTsMs: number, autoRenewing: boolean, status: ProStatus) { @@ -297,13 +299,14 @@ async function handleProProof(accessExpiryTsMs: number, autoRenewing: boolean, s if ( proofExpiry && - (!scheduledProofExpiryTaskTimestamp || proofExpiry > scheduledProofExpiryTaskTimestamp) + (!scheduledProofExpiryTaskTimestamp || proofExpiry !== lastKnownProofExpiryTimestamp) ) { if (scheduledProofExpiryTaskId) { clearTimeout(scheduledProofExpiryTaskId); } // Random number of minutes between 10 and 60 const minutes = Math.floor(Math.random() * 51) + 10; + lastKnownProofExpiryTimestamp = proofExpiry; scheduledProofExpiryTaskTimestamp = proofExpiry - minutes * DURATION.MINUTES; scheduledProofExpiryTaskId = scheduleRefresh(scheduledProofExpiryTaskTimestamp); }