Style donation amount picker
Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com>
This commit is contained in:
parent
52c57e3000
commit
3a617fb9ef
11 changed files with 414 additions and 98 deletions
|
@ -8866,6 +8866,18 @@
|
|||
"messageformat": "Date paid",
|
||||
"description": "Label for the payment date in donation receipt modal"
|
||||
},
|
||||
"icu:DonateFlow__make-a-one-time-donation": {
|
||||
"messageformat": "Make a one time donation",
|
||||
"description": "In the Donations settings section after beginning the donations process, then the user selects currency and amount which they want to donate. This string is the section header above currency amount options for a one time donation."
|
||||
},
|
||||
"icu:DonateFlow__desktop-one-time-only-notice": {
|
||||
"messageformat": "Only one time donations are available on desktop. Monthly donations can be made on your mobile device.",
|
||||
"description": "In the Donations settings section after beginning the donations process, then the user selects currency and amount which they want to donate. This footer notice indicates that only one time donations are available on desktop, and recurring donations must be processed on the mobile device."
|
||||
},
|
||||
"icu:DonateFlow__having-issues-contact-support": {
|
||||
"messageformat": "Having issues? <contactSupportLink>Contact support</contactSupportLink>",
|
||||
"description": "In the Donations settings section, this footer text appears during parts of the donation workflow such as when picking a donation currency and amount, or entering the credit card info."
|
||||
},
|
||||
"icu:DonationReceipt__title": {
|
||||
"messageformat": "Donation receipt",
|
||||
"description": "Title shown at the top of donation receipt documents"
|
||||
|
|
|
@ -40,6 +40,7 @@ $color-gray-65: #4a4a4a;
|
|||
$color-gray-75: #3b3b3b;
|
||||
$color-gray-78: #343434;
|
||||
$color-gray-80: #2e2e2e;
|
||||
$color-gray-85: #262626;
|
||||
$color-gray-90: #1b1b1b;
|
||||
$color-gray-95: #121212;
|
||||
$color-black: #000000;
|
||||
|
|
149
stylesheets/components/DonationForm.scss
Normal file
149
stylesheets/components/DonationForm.scss
Normal file
|
@ -0,0 +1,149 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
@use '../mixins';
|
||||
@use '../variables';
|
||||
|
||||
.DonationForm {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.DonationForm__CurrencySelect {
|
||||
width: 78px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.DonationForm__CurrencySelect.module-select select {
|
||||
height: auto;
|
||||
min-width: auto;
|
||||
padding-block: 5px;
|
||||
}
|
||||
|
||||
.DonationForm .DonationForm__CurrencySelect.module-select select {
|
||||
border-color: light-dark(
|
||||
variables.$color-gray-25,
|
||||
variables.$color-white-alpha-12
|
||||
);
|
||||
}
|
||||
|
||||
.DonationForm__HelpFooter {
|
||||
@include mixins.font-body-small;
|
||||
flex-grow: 1;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-50,
|
||||
variables.$color-white-alpha-50
|
||||
);
|
||||
align-content: flex-end;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.DonationForm__HelpFooterDesktopOneTimeOnlyNotice {
|
||||
margin-block: 10px 17px;
|
||||
}
|
||||
|
||||
a.DonationFormHelpFooter__ContactSupportLink {
|
||||
color: variables.$color-ultramarine;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.DonationAmountPicker__AmountOptions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
max-width: 340px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.DonationAmountPicker__PresetButton,
|
||||
.DonationForm.PreferencesDonations
|
||||
.DonationAmountPicker__CustomInput__container,
|
||||
.DonationForm .DonationAmountPicker__CustomInput--selected__container,
|
||||
.DonationForm .DonationAmountPicker__CustomInput--with-error__container {
|
||||
margin-block: 5px;
|
||||
margin-inline: 5px;
|
||||
border-width: 0.5px;
|
||||
border-style: solid;
|
||||
border-radius: 6px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.DonationAmountPicker__PresetButton,
|
||||
.DonationForm .DonationForm__CurrencySelect.module-select select,
|
||||
.DonationForm.PreferencesDonations
|
||||
.DonationAmountPicker__CustomInput__container,
|
||||
.DonationForm.PreferencesDonations
|
||||
.DonationAmountPicker__CustomInput--selected__container,
|
||||
.DonationForm.PreferencesDonations
|
||||
.DonationAmountPicker__CustomInput--with-error__container {
|
||||
background-color: light-dark(
|
||||
variables.$color-white,
|
||||
variables.$color-gray-85
|
||||
);
|
||||
border-color: light-dark(
|
||||
variables.$color-gray-25,
|
||||
variables.$color-white-alpha-12
|
||||
);
|
||||
}
|
||||
|
||||
.DonationAmountPicker__PresetButton {
|
||||
@include mixins.font-body-1;
|
||||
width: 100px;
|
||||
padding-inline: 12px;
|
||||
padding-block: 14px;
|
||||
margin-block: 5px;
|
||||
margin-inline: 5px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.DonationAmountPicker__PresetButton--selected,
|
||||
.DonationForm .DonationAmountPicker__CustomInput--selected__container,
|
||||
.DonationForm
|
||||
.DonationAmountPicker__CustomInput--with-error__container:focus-within,
|
||||
.DonationForm .DonationAmountPicker__CustomInput__container:focus-within {
|
||||
border-color: variables.$color-ultramarine;
|
||||
outline: 2.5px solid variables.$color-ultramarine;
|
||||
outline-offset: -2.5px;
|
||||
}
|
||||
|
||||
.DonationForm .DonationAmountPicker__CustomInput__container,
|
||||
.DonationForm .DonationAmountPicker__CustomInput--selected__container,
|
||||
.DonationForm .DonationAmountPicker__CustomInput--with-error__container {
|
||||
width: 320px;
|
||||
padding-block: 0;
|
||||
border-width: 0.5px;
|
||||
}
|
||||
|
||||
.DonationForm
|
||||
.DonationAmountPicker__CustomInput--with-error__container:not(:focus-within) {
|
||||
border-color: variables.$color-deep-red;
|
||||
outline: 2.5px solid variables.$color-deep-red;
|
||||
outline-offset: -2.5px;
|
||||
}
|
||||
|
||||
.DonationForm .DonationAmountPicker__CustomInput__input,
|
||||
.DonationForm .DonationAmountPicker__CustomInput--selected__input,
|
||||
.DonationForm .DonationAmountPicker__CustomInput--with-error__input {
|
||||
@include mixins.font-body-1;
|
||||
padding-inline: 12px;
|
||||
padding-block: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.DonationAmountPicker__CustomInput__input:not(:focus)::placeholder {
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-85,
|
||||
variables.$color-white-alpha-85
|
||||
);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.DonationAmountPicker__CustomInput__input:focus::placeholder,
|
||||
.DonationAmountPicker__CustomInput--selected__input:focus::placeholder,
|
||||
.DonationAmountPicker__CustomInput--with-error__input:focus::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.DonationAmountPicker__PrimaryButtonContainer {
|
||||
margin-block-start: 11px;
|
||||
margin-inline-end: 10px;
|
||||
text-align: end;
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 590px;
|
||||
padding-block: 0;
|
||||
padding-inline: 0;
|
||||
margin-inline-start: 24px;
|
||||
|
@ -15,14 +16,14 @@
|
|||
|
||||
&__title {
|
||||
@include mixins.font-title-medium;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
@include mixins.font-body-2;
|
||||
text-align: center;
|
||||
max-width: 320px;
|
||||
margin-bottom: 24px;
|
||||
margin-block-end: 12px;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-50,
|
||||
variables.$color-white-alpha-50
|
||||
|
@ -42,7 +43,7 @@
|
|||
}
|
||||
|
||||
&__donate-button {
|
||||
margin-bottom: 24px;
|
||||
margin-block-end: 32px;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
|
@ -60,13 +61,20 @@
|
|||
&__section-header {
|
||||
@include mixins.font-body-2-bold;
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
margin-block: 12px;
|
||||
padding-inline: 8px;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-85,
|
||||
variables.$color-white-alpha-85
|
||||
);
|
||||
|
||||
&--my-support {
|
||||
margin-block-start: 6px;
|
||||
}
|
||||
|
||||
&--donate-flow {
|
||||
margin-block-start: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
|
@ -161,9 +169,9 @@
|
|||
|
||||
&__mobile-info {
|
||||
@include mixins.font-subtitle;
|
||||
margin-top: 18px;
|
||||
align-self: flex-start;
|
||||
padding-inline: 8px;
|
||||
margin-block-start: 10px;
|
||||
align-self: flex-start;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-50,
|
||||
variables.$color-white-alpha-50
|
||||
|
@ -171,6 +179,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.PreferencesDonations__avatar {
|
||||
margin-block-end: 12px;
|
||||
}
|
||||
|
||||
// Receipts page specific styles
|
||||
.PreferencesDonations--receiptList {
|
||||
&__info {
|
||||
|
@ -386,3 +398,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.PreferencesDonations__PrimaryButton {
|
||||
@include mixins.font-body-2;
|
||||
padding-block: 5px;
|
||||
padding-inline: 12px;
|
||||
font-weight: 400;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
@use 'components/DisappearingTimeDialog.scss';
|
||||
@use 'components/DisappearingTimerSelect.scss';
|
||||
@use 'components/DonationErrorModal.scss';
|
||||
@use 'components/DonationForm.scss';
|
||||
@use 'components/DonationProgressModal.scss';
|
||||
@use 'components/DonationStillProcessingModal.scss';
|
||||
@use 'components/DonationVerificationModal.scss';
|
||||
|
|
|
@ -46,6 +46,7 @@ export enum AvatarSize {
|
|||
FORTY = 40,
|
||||
FORTY_EIGHT = 48,
|
||||
FIFTY_TWO = 52,
|
||||
SEVENTY_TWO = 72,
|
||||
SIXTY_FOUR = 64,
|
||||
EIGHTY = 80,
|
||||
NINETY_SIX = 96,
|
||||
|
|
|
@ -34,6 +34,7 @@ export type PropsType = {
|
|||
moduleClassName?: string;
|
||||
onChange: (value: string) => unknown;
|
||||
onBlur?: () => unknown;
|
||||
onFocus?: () => unknown;
|
||||
onEnter?: () => unknown;
|
||||
placeholder: string;
|
||||
value?: string;
|
||||
|
@ -80,6 +81,7 @@ export const Input = forwardRef<
|
|||
moduleClassName,
|
||||
onChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onEnter,
|
||||
placeholder,
|
||||
value = '',
|
||||
|
@ -220,6 +222,7 @@ export const Input = forwardRef<
|
|||
spellCheck: !disableSpellcheck,
|
||||
onChange: handleChange,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onKeyDown: handleKeyDown,
|
||||
onPaste: handlePaste,
|
||||
placeholder,
|
||||
|
|
|
@ -195,6 +195,7 @@ function RenderProfileEditor(): JSX.Element {
|
|||
function RenderDonationsPane(props: {
|
||||
me: typeof me;
|
||||
donationReceipts: ReadonlyArray<DonationReceipt>;
|
||||
page: SettingsPage;
|
||||
saveAttachmentToDisk: (options: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
|
@ -212,12 +213,12 @@ function RenderDonationsPane(props: {
|
|||
i18n={i18n}
|
||||
contentsRef={contentsRef}
|
||||
clearWorkflow={action('clearWorkflow')}
|
||||
isStaging={false}
|
||||
page={SettingsPage.Donations}
|
||||
isStaging
|
||||
page={props.page}
|
||||
setPage={action('setPage')}
|
||||
submitDonation={action('submitDonation')}
|
||||
workflow={undefined}
|
||||
userAvatarData={[]}
|
||||
badge={undefined}
|
||||
color={props.me.color}
|
||||
firstName={props.me.firstName}
|
||||
profileAvatarUrl={props.me.profileAvatarUrl}
|
||||
|
@ -227,6 +228,7 @@ function RenderDonationsPane(props: {
|
|||
saveAttachmentToDisk={props.saveAttachmentToDisk}
|
||||
generateDonationReceiptBlob={props.generateDonationReceiptBlob}
|
||||
showToast={props.showToast}
|
||||
theme={ThemeType.light}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -353,6 +355,7 @@ export default {
|
|||
RenderDonationsPane({
|
||||
me,
|
||||
donationReceipts: [],
|
||||
page: SettingsPage.Donations,
|
||||
saveAttachmentToDisk: async () => {
|
||||
action('saveAttachmentToDisk')();
|
||||
return { fullPath: '/mock/path/to/file.png', name: 'file.png' };
|
||||
|
@ -516,6 +519,26 @@ Donations.args = {
|
|||
donationsFeatureEnabled: true,
|
||||
page: SettingsPage.Donations,
|
||||
};
|
||||
export const DonationsDonateFlow = Template.bind({});
|
||||
DonationsDonateFlow.args = {
|
||||
donationsFeatureEnabled: true,
|
||||
page: SettingsPage.DonationsDonateFlow,
|
||||
renderDonationsPane: () =>
|
||||
RenderDonationsPane({
|
||||
me,
|
||||
donationReceipts: [],
|
||||
page: SettingsPage.DonationsDonateFlow,
|
||||
saveAttachmentToDisk: async () => {
|
||||
action('saveAttachmentToDisk')();
|
||||
return { fullPath: '/mock/path/to/file.png', name: 'file.png' };
|
||||
},
|
||||
generateDonationReceiptBlob: async () => {
|
||||
action('generateDonationReceiptBlob')();
|
||||
return new Blob();
|
||||
},
|
||||
showToast: action('showToast'),
|
||||
}),
|
||||
};
|
||||
export const Internal = Template.bind({});
|
||||
Internal.args = {
|
||||
page: SettingsPage.Internal,
|
||||
|
|
|
@ -10,6 +10,7 @@ import React, {
|
|||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
|
@ -55,12 +56,16 @@ import {
|
|||
DonateInputCardCvc,
|
||||
getCardCvcErrorMessage,
|
||||
} from './preferences/donations/DonateInputCardCvc';
|
||||
import { I18n } from './I18n';
|
||||
|
||||
const SUPPORT_URL = 'https://support.signal.org/hc/requests/new?desktop';
|
||||
|
||||
export type PropsDataType = {
|
||||
i18n: LocalizerType;
|
||||
donationAmountsConfig: OneTimeDonationHumanAmounts | undefined;
|
||||
validCurrencies: ReadonlyArray<string>;
|
||||
workflow: DonationWorkflow | undefined;
|
||||
renderDonationHero: () => JSX.Element;
|
||||
};
|
||||
|
||||
type PropsHousekeepingType = {
|
||||
|
@ -82,6 +87,7 @@ export function PreferencesDonateFlow({
|
|||
validCurrencies,
|
||||
workflow,
|
||||
clearWorkflow,
|
||||
renderDonationHero,
|
||||
submitDonation,
|
||||
onBack,
|
||||
}: PropsType): JSX.Element {
|
||||
|
@ -191,8 +197,8 @@ export function PreferencesDonateFlow({
|
|||
// TODO: DESKTOP-8950
|
||||
};
|
||||
|
||||
confirmDiscardIf(true, onDiscard);
|
||||
}, [confirmDiscardIf]);
|
||||
confirmDiscardIf(step === 'paymentDetails', onDiscard);
|
||||
}, [confirmDiscardIf, step]);
|
||||
tryClose.current = onTryClose;
|
||||
|
||||
let innerContent: JSX.Element;
|
||||
|
@ -200,14 +206,18 @@ export function PreferencesDonateFlow({
|
|||
|
||||
if (step === 'amount') {
|
||||
innerContent = (
|
||||
<AmountPicker
|
||||
i18n={i18n}
|
||||
initialAmount={amount}
|
||||
initialCurrency={currency}
|
||||
donationAmountsConfig={donationAmountsConfig}
|
||||
validCurrencies={validCurrencies}
|
||||
onSubmit={handleAmountPickerResult}
|
||||
/>
|
||||
<>
|
||||
{renderDonationHero()}
|
||||
<AmountPicker
|
||||
i18n={i18n}
|
||||
initialAmount={amount}
|
||||
initialCurrency={currency}
|
||||
donationAmountsConfig={donationAmountsConfig}
|
||||
validCurrencies={validCurrencies}
|
||||
onSubmit={handleAmountPickerResult}
|
||||
/>
|
||||
<HelpFooter i18n={i18n} showOneTimeOnlyNotice />
|
||||
</>
|
||||
);
|
||||
// Dismiss DonateFlow and return to Donations home
|
||||
handleBack = () => onBack();
|
||||
|
@ -287,10 +297,10 @@ export function PreferencesDonateFlow({
|
|||
/>
|
||||
);
|
||||
const content = (
|
||||
<>
|
||||
<div className="PreferencesDonations DonationForm">
|
||||
{confirmDiscardModal}
|
||||
{innerContent}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -321,16 +331,16 @@ function AmountPicker({
|
|||
donationAmountsConfig,
|
||||
i18n,
|
||||
initialAmount,
|
||||
initialCurrency,
|
||||
initialCurrency = 'usd',
|
||||
validCurrencies,
|
||||
onSubmit,
|
||||
}: AmountPickerProps): JSX.Element {
|
||||
const [currency, setCurrency] = useState(initialCurrency ?? 'usd');
|
||||
const [currency, setCurrency] = useState(initialCurrency);
|
||||
|
||||
const [presetAmount, setPresetAmount] = useState<
|
||||
HumanDonationAmount | undefined
|
||||
>(initialAmount);
|
||||
const [customAmount, setCustomAmount] = useState<string>();
|
||||
>();
|
||||
const [customAmount, setCustomAmount] = useState<string | undefined>();
|
||||
|
||||
// Reset amount selections when API donation config or selected currency changes
|
||||
// Memo here so preset options instantly load when component mounts.
|
||||
|
@ -345,9 +355,17 @@ function AmountPicker({
|
|||
}, [donationAmountsConfig, currency]);
|
||||
|
||||
useEffect(() => {
|
||||
setCustomAmount(undefined);
|
||||
setPresetAmount(undefined);
|
||||
}, [donationAmountsConfig, currency]);
|
||||
if (
|
||||
initialAmount &&
|
||||
presetAmountOptions.find(option => option === initialAmount)
|
||||
) {
|
||||
setPresetAmount(initialAmount);
|
||||
setCustomAmount(undefined);
|
||||
} else {
|
||||
setPresetAmount(undefined);
|
||||
setCustomAmount(initialAmount?.toString());
|
||||
}
|
||||
}, [initialAmount, presetAmountOptions]);
|
||||
|
||||
const minimumAmount = useMemo<HumanDonationAmount>(() => {
|
||||
if (!donationAmountsConfig || !donationAmountsConfig[currency]) {
|
||||
|
@ -422,51 +440,103 @@ function AmountPicker({
|
|||
onSubmit({ amount, currency });
|
||||
}, [amount, currency, isContinueEnabled, onSubmit]);
|
||||
|
||||
let customInputClassName;
|
||||
if (error) {
|
||||
customInputClassName = 'DonationAmountPicker__CustomInput--with-error';
|
||||
} else if (parsedCustomAmount) {
|
||||
customInputClassName = 'DonationAmountPicker__CustomInput--selected';
|
||||
} else {
|
||||
customInputClassName = 'DonationAmountPicker__CustomInput';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="DonationAmountPicker">
|
||||
<Select
|
||||
moduleClassName="DonationForm__CurrencySelect"
|
||||
id="currency"
|
||||
options={currencyOptionsForSelect}
|
||||
onChange={handleCurrencyChanged}
|
||||
value={currency}
|
||||
/>
|
||||
<div>
|
||||
<div className="PreferencesDonations__section-header PreferencesDonations__section-header--donate-flow">
|
||||
{i18n('icu:DonateFlow__make-a-one-time-donation')}
|
||||
</div>
|
||||
<div className="DonationAmountPicker__AmountOptions">
|
||||
{presetAmountOptions.map(value => (
|
||||
<Button
|
||||
<button
|
||||
className={classNames({
|
||||
DonationAmountPicker__PresetButton: true,
|
||||
'DonationAmountPicker__PresetButton--selected':
|
||||
presetAmount === value,
|
||||
})}
|
||||
key={value}
|
||||
onClick={() => {
|
||||
setCustomAmount(undefined);
|
||||
setPresetAmount(value);
|
||||
}}
|
||||
variant={
|
||||
presetAmount === value
|
||||
? ButtonVariant.SecondaryAffirmative
|
||||
: ButtonVariant.Secondary
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
{toHumanCurrencyString({ amount: value, currency })}
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<label htmlFor="customAmount">Custom Amount</label>
|
||||
<div>
|
||||
<Input
|
||||
moduleClassName={customInputClassName}
|
||||
id="customAmount"
|
||||
i18n={i18n}
|
||||
onChange={handleCustomAmountChanged}
|
||||
onFocus={() => setPresetAmount(undefined)}
|
||||
placeholder="Enter Custom Amount"
|
||||
value={customAmount}
|
||||
/>
|
||||
<span>{currency.toUpperCase()}</span>
|
||||
</div>
|
||||
{error && <div>Error: {error}</div>}
|
||||
<Button
|
||||
disabled={!isContinueEnabled}
|
||||
onClick={handleContinueClicked}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<div className="DonationAmountPicker__PrimaryButtonContainer">
|
||||
<Button
|
||||
className="PreferencesDonations__PrimaryButton"
|
||||
disabled={!isContinueEnabled}
|
||||
onClick={handleContinueClicked}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type HelpFooterProps = {
|
||||
i18n: LocalizerType;
|
||||
showOneTimeOnlyNotice?: boolean;
|
||||
};
|
||||
|
||||
function HelpFooter({
|
||||
i18n,
|
||||
showOneTimeOnlyNotice,
|
||||
}: HelpFooterProps): JSX.Element {
|
||||
const contactSupportLink = (parts: Array<string | JSX.Element>) => (
|
||||
<a
|
||||
className="DonationFormHelpFooter__ContactSupportLink"
|
||||
href={SUPPORT_URL}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{parts}
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="DonationForm__HelpFooter">
|
||||
{showOneTimeOnlyNotice && (
|
||||
<div className="DonationForm__HelpFooterDesktopOneTimeOnlyNotice">
|
||||
{i18n('icu:DonateFlow__desktop-one-time-only-notice')}
|
||||
</div>
|
||||
)}
|
||||
<I18n
|
||||
id="icu:DonateFlow__having-issues-contact-support"
|
||||
i18n={i18n}
|
||||
components={{
|
||||
contactSupportLink,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import type { MutableRefObject, ReactNode } from 'react';
|
|||
import { ListBox, ListBoxItem } from 'react-aria-components';
|
||||
import { getDateTimeFormatter } from '../util/formatTimestamp';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util';
|
||||
import { PreferencesContent } from './Preferences';
|
||||
import { SettingsPage } from '../types/Nav';
|
||||
import { PreferencesDonateFlow } from './PreferencesDonateFlow';
|
||||
|
@ -18,8 +18,6 @@ import type {
|
|||
OneTimeDonationHumanAmounts,
|
||||
} from '../types/Donations';
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
import type { AvatarDataType } from '../types/Avatar';
|
||||
import { AvatarPreview } from './AvatarPreview';
|
||||
import { Button, ButtonSize, ButtonVariant } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
import { Spinner } from './Spinner';
|
||||
|
@ -32,6 +30,8 @@ import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
|||
import { DonationPrivacyInformationModal } from './DonationPrivacyInformationModal';
|
||||
import type { SubmitDonationType } from '../state/ducks/donations';
|
||||
import { getHumanDonationAmount } from '../util/currency';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import type { BadgeType } from '../badges/types';
|
||||
|
||||
const log = createLogger('PreferencesDonations');
|
||||
|
||||
|
@ -44,13 +44,14 @@ export type PropsDataType = {
|
|||
isStaging: boolean;
|
||||
page: SettingsPage;
|
||||
workflow: DonationWorkflow | undefined;
|
||||
userAvatarData: ReadonlyArray<AvatarDataType>;
|
||||
color?: AvatarColorType;
|
||||
firstName?: string;
|
||||
badge: BadgeType | undefined;
|
||||
color: AvatarColorType | undefined;
|
||||
firstName: string | undefined;
|
||||
profileAvatarUrl?: string;
|
||||
donationAmountsConfig: OneTimeDonationHumanAmounts | undefined;
|
||||
validCurrencies: ReadonlyArray<string>;
|
||||
donationReceipts: ReadonlyArray<DonationReceipt>;
|
||||
theme: ThemeType;
|
||||
saveAttachmentToDisk: (options: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
|
@ -76,8 +77,9 @@ type DonationPage =
|
|||
| SettingsPage.DonationsDonateFlow
|
||||
| SettingsPage.DonationsReceiptList;
|
||||
|
||||
type PreferencesHomeProps = PropsType & {
|
||||
type PreferencesHomeProps = Omit<PropsType, 'badge' | 'theme'> & {
|
||||
navigateToPage: (newPage: SettingsPage) => void;
|
||||
renderDonationHero: () => JSX.Element;
|
||||
};
|
||||
|
||||
function isDonationPage(page: SettingsPage): page is DonationPage {
|
||||
|
@ -88,20 +90,19 @@ function isDonationPage(page: SettingsPage): page is DonationPage {
|
|||
);
|
||||
}
|
||||
|
||||
function DonationsHome({
|
||||
i18n,
|
||||
userAvatarData,
|
||||
type DonationHeroProps = Pick<
|
||||
PropsDataType,
|
||||
'badge' | 'color' | 'firstName' | 'i18n' | 'profileAvatarUrl' | 'theme'
|
||||
>;
|
||||
|
||||
function DonationHero({
|
||||
badge,
|
||||
color,
|
||||
firstName,
|
||||
i18n,
|
||||
profileAvatarUrl,
|
||||
navigateToPage,
|
||||
setPage,
|
||||
isStaging,
|
||||
donationReceipts,
|
||||
}: PreferencesHomeProps): JSX.Element {
|
||||
const avatarData = userAvatarData[0];
|
||||
const avatarBuffer = avatarData?.buffer;
|
||||
const hasReceipts = donationReceipts.length > 0;
|
||||
theme,
|
||||
}: DonationHeroProps): JSX.Element {
|
||||
const [showPrivacyModal, setShowPrivacyModal] = useState(false);
|
||||
|
||||
const ReadMoreButtonWithModal = useCallback(
|
||||
|
@ -122,18 +123,25 @@ function DonationsHome({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="PreferencesDonations">
|
||||
<div className="PreferencesDonations__avatar">
|
||||
<AvatarPreview
|
||||
avatarColor={color}
|
||||
avatarUrl={profileAvatarUrl}
|
||||
avatarValue={avatarBuffer}
|
||||
conversationTitle={firstName || i18n('icu:unknownContact')}
|
||||
<>
|
||||
{showPrivacyModal && (
|
||||
<DonationPrivacyInformationModal
|
||||
i18n={i18n}
|
||||
style={{
|
||||
height: 80,
|
||||
width: 80,
|
||||
}}
|
||||
onClose={() => setShowPrivacyModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="PreferencesDonations__avatar">
|
||||
<Avatar
|
||||
avatarUrl={profileAvatarUrl}
|
||||
badge={badge}
|
||||
color={color}
|
||||
conversationType="direct"
|
||||
title={firstName ?? ''}
|
||||
i18n={i18n}
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.SEVENTY_TWO}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
<div className="PreferencesDonations__title">
|
||||
|
@ -148,9 +156,26 @@ function DonationsHome({
|
|||
id="icu:PreferencesDonations__description"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DonationsHome({
|
||||
i18n,
|
||||
renderDonationHero,
|
||||
navigateToPage,
|
||||
setPage,
|
||||
isStaging,
|
||||
donationReceipts,
|
||||
}: PreferencesHomeProps): JSX.Element {
|
||||
const hasReceipts = donationReceipts.length > 0;
|
||||
|
||||
return (
|
||||
<div className="PreferencesDonations">
|
||||
{renderDonationHero()}
|
||||
{isStaging && (
|
||||
<Button
|
||||
className="PreferencesDonations__donate-button"
|
||||
className="PreferencesDonations__PrimaryButton PreferencesDonations__donate-button"
|
||||
variant={ButtonVariant.Primary}
|
||||
size={ButtonSize.Medium}
|
||||
onClick={() => {
|
||||
|
@ -164,7 +189,7 @@ function DonationsHome({
|
|||
<hr className="PreferencesDonations__separator" />
|
||||
|
||||
{hasReceipts && (
|
||||
<div className="PreferencesDonations__section-header">
|
||||
<div className="PreferencesDonations__section-header PreferencesDonations__section-header--my-support">
|
||||
{i18n('icu:PreferencesDonations__my-support')}
|
||||
</div>
|
||||
)}
|
||||
|
@ -203,13 +228,6 @@ function DonationsHome({
|
|||
<div className="PreferencesDonations__mobile-info">
|
||||
{i18n('icu:PreferencesDonations__mobile-info')}
|
||||
</div>
|
||||
|
||||
{showPrivacyModal && (
|
||||
<DonationPrivacyInformationModal
|
||||
i18n={i18n}
|
||||
onClose={() => setShowPrivacyModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -432,13 +450,14 @@ export function PreferencesDonations({
|
|||
clearWorkflow,
|
||||
setPage,
|
||||
submitDonation,
|
||||
userAvatarData,
|
||||
badge,
|
||||
color,
|
||||
firstName,
|
||||
profileAvatarUrl,
|
||||
donationAmountsConfig,
|
||||
validCurrencies,
|
||||
donationReceipts,
|
||||
theme,
|
||||
saveAttachmentToDisk,
|
||||
generateDonationReceiptBlob,
|
||||
showToast,
|
||||
|
@ -450,6 +469,20 @@ export function PreferencesDonations({
|
|||
[setPage]
|
||||
);
|
||||
|
||||
const renderDonationHero = useCallback(
|
||||
() => (
|
||||
<DonationHero
|
||||
badge={badge}
|
||||
color={color}
|
||||
firstName={firstName}
|
||||
i18n={i18n}
|
||||
profileAvatarUrl={profileAvatarUrl}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
[badge, color, firstName, i18n, profileAvatarUrl, theme]
|
||||
);
|
||||
|
||||
if (!isDonationPage(page)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -465,6 +498,7 @@ export function PreferencesDonations({
|
|||
validCurrencies={validCurrencies}
|
||||
workflow={workflow}
|
||||
clearWorkflow={clearWorkflow}
|
||||
renderDonationHero={renderDonationHero}
|
||||
submitDonation={submitDonation}
|
||||
onBack={() => setPage(SettingsPage.Donations)}
|
||||
/>
|
||||
|
@ -475,7 +509,6 @@ export function PreferencesDonations({
|
|||
<DonationsHome
|
||||
contentsRef={contentsRef}
|
||||
i18n={i18n}
|
||||
userAvatarData={userAvatarData}
|
||||
color={color}
|
||||
firstName={firstName}
|
||||
profileAvatarUrl={profileAvatarUrl}
|
||||
|
@ -490,6 +523,7 @@ export function PreferencesDonations({
|
|||
page={page}
|
||||
workflow={workflow}
|
||||
clearWorkflow={clearWorkflow}
|
||||
renderDonationHero={renderDonationHero}
|
||||
setPage={setPage}
|
||||
submitDonation={submitDonation}
|
||||
/>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useSelector } from 'react-redux';
|
|||
|
||||
import type { MutableRefObject } from 'react';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getIntl, getTheme } from '../selectors/user';
|
||||
import { getMe } from '../selectors/conversations';
|
||||
import { PreferencesDonations } from '../../components/PreferencesDonations';
|
||||
import type { SettingsPage } from '../../types/Nav';
|
||||
|
@ -18,6 +18,7 @@ import { useToastActions } from '../ducks/toast';
|
|||
import { getDonationHumanAmounts } from '../../util/subscriptionConfiguration';
|
||||
import { drop } from '../../util/drop';
|
||||
import type { OneTimeDonationHumanAmounts } from '../../types/Donations';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
|
||||
export const SmartPreferencesDonations = memo(
|
||||
function SmartPreferencesDonations({
|
||||
|
@ -35,24 +36,24 @@ export const SmartPreferencesDonations = memo(
|
|||
const [donationAmountsConfig, setDonationAmountsConfig] =
|
||||
useState<OneTimeDonationHumanAmounts>();
|
||||
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const isStaging = isStagingServer();
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
|
||||
const workflow = useSelector(
|
||||
(state: StateType) => state.donations.currentWorkflow
|
||||
);
|
||||
const { clearWorkflow, submitDonation } = useDonationsActions();
|
||||
|
||||
const {
|
||||
avatars: userAvatarData = [],
|
||||
color,
|
||||
firstName,
|
||||
profileAvatarUrl,
|
||||
} = useSelector(getMe);
|
||||
const { badges, color, firstName, profileAvatarUrl } = useSelector(getMe);
|
||||
const badge = getPreferredBadge(badges);
|
||||
|
||||
const { showToast } = useToastActions();
|
||||
const donationReceipts = useSelector(
|
||||
(state: StateType) => state.donations.receipts
|
||||
);
|
||||
|
||||
const { saveAttachmentToDisk } = window.Signal.Migrations;
|
||||
|
||||
// Eagerly load donation config from API when entering Donations Home so the
|
||||
|
@ -70,7 +71,7 @@ export const SmartPreferencesDonations = memo(
|
|||
return (
|
||||
<PreferencesDonations
|
||||
i18n={i18n}
|
||||
userAvatarData={userAvatarData}
|
||||
badge={badge}
|
||||
color={color}
|
||||
firstName={firstName}
|
||||
profileAvatarUrl={profileAvatarUrl}
|
||||
|
@ -87,6 +88,7 @@ export const SmartPreferencesDonations = memo(
|
|||
clearWorkflow={clearWorkflow}
|
||||
submitDonation={submitDonation}
|
||||
setPage={setPage}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue