Disable donations while offline

This commit is contained in:
ayumi-signal
2025-08-06 09:40:30 -07:00
committed by GitHub
parent 20ac4e9a4e
commit 46d5b06bfc
7 changed files with 125 additions and 31 deletions
+4
View File
@@ -8958,6 +8958,10 @@
"messageformat": "Thank you for supporting Signal. Your contribution helps fuel the mission of protecting free expression and enabling secure global communication for millions around the world, through open source privacy technology. If youre a resident of the United States, please retain this receipt for your tax records. Signal Technology Foundation is a tax-exempt nonprofit organization in the United States under section 501c3 of the Internal Revenue Code. Our Federal Tax ID is 82-4506840.",
"description": "Footer text shown on donation receipts explaining tax deductibility and Signal's mission"
},
"icu:Donations__OfflineTooltip": {
"messageformat": "You are offline. Please connect to the internet to complete a donation.",
"description": "Tooltip for donation related buttons which are temporarily disabled due to the app being offline."
},
"icu:Donations__Toast__Canceled": {
"messageformat": "Donation canceled",
"description": "Toast shown when a donation was manually canceled by the user"
@@ -417,5 +417,6 @@
padding-block: 5px;
padding-inline: 12px;
font-weight: 400;
border: 0.5px solid variables.$color-black-alpha-16;
border-radius: 6px;
}
+1
View File
@@ -220,6 +220,7 @@ function renderDonationsPane(props: {
clearWorkflow={action('clearWorkflow')}
initialCurrency="usd"
resumeWorkflow={action('resumeWorkflow')}
isOnline
isStaging
page={props.page}
setPage={props.setPage}
+49 -19
View File
@@ -61,6 +61,7 @@ import {
} from './preferences/donations/DonateInputCardCvc';
import { I18n } from './I18n';
import { strictAssert } from '../util/assert';
import { DonationsOfflineTooltip } from './conversation/DonationsOfflineTooltip';
import { DonateInputAmount } from './preferences/donations/DonateInputAmount';
const SUPPORT_URL = 'https://support.signal.org/hc/requests/new?desktop';
@@ -68,6 +69,7 @@ const SUPPORT_URL = 'https://support.signal.org/hc/requests/new?desktop';
export type PropsDataType = {
i18n: LocalizerType;
initialCurrency: string;
isOnline: boolean;
donationAmountsConfig: OneTimeDonationHumanAmounts | undefined;
lastError: DonationErrorType | undefined;
validCurrencies: ReadonlyArray<string>;
@@ -92,6 +94,7 @@ export function PreferencesDonateFlow({
contentsRef,
i18n,
initialCurrency,
isOnline,
donationAmountsConfig,
lastError,
validCurrencies,
@@ -184,6 +187,7 @@ export function PreferencesDonateFlow({
i18n={i18n}
initialAmount={amount}
initialCurrency={currency}
isOnline={isOnline}
donationAmountsConfig={donationAmountsConfig}
validCurrencies={validCurrencies}
onChangeCurrency={handleAmountPickerCurrencyChanged}
@@ -206,6 +210,7 @@ export function PreferencesDonateFlow({
disabled={isCardFormDisabled}
i18n={i18n}
initialValues={cardFormValues}
isOnline={isOnline}
onChange={handleCardFormChanged}
onSubmit={handleSubmitDonation}
showPrivacyModal={showPrivacyModal}
@@ -252,6 +257,7 @@ type AmountPickerProps = {
i18n: LocalizerType;
initialAmount: HumanDonationAmount | undefined;
initialCurrency: string | undefined;
isOnline: boolean;
donationAmountsConfig: OneTimeDonationHumanAmounts | undefined;
validCurrencies: ReadonlyArray<string>;
onChangeCurrency: (value: string) => void;
@@ -263,6 +269,7 @@ function AmountPicker({
i18n,
initialAmount,
initialCurrency = 'usd',
isOnline,
validCurrencies,
onChangeCurrency,
onSubmit,
@@ -369,7 +376,7 @@ function AmountPicker({
}, []);
const amount = parsedCustomAmount ?? presetAmount;
const isContinueEnabled = currency != null && amount != null;
const isContinueEnabled = isOnline && currency != null && amount != null;
const handleContinueClicked = useCallback(() => {
if (!isContinueEnabled) {
@@ -388,6 +395,17 @@ function AmountPicker({
customInputClassName = 'DonationAmountPicker__CustomInput';
}
const continueButton = (
<Button
className="PreferencesDonations__PrimaryButton"
disabled={!isContinueEnabled}
onClick={handleContinueClicked}
variant={isOnline ? ButtonVariant.Primary : ButtonVariant.Secondary}
>
Continue
</Button>
);
return (
<div className="DonationAmountPicker">
<Select
@@ -431,14 +449,13 @@ function AmountPicker({
/>
</div>
<div className="DonationAmountPicker__PrimaryButtonContainer">
<Button
className="PreferencesDonations__PrimaryButton"
disabled={!isContinueEnabled}
onClick={handleContinueClicked}
variant={ButtonVariant.Primary}
>
Continue
</Button>
{isOnline ? (
continueButton
) : (
<DonationsOfflineTooltip i18n={i18n}>
{continueButton}
</DonationsOfflineTooltip>
)}
</div>
</div>
);
@@ -456,6 +473,7 @@ type CardFormProps = {
disabled: boolean;
i18n: LocalizerType;
initialValues: CardFormValues | undefined;
isOnline: boolean;
onChange: (values: CardFormValues) => void;
onSubmit: (cardDetail: CardDetail) => void;
showPrivacyModal: () => void;
@@ -467,6 +485,7 @@ function CardForm({
disabled,
i18n,
initialValues,
isOnline,
onChange,
onSubmit,
showPrivacyModal,
@@ -566,6 +585,7 @@ function CardForm({
const isDonateDisabled =
disabled ||
!isOnline ||
cardNumber === '' ||
cardExpiration === '' ||
cardCvc === '' ||
@@ -573,6 +593,19 @@ function CardForm({
cardExpirationError != null ||
cardCvcError != null;
const donateButton = (
<Button
className="PreferencesDonations__PrimaryButton"
disabled={isDonateDisabled}
onClick={handleDonateClicked}
variant={isOnline ? ButtonVariant.Primary : ButtonVariant.Secondary}
>
{i18n('icu:PreferencesDonations__donate-button-with-amount', {
formattedCurrencyAmount,
})}
</Button>
);
return (
<div className="DonationCardForm">
<div className="DonationCardForm__Header--Info PreferencesDonations__section-header">
@@ -658,16 +691,13 @@ function CardForm({
</div>
</div>
<div className="DonationCardForm__PrimaryButtonContainer">
<Button
className="PreferencesDonations__PrimaryButton"
disabled={isDonateDisabled}
onClick={handleDonateClicked}
variant={ButtonVariant.Primary}
>
{i18n('icu:PreferencesDonations__donate-button-with-amount', {
formattedCurrencyAmount,
})}
</Button>
{isOnline ? (
donateButton
) : (
<DonationsOfflineTooltip i18n={i18n}>
{donateButton}
</DonationsOfflineTooltip>
)}
</div>
</div>
);
+32 -12
View File
@@ -42,6 +42,7 @@ import { DonationErrorModal } from './DonationErrorModal';
import { DonationVerificationModal } from './DonationVerificationModal';
import { DonationProgressModal } from './DonationProgressModal';
import { DonationStillProcessingModal } from './DonationStillProcessingModal';
import { DonationsOfflineTooltip } from './conversation/DonationsOfflineTooltip';
const log = createLogger('PreferencesDonations');
@@ -52,6 +53,7 @@ type PropsExternalType = {
export type PropsDataType = {
i18n: LocalizerType;
initialCurrency: string;
isOnline: boolean;
isStaging: boolean;
page: SettingsPage;
didResumeWorkflowAtStartup: boolean;
@@ -94,7 +96,12 @@ type DonationPage =
type PreferencesHomeProps = Pick<
PropsType,
'contentsRef' | 'i18n' | 'setPage' | 'isStaging' | 'donationReceipts'
| 'contentsRef'
| 'i18n'
| 'setPage'
| 'isOnline'
| 'isStaging'
| 'donationReceipts'
> & {
navigateToPage: (newPage: SettingsPage) => void;
renderDonationHero: () => JSX.Element;
@@ -175,25 +182,35 @@ function DonationsHome({
renderDonationHero,
navigateToPage,
setPage,
isOnline,
isStaging,
donationReceipts,
}: PreferencesHomeProps): JSX.Element {
const hasReceipts = donationReceipts.length > 0;
const donateButton = (
<Button
className="PreferencesDonations__PrimaryButton PreferencesDonations__donate-button"
disabled={!isOnline}
variant={isOnline ? ButtonVariant.Primary : ButtonVariant.Secondary}
size={ButtonSize.Medium}
onClick={() => {
setPage(SettingsPage.DonationsDonateFlow);
}}
>
{i18n('icu:PreferencesDonations__donate-button')}
</Button>
);
return (
<div className="PreferencesDonations">
{renderDonationHero()}
{isStaging && (
<Button
className="PreferencesDonations__PrimaryButton PreferencesDonations__donate-button"
variant={ButtonVariant.Primary}
size={ButtonSize.Medium}
onClick={() => {
setPage(SettingsPage.DonationsDonateFlow);
}}
>
{i18n('icu:PreferencesDonations__donate-button')}
</Button>
{isStaging && isOnline ? (
donateButton
) : (
<DonationsOfflineTooltip i18n={i18n}>
{donateButton}
</DonationsOfflineTooltip>
)}
<hr className="PreferencesDonations__separator" />
@@ -455,6 +472,7 @@ export function PreferencesDonations({
contentsRef,
i18n,
initialCurrency,
isOnline,
isStaging,
page,
workflow,
@@ -619,6 +637,7 @@ export function PreferencesDonations({
<PreferencesDonateFlow
contentsRef={contentsRef}
i18n={i18n}
isOnline={isOnline}
initialCurrency={initialCurrency}
donationAmountsConfig={donationAmountsConfig}
lastError={lastError}
@@ -641,6 +660,7 @@ export function PreferencesDonations({
<DonationsHome
contentsRef={contentsRef}
i18n={i18n}
isOnline={isOnline}
navigateToPage={navigateToPage}
donationReceipts={donationReceipts}
isStaging={isStaging}
@@ -0,0 +1,34 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { offsetDistanceModifier } from '../../util/popperUtil';
import { Tooltip, TooltipPlacement } from '../Tooltip';
import type { LocalizerType } from '../../types/I18N';
type Props = {
i18n: LocalizerType;
children: React.ReactNode;
};
export function getTooltipContent(i18n: LocalizerType): string {
return i18n('icu:Donations__OfflineTooltip');
}
export function DonationsOfflineTooltip({
i18n,
children,
}: Props): JSX.Element {
return (
<Tooltip
className="InAnotherCallTooltip"
content={getTooltipContent(i18n)}
direction={TooltipPlacement.Top}
popperModifiers={[offsetDistanceModifier(15)]}
>
{children}
</Tooltip>
);
}
+4
View File
@@ -20,6 +20,7 @@ import { drop } from '../../util/drop';
import type { OneTimeDonationHumanAmounts } from '../../types/Donations';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { phoneNumberToCurrencyCode } from '../../services/donations';
import { getNetworkIsOnline } from '../selectors/network';
export const SmartPreferencesDonations = memo(
function SmartPreferencesDonations({
@@ -38,6 +39,8 @@ export const SmartPreferencesDonations = memo(
useState<OneTimeDonationHumanAmounts>();
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const isOnline = useSelector(getNetworkIsOnline);
const isStaging = isStagingServer();
const i18n = useSelector(getIntl);
const theme = useSelector(getTheme);
@@ -91,6 +94,7 @@ export const SmartPreferencesDonations = memo(
showToast={showToast}
contentsRef={contentsRef}
initialCurrency={initialCurrency}
isOnline={isOnline}
isStaging={isStaging}
page={page}
didResumeWorkflowAtStartup={donationsState.didResumeWorkflowAtStartup}