mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-02-14 23:18:54 +00:00
Fix megaphone donate cta to navigate to amount picker and cache config in redux
This commit is contained in:
@@ -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 {
|
||||
@@ -87,7 +88,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;
|
||||
@@ -555,7 +556,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,5 +27,6 @@ export function getDonationsForRedux(): DonationsStateType {
|
||||
didResumeWorkflowAtStartup: Boolean(currentWorkflow),
|
||||
lastError: undefined,
|
||||
receipts: donationReceipts,
|
||||
configCache: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,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');
|
||||
|
||||
@@ -140,6 +141,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);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
DonationErrorType,
|
||||
DonationReceipt,
|
||||
DonationWorkflow,
|
||||
OneTimeDonationHumanAmounts,
|
||||
StripeDonationAmount,
|
||||
} from '../../types/Donations.std.js';
|
||||
import type { BadgeType } from '../../badges/types.std.js';
|
||||
@@ -45,11 +46,13 @@ export type DonationsStateType = ReadonlyDeep<{
|
||||
didResumeWorkflowAtStartup: boolean;
|
||||
lastError: DonationErrorType | 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';
|
||||
@@ -60,6 +63,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;
|
||||
@@ -82,6 +90,7 @@ export type UpdateWorkflowAction = ReadonlyDeep<{
|
||||
|
||||
export type DonationsActionType = ReadonlyDeep<
|
||||
| AddReceiptAction
|
||||
| HydrateConfigCacheAction
|
||||
| SetDidResumeAction
|
||||
| SubmitDonationAction
|
||||
| UpdateLastErrorAction
|
||||
@@ -120,6 +129,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,
|
||||
@@ -398,6 +416,7 @@ export const actions = {
|
||||
applyDonationBadge,
|
||||
clearWorkflow,
|
||||
internalAddDonationReceipt,
|
||||
hydrateConfigCache,
|
||||
setDidResume,
|
||||
resumeWorkflow,
|
||||
submitDonation,
|
||||
@@ -417,6 +436,7 @@ export function getEmptyState(): DonationsStateType {
|
||||
didResumeWorkflowAtStartup: false,
|
||||
lastError: undefined,
|
||||
receipts: [],
|
||||
configCache: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -431,6 +451,13 @@ export function reducer(
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === HYDRATE_CONFIG_CACHE) {
|
||||
return {
|
||||
...state,
|
||||
configCache: action.payload.configCache,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_DID_RESUME) {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -105,7 +105,7 @@ function interactWithMegaphone(
|
||||
navActions.changeLocation({
|
||||
tab: NavTab.Settings,
|
||||
details: {
|
||||
page: SettingsPage.Donations,
|
||||
page: SettingsPage.DonationsDonateFlow,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
21
ts/state/selectors/donations.std.ts
Normal file
21
ts/state/selectors/donations.std.ts
Normal 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
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -10,17 +10,35 @@ import {
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -28,6 +46,7 @@ export async function getCachedSubscriptionConfiguration(): Promise<Subscription
|
||||
return cachedSubscriptionConfig;
|
||||
}
|
||||
|
||||
log.info('Refreshing config cache');
|
||||
const response = await getSubscriptionConfiguration();
|
||||
|
||||
cachedSubscriptionConfig = response;
|
||||
@@ -37,7 +56,11 @@ 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();
|
||||
// pickBy returns a Partial so we need to cast it
|
||||
return pickBy(
|
||||
@@ -47,3 +70,12 @@ export async function getDonationHumanAmounts(): Promise<OneTimeDonationHumanAmo
|
||||
supportedPaymentMethods.includes(PaymentMethod.Paypal)
|
||||
) as unknown as OneTimeDonationHumanAmounts;
|
||||
}
|
||||
|
||||
export async function maybeHydrateDonationConfigCache(): Promise<void> {
|
||||
if (!isCacheRefreshNeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const amounts = await getCachedDonationHumanAmounts();
|
||||
window.reduxActions.donations.hydrateConfigCache(amounts);
|
||||
}
|
||||
|
||||
@@ -138,6 +138,7 @@ window.testUtilities = {
|
||||
didResumeWorkflowAtStartup: false,
|
||||
lastError: undefined,
|
||||
receipts: [],
|
||||
configCache: undefined,
|
||||
},
|
||||
stickers: {
|
||||
installedPack: null,
|
||||
|
||||
Reference in New Issue
Block a user