Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions ts/components/dialog/SessionCTA.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -426,23 +428,38 @@ export const useShowSessionCTACbWithVariant = () => {
};

export async function handleTriggeredProCTAs(dispatch: Dispatch<any>) {
if (!getFeatureFlag('proAvailable')) {
return;
}
const proAvailable = getFeatureFlag('proAvailable');

if (Storage.get(SettingsKey.proExpiringSoonCTA)) {
if (!proAvailable) {
return;
}
dispatch(
updateSessionCTA({
variant: CTAVariant.PRO_EXPIRING_SOON,
})
);
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 }));
}
}
}
}
69 changes: 69 additions & 0 deletions ts/components/dialog/debug/playgrounds/CTAPlaygroundPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<ProDebugSection {...props} forceUpdate={forceUpdate} />
<h2>Call to Actions (CTAs)</h2>
<Flex
$container={true}
$flexGap="var(--margins-sm)"
style={{ color: 'var(--warning-color)' }}
>
<LucideIcon unicode={LUCIDE_ICONS_UNICODE.TRIANGLE_ALERT} iconSize={'small'} />{' '}
{'Pro CTAs only work if pro is available, toggle it above!'}
</Flex>
<SpacerLG />
<DebugMenuSection title="CTAs" rowWrap={true}>
<h3 style={{ width: '100%' }}>Feature CTAs</h3>
<DebugButton onClick={() => handleClick(CTAVariant.PRO_GENERIC)}>Generic</DebugButton>
<DebugButton onClick={() => handleClick(CTAVariant.PRO_MESSAGE_CHARACTER_LIMIT)}>
Character Limit
</DebugButton>
<DebugButton onClick={() => handleClick(CTAVariant.PRO_PINNED_CONVERSATION_LIMIT)}>
Pinned Conversations
</DebugButton>
<DebugButton
onClick={() => handleClick(CTAVariant.PRO_PINNED_CONVERSATION_LIMIT_GRANDFATHERED)}
>
Pinned Conversations (Grandfathered)
</DebugButton>
<DebugButton onClick={() => handleClick(CTAVariant.PRO_ANIMATED_DISPLAY_PICTURE)}>
Animated Profile Picture
</DebugButton>
<DebugButton onClick={() => handleClick(CTAVariant.PRO_ANIMATED_DISPLAY_PICTURE_ACTIVATED)}>
Animated Profile Picture (Has pro)
</DebugButton>
<h3 style={{ width: '100%' }}>
Pro Group CTAs <i>WIP</i>
</h3>
<DebugButton onClick={() => handleClick(CTAVariant.PRO_GROUP_ACTIVATED)}>
Group Activated
</DebugButton>
<DebugButton onClick={() => handleClick(CTAVariant.PRO_GROUP_NON_ADMIN)}>
Group (Non-Admin)
</DebugButton>
<DebugButton onClick={() => handleClick(CTAVariant.PRO_GROUP_ADMIN)}>
Group (Admin)
</DebugButton>
<h3 style={{ width: '100%' }}>Special CTAs</h3>
<DebugButton onClick={() => handleClick(CTAVariant.PRO_EXPIRING_SOON)}>
Expiring Soon
</DebugButton>
<DebugButton onClick={() => handleClick(CTAVariant.PRO_EXPIRED)}>Expired</DebugButton>
</DebugMenuSection>
</>
);
}
24 changes: 9 additions & 15 deletions ts/components/leftpane/ActionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -62,8 +61,8 @@ 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';

const StyledContainerAvatar = styled.div`
padding: var(--margins-lg);
Expand Down Expand Up @@ -117,6 +116,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();

Expand Down Expand Up @@ -147,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() {
Expand Down
7 changes: 6 additions & 1 deletion ts/data/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@ async function getPasswordHash(): Promise<string | null> {
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<number | null> {
return channels.getDBCreationTimestampMs();
if (!cachedDBCreationTimestampMs) {
cachedDBCreationTimestampMs = await channels.getDBCreationTimestampMs();
}
return cachedDBCreationTimestampMs;
}

// Guard Nodes
Expand Down
70 changes: 63 additions & 7 deletions ts/state/ducks/proBackendData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProProof | null> {
const masterPrivKeyHex = await getProMasterKeyHex();
const response = await ProBackendAPI.generateProProof({
masterPrivKeyHex,
Expand All @@ -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(
Expand Down Expand Up @@ -231,17 +233,45 @@ async function handleExpiryCTAs(
}
}

let lastKnownProofExpiryTimestamp: number | null = null;
let scheduledProofExpiryTaskTimestamp: number | null = null;
let scheduledProofExpiryTaskId: ReturnType<typeof setTimeout> | null = null;
let scheduledAccessExpiryTaskTimestamp: number | null = null;
let scheduledAccessExpiryTaskId: ReturnType<typeof setTimeout> | null = null;

function scheduleRefresh(timestampMs: number) {
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
);
}, delay);
}

async function handleProProof(accessExpiryTsMs: number, autoRenewing: boolean, status: ProStatus) {
if (status !== ProStatus.Active) {
return;
}

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();
Expand All @@ -251,8 +281,34 @@ 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 !== 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);
}
}

Expand Down
Loading