Fix megaphone donate cta to navigate to amount picker and cache config in redux

Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2026-02-11 09:10:02 -06:00
committed by GitHub
parent 87bce6a82b
commit b2e7a07f19
10 changed files with 118 additions and 26 deletions

View File

@@ -11,6 +11,7 @@ import React, {
} from 'react';
import classNames from 'classnames';
import type { ReadonlyDeep } from 'type-fest';
import type { LocalizerType } from '../types/Util.std.js';
import { useConfirmDiscard } from '../hooks/useConfirmDiscard.dom.js';
import {
@@ -86,7 +87,7 @@ export type PropsDataType = {
initialCurrency: string;
isDonationPaypalEnabled: boolean;
isOnline: boolean;
donationAmountsConfig: OneTimeDonationHumanAmounts | undefined;
donationAmountsConfig: ReadonlyDeep<OneTimeDonationHumanAmounts> | undefined;
lastError: DonationErrorType | undefined;
validCurrencies: ReadonlyArray<string>;
workflow: DonationWorkflow | undefined;
@@ -537,7 +538,7 @@ type AmountPickerProps = {
initialAmount: HumanDonationAmount | undefined;
initialCurrency: string | undefined;
isOnline: boolean;
donationAmountsConfig: OneTimeDonationHumanAmounts | undefined;
donationAmountsConfig: ReadonlyDeep<OneTimeDonationHumanAmounts> | undefined;
validCurrencies: ReadonlyArray<string>;
onChangeCurrency: (value: string) => void;
onSubmit: (result: AmountPickerResult) => void;

View File

@@ -6,6 +6,7 @@ import lodash from 'lodash';
import type { MutableRefObject, ReactNode } from 'react';
import { ListBox, ListBoxItem } from 'react-aria-components';
import type { ReadonlyDeep } from 'type-fest';
import { getDateTimeFormatter } from '../util/formatTimestamp.dom.js';
import type { LocalizerType, ThemeType } from '../types/Util.std.js';
@@ -79,7 +80,7 @@ export type PropsDataType = {
color: AvatarColorType | undefined;
firstName: string | undefined;
profileAvatarUrl?: string;
donationAmountsConfig: OneTimeDonationHumanAmounts | undefined;
donationAmountsConfig: ReadonlyDeep<OneTimeDonationHumanAmounts> | undefined;
validCurrencies: ReadonlyArray<string>;
donationReceipts: ReadonlyArray<DonationReceipt>;
theme: ThemeType;

View File

@@ -28,5 +28,6 @@ export function getDonationsForRedux(): DonationsStateType {
lastError: undefined,
lastReturnToken: undefined,
receipts: donationReceipts,
configCache: undefined,
};
}

View File

@@ -20,6 +20,7 @@ import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary.std.js'
import { itemStorage } from '../textsecure/Storage.preload.js';
import { isMoreRecentThan } from '../util/timestamp.std.js';
import { isFeaturedEnabledNoRedux } from '../util/isFeatureEnabled.dom.js';
import { maybeHydrateDonationConfigCache } from '../util/subscriptionConfiguration.preload.js';
const log = createLogger('megaphoneService');
@@ -133,6 +134,16 @@ async function processMegaphone(megaphone: RemoteMegaphoneType): Promise<void> {
}
if (isMegaphoneShowable(megaphone)) {
if (
megaphone.primaryCtaId === 'donate' ||
megaphone.secondaryCtaId === 'donate'
) {
log.info(
'processMegaphone: Megaphone ctaId donate, prefetching donation amount config'
);
drop(maybeHydrateDonationConfigCache());
}
log.info(`processMegaphone: Showing ${id}`);
window.reduxActions.megaphones.addVisibleMegaphone(megaphone);
}

View File

@@ -29,6 +29,7 @@ import type {
DonationErrorType,
DonationReceipt,
DonationWorkflow,
OneTimeDonationHumanAmounts,
StripeDonationAmount,
} from '../../types/Donations.std.js';
import type { BadgeType } from '../../badges/types.std.js';
@@ -46,11 +47,13 @@ export type DonationsStateType = ReadonlyDeep<{
lastError: DonationErrorType | undefined;
lastReturnToken: string | undefined;
receipts: Array<DonationReceipt>;
configCache: OneTimeDonationHumanAmounts | undefined;
}>;
// Actions
export const ADD_RECEIPT = 'donations/ADD_RECEIPT';
export const HYDRATE_CONFIG_CACHE = 'donations/HYDRATE_CONFIG_CACHE';
export const SUBMIT_DONATION = 'donations/SUBMIT_DONATION';
export const UPDATE_WORKFLOW = 'donations/UPDATE_WORKFLOW';
export const UPDATE_LAST_ERROR = 'donations/UPDATE_LAST_ERROR';
@@ -61,6 +64,11 @@ export type AddReceiptAction = ReadonlyDeep<{
payload: { receipt: DonationReceipt };
}>;
export type HydrateConfigCacheAction = ReadonlyDeep<{
type: typeof HYDRATE_CONFIG_CACHE;
payload: { configCache: OneTimeDonationHumanAmounts };
}>;
export type SetDidResumeAction = ReadonlyDeep<{
type: typeof SET_DID_RESUME;
payload: boolean;
@@ -83,6 +91,7 @@ export type UpdateWorkflowAction = ReadonlyDeep<{
export type DonationsActionType = ReadonlyDeep<
| AddReceiptAction
| HydrateConfigCacheAction
| SetDidResumeAction
| SubmitDonationAction
| UpdateLastErrorAction
@@ -121,6 +130,15 @@ function internalAddDonationReceipt(
};
}
function hydrateConfigCache(
configCache: OneTimeDonationHumanAmounts
): HydrateConfigCacheAction {
return {
type: HYDRATE_CONFIG_CACHE,
payload: { configCache },
};
}
function setDidResume(didResume: boolean): SetDidResumeAction {
return {
type: SET_DID_RESUME,
@@ -399,6 +417,7 @@ export const actions = {
applyDonationBadge,
clearWorkflow,
internalAddDonationReceipt,
hydrateConfigCache,
setDidResume,
resumeWorkflow,
submitDonation,
@@ -419,6 +438,7 @@ export function getEmptyState(): DonationsStateType {
lastError: undefined,
lastReturnToken: undefined,
receipts: [],
configCache: undefined,
};
}
@@ -433,6 +453,13 @@ export function reducer(
};
}
if (action.type === HYDRATE_CONFIG_CACHE) {
return {
...state,
configCache: action.payload.configCache,
};
}
if (action.type === SET_DID_RESUME) {
return {
...state,

View File

@@ -105,7 +105,7 @@ function interactWithMegaphone(
navActions.changeLocation({
tab: NavTab.Settings,
details: {
page: SettingsPage.Donations,
page: SettingsPage.DonationsDonateFlow,
},
})
);

View File

@@ -0,0 +1,21 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { ReadonlyDeep } from 'type-fest';
import type { StateType } from '../reducer.preload.js';
import type { DonationsStateType } from '../ducks/donations.preload.js';
import type { OneTimeDonationHumanAmounts } from '../../types/Donations.std.js';
export const getDonationsState = (
state: Readonly<StateType>
): DonationsStateType => state.donations;
export const getDonationConfigCache = createSelector(
getDonationsState,
({
configCache,
}: Readonly<DonationsStateType>):
| ReadonlyDeep<OneTimeDonationHumanAmounts>
| undefined => configCache
);

View File

@@ -1,7 +1,7 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useEffect, useState, useCallback } from 'react';
import React, { memo, useEffect, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import type { MutableRefObject } from 'react';
@@ -21,12 +21,11 @@ import { useConversationsActions } from '../ducks/conversations.preload.js';
import { generateDonationReceiptBlob } from '../../util/generateDonationReceipt.dom.js';
import { useToastActions } from '../ducks/toast.preload.js';
import {
getDonationHumanAmounts,
getCachedSubscriptionConfiguration,
maybeHydrateDonationConfigCache,
} from '../../util/subscriptionConfiguration.preload.js';
import { drop } from '../../util/drop.std.js';
import { saveAttachmentToDisk } from '../../util/migrations.preload.js';
import type { OneTimeDonationHumanAmounts } from '../../types/Donations.std.js';
import {
ONE_TIME_DONATION_CONFIG_ID,
BOOST_ID,
@@ -42,6 +41,10 @@ import { useBadgesActions } from '../ducks/badges.preload.js';
import { getNetworkIsOnline } from '../selectors/network.preload.js';
import { getItems } from '../selectors/items.dom.js';
import { isFeaturedEnabledSelector } from '../../util/isFeatureEnabled.dom.js';
import {
getDonationConfigCache,
getDonationsState,
} from '../selectors/donations.std.js';
const log = createLogger('SmartPreferencesDonations');
@@ -55,12 +58,6 @@ export const SmartPreferencesDonations = memo(
settingsLocation: SettingsLocation;
setSettingsLocation: (settingsLocation: SettingsLocation) => void;
}) {
const [validCurrencies, setValidCurrencies] = useState<
ReadonlyArray<string>
>([]);
const [donationAmountsConfig, setDonationAmountsConfig] =
useState<OneTimeDonationHumanAmounts>();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const isOnline = useSelector(getNetworkIsOnline);
@@ -68,7 +65,13 @@ export const SmartPreferencesDonations = memo(
const items = useSelector(getItems);
const theme = useSelector(getTheme);
const donationsState = useSelector((state: StateType) => state.donations);
const donationsState = useSelector(getDonationsState);
const donationAmountsConfig = useSelector(getDonationConfigCache);
const validCurrencies = useMemo(
() => (donationAmountsConfig ? Object.keys(donationAmountsConfig) : []),
[donationAmountsConfig]
);
const {
applyDonationBadge,
clearWorkflow,
@@ -123,13 +126,7 @@ export const SmartPreferencesDonations = memo(
// Eagerly load donation config from API when entering Donations Home so the
// Amount picker loads instantly
useEffect(() => {
async function loadDonationAmounts() {
const amounts = await getDonationHumanAmounts();
setDonationAmountsConfig(amounts);
const currencies = Object.keys(amounts);
setValidCurrencies(currencies);
}
drop(loadDonationAmounts());
drop(maybeHydrateDonationConfigCache());
}, []);
const currencyFromPhone = ourNumber

View File

@@ -6,17 +6,35 @@ import { getSubscriptionConfiguration } from '../textsecure/WebAPI.preload.js';
import type { OneTimeDonationHumanAmounts } from '../types/Donations.std.js';
import { HOUR } from './durations/index.std.js';
import { isInPast } from './timestamp.std.js';
import { createLogger } from '../logging/log.std.js';
import { TaskDeduplicator } from './TaskDeduplicator.std.js';
const log = createLogger('subscriptionConfiguration');
const SUBSCRIPTION_CONFIG_CACHE_TIME = HOUR;
let cachedSubscriptionConfig: SubscriptionConfigurationResultType | undefined;
let cachedSubscriptionConfigExpiresAt: number | undefined;
export async function getCachedSubscriptionConfiguration(): Promise<SubscriptionConfigurationResultType> {
if (
cachedSubscriptionConfigExpiresAt != null &&
function isCacheRefreshNeeded(): boolean {
return (
cachedSubscriptionConfig == null ||
cachedSubscriptionConfigExpiresAt == null ||
isInPast(cachedSubscriptionConfigExpiresAt)
) {
);
}
export async function getCachedSubscriptionConfiguration(): Promise<SubscriptionConfigurationResultType> {
return getCachedSubscriptionConfigurationDedup.run();
}
const getCachedSubscriptionConfigurationDedup = new TaskDeduplicator(
'getCachedSubscriptionConfiguration',
() => _getCachedSubscriptionConfiguration()
);
export async function _getCachedSubscriptionConfiguration(): Promise<SubscriptionConfigurationResultType> {
if (isCacheRefreshNeeded()) {
cachedSubscriptionConfig = undefined;
}
@@ -24,6 +42,7 @@ export async function getCachedSubscriptionConfiguration(): Promise<Subscription
return cachedSubscriptionConfig;
}
log.info('Refreshing config cache');
const response = await getSubscriptionConfiguration();
cachedSubscriptionConfig = response;
@@ -33,7 +52,20 @@ export async function getCachedSubscriptionConfiguration(): Promise<Subscription
return response;
}
export async function getDonationHumanAmounts(): Promise<OneTimeDonationHumanAmounts> {
export function getCachedSubscriptionConfigExpiresAt(): number | undefined {
return cachedSubscriptionConfigExpiresAt;
}
export async function getCachedDonationHumanAmounts(): Promise<OneTimeDonationHumanAmounts> {
const { currencies } = await getCachedSubscriptionConfiguration();
return currencies;
}
export async function maybeHydrateDonationConfigCache(): Promise<void> {
if (!isCacheRefreshNeeded()) {
return;
}
const amounts = await getCachedDonationHumanAmounts();
window.reduxActions.donations.hydrateConfigCache(amounts);
}

View File

@@ -139,6 +139,7 @@ window.testUtilities = {
lastError: undefined,
lastReturnToken: undefined,
receipts: [],
configCache: undefined,
},
stickers: {
installedPack: null,