Start Donations Receipt Flow UI
This commit is contained in:
parent
b04d3a9c7b
commit
70162be74e
13 changed files with 994 additions and 68 deletions
|
@ -816,6 +816,14 @@
|
|||
"messageformat": "Failed to process some frames during backup import. Please share your logs.",
|
||||
"description": "[Only shown to internal users] An error popup when we failed to process some parts of a backup import."
|
||||
},
|
||||
"icu:Toast--ReceiptSaved": {
|
||||
"messageformat": "Receipt saved",
|
||||
"description": "Toast message shown when a donation receipt has been successfully saved to disk"
|
||||
},
|
||||
"icu:Toast--ReceiptSaveFailed": {
|
||||
"messageformat": "Error saving receipt. Please try again.",
|
||||
"description": "Toast message shown when a donation receipt fails to save to disk"
|
||||
},
|
||||
"icu:cannotSelectPhotosAndVideosAlongWithFiles": {
|
||||
"messageformat": "You can't select photos and videos along with files.",
|
||||
"description": "An error popup when the user has attempted to add an attachment"
|
||||
|
@ -8802,29 +8810,82 @@
|
|||
"messageformat": "What's New",
|
||||
"description": "Title for the whats new modal"
|
||||
},
|
||||
"icu:PreferencesDonations__title": {
|
||||
"messageformat": "Privacy over Profit",
|
||||
"description": "Title shown at the top of the donations preferences page"
|
||||
},
|
||||
"icu:PreferencesDonations__description": {
|
||||
"messageformat": "Private messaging, funded by you. No ads, no tracking, no compromise. Donate now to support Signal. <learnMoreLink>Learn more</learnMoreLink>",
|
||||
"description": "Description text explaining Signal's donation model with learn more link"
|
||||
},
|
||||
"icu:PreferencesDonations__donate-button": {
|
||||
"messageformat": "Donate",
|
||||
"description": "Button text to make a donation"
|
||||
},
|
||||
"icu:PreferencesDonations__mobile-info": {
|
||||
"messageformat": "Badges and monthly donations can be managed on your mobile device.",
|
||||
"description": "Information about donations receipt syncing limitations"
|
||||
},
|
||||
|
||||
"icu:PreferencesDonations__receipts": {
|
||||
"messageformat": "Receipts",
|
||||
"description": "Menu item to view donation receipts"
|
||||
},
|
||||
|
||||
"icu:PreferencesDonations__faqs": {
|
||||
"messageformat": "Donation FAQs",
|
||||
"description": "Menu item to view donation FAQs"
|
||||
},
|
||||
|
||||
"icu:PreferencesDonations--receiptList__info": {
|
||||
"messageformat": "Receipts do not sync across devices. If you have reinstalled Signal, receipts from previous donations will not be available.",
|
||||
"description": "Information about donation receipts syncing limitations"
|
||||
},
|
||||
|
||||
"icu:PreferencesDonations--receiptList__empty-title": {
|
||||
"messageformat": "No Receipts",
|
||||
"description": "Title shown when there are no donation receipts"
|
||||
},
|
||||
|
||||
"icu:PreferencesDonations__ReceiptModal--title": {
|
||||
"messageformat": "Details",
|
||||
"description": "Title of the donation receipt details modal"
|
||||
},
|
||||
|
||||
"icu:PreferencesDonations__ReceiptModal--download": {
|
||||
"messageformat": "Download receipt",
|
||||
"description": "Button text to download a donation receipt"
|
||||
},
|
||||
|
||||
"icu:PreferencesDonations__ReceiptModal--type-label": {
|
||||
"messageformat": "Type",
|
||||
"description": "Label for the donation type in donation receipt modal"
|
||||
},
|
||||
|
||||
"icu:PreferencesDonations__ReceiptModal--date-paid-label": {
|
||||
"messageformat": "Date paid",
|
||||
"description": "Label for the payment date in donation receipt modal"
|
||||
},
|
||||
|
||||
"icu:DonationReceipt__title": {
|
||||
"messageformat": "Donation receipt",
|
||||
"description": "Title shown at the top of donation receipt documents"
|
||||
},
|
||||
"icu:DonationReceipt__amount-label": {
|
||||
"messageformat": "Amount",
|
||||
"description": "Label for the donation amount field on receipt"
|
||||
"description": "Label for the donation amount field on donation receipt"
|
||||
},
|
||||
"icu:DonationReceipt__type-label": {
|
||||
"messageformat": "Type",
|
||||
"description": "Label for the donation type field on receipt"
|
||||
"description": "Label for the donation type field on donation receipt"
|
||||
},
|
||||
"icu:DonationReceipt__date-paid-label": {
|
||||
"messageformat": "Date paid",
|
||||
"description": "Label for the payment date field on receipt"
|
||||
"description": "Label for the payment date field on donation receipt"
|
||||
},
|
||||
"icu:DonationReceipt__type-value--one-time": {
|
||||
"messageformat": "One-time",
|
||||
"description": "Value shown for one-time donations on receipt"
|
||||
},
|
||||
"icu:DonationReceipt__payment-method-label": {
|
||||
"messageformat": "Payment method",
|
||||
"description": "(Deleted 2025/07/02) Label for the payment method field on receipt"
|
||||
"messageformat": "One time",
|
||||
"description": "Value shown for one-time donations on donation receipt"
|
||||
},
|
||||
"icu:DonationReceipt__footer-text": {
|
||||
"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 you’re 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.",
|
||||
|
|
1
images/icons/v3/receipt/receipt.svg
Normal file
1
images/icons/v3/receipt/receipt.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.667 5.625a.625.625 0 1 0 0 1.25h6.666a.625.625 0 1 0 0-1.25H6.667Zm-.625 3.542c0-.345.28-.625.625-.625h6.666a.625.625 0 1 1 0 1.25H6.667a.625.625 0 0 1-.625-.625Zm.625 3.541a.625.625 0 0 1 0-1.25h4.166a.625.625 0 1 1 0 1.25H6.667Z" fill="#000"/><path fill-rule="evenodd" clip-rule="evenodd" d="M7.053 1.354c-.558 0-1.02 0-1.397.031-.393.032-.757.101-1.1.276-.529.27-.959.7-1.229 1.229-.174.343-.243.706-.275 1.099-.031.377-.031.84-.031 1.398v10.09c0 .25 0 .487.055.717.048.202.128.395.236.572.124.202.292.37.469.546l1.537 1.537a.73.73 0 0 0 1.03 0l1.569-1.568 1.567 1.568a.73.73 0 0 0 1.032 0l1.567-1.568 1.568 1.568a.73.73 0 0 0 1.031 0l1.537-1.537c.177-.177.345-.344.468-.546a1.95 1.95 0 0 0 .237-.572c.056-.23.056-.467.055-.717V5.387c0-.558 0-1.02-.03-1.398-.033-.393-.102-.756-.276-1.1-.27-.528-.7-.959-1.23-1.228-.342-.175-.706-.244-1.099-.276-.377-.03-.84-.03-1.397-.03H7.053ZM5.22 2.96c.102-.052.253-.097.555-.122.311-.025.714-.025 1.31-.025h5.833c.595 0 .998 0 1.309.025.302.025.453.07.555.122.255.13.462.337.592.592.053.103.097.253.122.556.025.31.026.713.026 1.309v10.005c0 .34-.005.391-.015.432a.52.52 0 0 1-.062.15c-.022.036-.055.075-.295.316l-.982.982-1.568-1.568a.73.73 0 0 0-1.031 0L10 17.302l-1.568-1.568a.73.73 0 0 0-1.03 0l-1.569 1.568-.982-.982c-.24-.24-.273-.28-.295-.316a.52.52 0 0 1-.062-.15c-.01-.04-.015-.092-.015-.432V5.417c0-.596 0-.998.026-1.31.025-.302.07-.452.122-.555.13-.255.337-.462.592-.592Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -57,6 +57,7 @@ $color-white-alpha-55: rgba($color-white, 0.55);
|
|||
$color-white-alpha-60: rgba($color-white, 0.6);
|
||||
$color-white-alpha-70: rgba($color-white, 0.7);
|
||||
$color-white-alpha-80: rgba($color-white, 0.8);
|
||||
$color-white-alpha-85: rgba($color-white, 0.85);
|
||||
$color-white-alpha-90: rgba($color-white, 0.9);
|
||||
|
||||
$color-black-alpha-05: rgba($color-black, 0.05);
|
||||
|
@ -75,6 +76,7 @@ $color-black-alpha-50: rgba($color-black, 0.5);
|
|||
$color-black-alpha-60: rgba($color-black, 0.6);
|
||||
$color-black-alpha-70: rgba($color-black, 0.7);
|
||||
$color-black-alpha-80: rgba($color-black, 0.8);
|
||||
$color-black-alpha-85: rgba($color-black, 0.85);
|
||||
$color-black-alpha-90: rgba($color-black, 0.9);
|
||||
|
||||
$color-transparent: rgba(0, 0, 0, 0);
|
||||
|
|
341
stylesheets/components/PreferencesDonations.scss
Normal file
341
stylesheets/components/PreferencesDonations.scss
Normal file
|
@ -0,0 +1,341 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
@use '../mixins';
|
||||
@use '../variables';
|
||||
|
||||
.PreferencesDonations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-block: 0;
|
||||
padding-inline: 0;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: 24px;
|
||||
|
||||
&__title {
|
||||
@include mixins.font-title-medium;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
@include mixins.font-body-2;
|
||||
text-align: center;
|
||||
max-width: 320px;
|
||||
margin-bottom: 24px;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-50,
|
||||
variables.$color-white-alpha-50
|
||||
);
|
||||
|
||||
&__read-more {
|
||||
@include mixins.button-reset;
|
||||
|
||||
& {
|
||||
color: variables.$color-ultramarine;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__donate-button {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
width: 100%;
|
||||
height: 0.5px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
background-color: light-dark(
|
||||
variables.$color-black-alpha-12,
|
||||
variables.$color-white-alpha-12
|
||||
);
|
||||
}
|
||||
|
||||
&__section-header {
|
||||
@include mixins.font-body-2-bold;
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-85,
|
||||
variables.$color-white-alpha-85
|
||||
);
|
||||
}
|
||||
|
||||
&__list {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__list-item {
|
||||
@include mixins.button-reset;
|
||||
|
||||
& {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-block: 12px;
|
||||
padding-inline: 24px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: light-dark(
|
||||
variables.$color-gray-02,
|
||||
variables.$color-gray-80
|
||||
);
|
||||
}
|
||||
|
||||
@include mixins.keyboard-mode {
|
||||
&:focus {
|
||||
outline: 2px solid variables.$color-ultramarine;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
&--receipts::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/receipt/receipt.svg',
|
||||
light-dark(variables.$color-gray-75, variables.$color-gray-15)
|
||||
);
|
||||
}
|
||||
|
||||
&--faqs::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/help/help-light.svg',
|
||||
light-dark(variables.$color-gray-75, variables.$color-gray-15)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
@include mixins.font-body-1;
|
||||
flex: 1;
|
||||
color: light-dark(variables.$color-gray-90, variables.$color-gray-05);
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@include mixins.color-svg(
|
||||
'../images/icons/v3/chevron/chevron-right.svg',
|
||||
light-dark(variables.$color-gray-45, variables.$color-gray-25)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Receipts page specific styles
|
||||
.PreferencesDonations--receiptList {
|
||||
&__info {
|
||||
margin-inline: 24px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&__text {
|
||||
@include mixins.font-subtitle;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-50,
|
||||
variables.$color-white-alpha-50
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&-yearContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__year-header {
|
||||
@include mixins.font-body-2-bold;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-85,
|
||||
variables.$color-white-alpha-85
|
||||
);
|
||||
padding-block: 8px;
|
||||
padding-inline: 24px;
|
||||
background-color: light-dark(
|
||||
variables.$color-white,
|
||||
variables.$color-gray-95
|
||||
);
|
||||
}
|
||||
|
||||
&__list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__receipt-item {
|
||||
@include mixins.button-reset;
|
||||
|
||||
& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-block: 8px;
|
||||
padding-inline: 24px;
|
||||
border-radius: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: light-dark(
|
||||
variables.$color-gray-02,
|
||||
variables.$color-gray-80
|
||||
);
|
||||
}
|
||||
|
||||
// Placeholder for icon depending on receipt type
|
||||
&__icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
background-color: variables.$color-ultramarine-pale;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__date {
|
||||
@include mixins.font-body-1;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-85,
|
||||
variables.$color-white-alpha-85
|
||||
);
|
||||
}
|
||||
|
||||
&__type {
|
||||
@include mixins.font-subtitle;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-50,
|
||||
variables.$color-white-alpha-50
|
||||
);
|
||||
}
|
||||
|
||||
&__amount {
|
||||
@include mixins.font-body-1;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-50,
|
||||
variables.$color-white-alpha-50
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&__empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
margin: auto;
|
||||
|
||||
&__title {
|
||||
@include mixins.font-body-2;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-50,
|
||||
variables.$color-white-alpha-50
|
||||
);
|
||||
}
|
||||
|
||||
&__description {
|
||||
@include mixins.font-caption;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-50,
|
||||
variables.$color-white-alpha-50
|
||||
);
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.PreferencesDonations__ReceiptModal {
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__logo-container {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__logo {
|
||||
width: 100px;
|
||||
height: 28.571px;
|
||||
margin-bottom: 24px;
|
||||
background-image: url('../images/signal-logo-and-wordmark.svg');
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
&__amount {
|
||||
font-size: 40px;
|
||||
letter-spacing: 0.07px;
|
||||
color: light-dark(variables.$color-gray-90, variables.$color-gray-05);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
width: 100%;
|
||||
height: 0.5px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
background-color: light-dark(
|
||||
variables.$color-black-alpha-12,
|
||||
variables.$color-white-alpha-12
|
||||
);
|
||||
}
|
||||
|
||||
&__details {
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
&__detail-item {
|
||||
padding-block: 10px;
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
&__detail-label {
|
||||
@include mixins.font-body-1;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-85,
|
||||
variables.$color-white-alpha-85
|
||||
);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__detail-value {
|
||||
@include mixins.font-subtitle;
|
||||
color: light-dark(
|
||||
variables.$color-black-alpha-50,
|
||||
variables.$color-white-alpha-50
|
||||
);
|
||||
}
|
||||
}
|
|
@ -144,6 +144,7 @@
|
|||
@use 'components/PlaybackButton.scss';
|
||||
@use 'components/PlaybackRateButton.scss';
|
||||
@use 'components/Preferences.scss';
|
||||
@use 'components/PreferencesDonations.scss';
|
||||
@use 'components/ProfileEditor.scss';
|
||||
@use 'components/ProfileMovedModal.scss';
|
||||
@use 'components/ProfileNameWarningModal.scss';
|
||||
|
|
|
@ -15,6 +15,7 @@ import { DAY, DurationInSeconds, WEEK } from '../util/durations';
|
|||
import { DialogUpdate } from './DialogUpdate';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import {
|
||||
getDefaultConversation,
|
||||
getDefaultGroup,
|
||||
|
@ -30,6 +31,8 @@ import type { WidthBreakpoint } from './_util';
|
|||
import type { MessageAttributesType } from '../model-types';
|
||||
import { PreferencesDonations } from './PreferencesDonations';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import type { DonationReceipt } from '../types/Donations';
|
||||
import type { AnyToast } from '../types/Toast';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
|
@ -168,7 +171,20 @@ function RenderProfileEditor(): JSX.Element {
|
|||
);
|
||||
}
|
||||
|
||||
function RenderDonationsPane(): JSX.Element {
|
||||
function RenderDonationsPane(props: {
|
||||
me: typeof me;
|
||||
donationReceipts: ReadonlyArray<DonationReceipt>;
|
||||
saveAttachmentToDisk: (options: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
baseDir?: string | undefined;
|
||||
}) => Promise<{ fullPath: string; name: string } | null>;
|
||||
generateDonationReceiptBlob: (
|
||||
receipt: DonationReceipt,
|
||||
i18n: LocalizerType
|
||||
) => Promise<Blob>;
|
||||
showToast: (toast: AnyToast) => void;
|
||||
}): JSX.Element {
|
||||
const contentsRef = useRef<HTMLDivElement | null>(null);
|
||||
return (
|
||||
<PreferencesDonations
|
||||
|
@ -180,6 +196,14 @@ function RenderDonationsPane(): JSX.Element {
|
|||
setPage={action('setPage')}
|
||||
submitDonation={action('submitDonation')}
|
||||
workflow={undefined}
|
||||
userAvatarData={[]}
|
||||
color={props.me.color}
|
||||
firstName={props.me.firstName}
|
||||
profileAvatarUrl={props.me.profileAvatarUrl}
|
||||
donationReceipts={props.donationReceipts}
|
||||
saveAttachmentToDisk={props.saveAttachmentToDisk}
|
||||
generateDonationReceiptBlob={props.generateDonationReceiptBlob}
|
||||
showToast={props.showToast}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -302,7 +326,20 @@ export default {
|
|||
whoCanSeeMe: PhoneNumberSharingMode.Everybody,
|
||||
zoomFactor: 1,
|
||||
|
||||
renderDonationsPane: RenderDonationsPane,
|
||||
renderDonationsPane: () =>
|
||||
RenderDonationsPane({
|
||||
me,
|
||||
donationReceipts: [],
|
||||
saveAttachmentToDisk: async () => {
|
||||
action('saveAttachmentToDisk')();
|
||||
return { fullPath: '/mock/path/to/file.png', name: 'file.png' };
|
||||
},
|
||||
generateDonationReceiptBlob: async () => {
|
||||
action('generateDonationReceiptBlob')();
|
||||
return new Blob();
|
||||
},
|
||||
showToast: action('showToast'),
|
||||
}),
|
||||
renderProfileEditor: RenderProfileEditor,
|
||||
renderToastManager,
|
||||
renderUpdateDialog,
|
||||
|
|
|
@ -336,6 +336,7 @@ export enum Page {
|
|||
ChatColor = 'ChatColor',
|
||||
ChatFolders = 'ChatFolders',
|
||||
DonationsDonateFlow = 'DonationsDonateFlow',
|
||||
DonationsReceiptList = 'DonationsReceiptList',
|
||||
EditChatFolder = 'EditChatFolder',
|
||||
PNP = 'PNP',
|
||||
BackupsDetails = 'BackupsDetails',
|
||||
|
@ -345,6 +346,14 @@ export enum Page {
|
|||
LocalBackupsKeyReference = 'LocalBackupsKeyReference',
|
||||
}
|
||||
|
||||
function isDonationsPage(page: Page): boolean {
|
||||
return (
|
||||
page === Page.Donations ||
|
||||
page === Page.DonationsDonateFlow ||
|
||||
page === Page.DonationsReceiptList
|
||||
);
|
||||
}
|
||||
|
||||
enum LanguageDialog {
|
||||
Selection,
|
||||
Confirmation,
|
||||
|
@ -607,10 +616,7 @@ export function Preferences({
|
|||
if (page === Page.Backups && !shouldShowBackupsPage) {
|
||||
setPage(Page.General);
|
||||
}
|
||||
if (
|
||||
(page === Page.Donations || page === Page.DonationsDonateFlow) &&
|
||||
!donationsFeatureEnabled
|
||||
) {
|
||||
if (isDonationsPage(page) && !donationsFeatureEnabled) {
|
||||
setPage(Page.General);
|
||||
}
|
||||
if (page === Page.Internal && !isInternalUser) {
|
||||
|
@ -908,7 +914,7 @@ export function Preferences({
|
|||
title={i18n('icu:Preferences__button--general')}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.Donations || page === Page.DonationsDonateFlow) {
|
||||
} else if (isDonationsPage(page)) {
|
||||
content = renderDonationsPane({
|
||||
contentsRef: settingsPaneRef,
|
||||
page,
|
||||
|
@ -2380,9 +2386,7 @@ export function Preferences({
|
|||
className={classNames({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--appearance': true,
|
||||
'Preferences__button--selected':
|
||||
page === Page.Donations ||
|
||||
page === Page.DonationsDonateFlow,
|
||||
'Preferences__button--selected': isDonationsPage(page),
|
||||
})}
|
||||
onClick={() => setPage(Page.Donations)}
|
||||
>
|
||||
|
|
|
@ -3,19 +3,12 @@
|
|||
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import type { MutableRefObject } from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
|
||||
import { PreferencesContent } from './Preferences';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import type { CardDetail, DonationWorkflow } from '../types/Donations';
|
||||
import { Input } from './Input';
|
||||
|
||||
type PropsExternalType = {
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
export type PropsDataType = {
|
||||
i18n: LocalizerType;
|
||||
workflow: DonationWorkflow | undefined;
|
||||
|
@ -23,7 +16,6 @@ export type PropsDataType = {
|
|||
|
||||
type PropsActionType = {
|
||||
clearWorkflow: () => void;
|
||||
onBack: () => void;
|
||||
submitDonation: (options: {
|
||||
currencyType: string;
|
||||
paymentAmount: number;
|
||||
|
@ -31,14 +23,12 @@ type PropsActionType = {
|
|||
}) => void;
|
||||
};
|
||||
|
||||
export type PropsType = PropsDataType & PropsActionType & PropsExternalType;
|
||||
export type PropsType = PropsDataType & PropsActionType;
|
||||
|
||||
export function PreferencesDonateFlow({
|
||||
contentsRef,
|
||||
i18n,
|
||||
workflow,
|
||||
clearWorkflow,
|
||||
onBack,
|
||||
submitDonation,
|
||||
}: PropsType): JSX.Element {
|
||||
const tryClose = useRef<() => void | undefined>();
|
||||
|
@ -81,15 +71,6 @@ export function PreferencesDonateFlow({
|
|||
|
||||
const isDonateDisabled = workflow !== undefined;
|
||||
|
||||
const backButton = (
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="Preferences__back-icon"
|
||||
onClick={onBack}
|
||||
type="button"
|
||||
/>
|
||||
);
|
||||
|
||||
const onTryClose = useCallback(() => {
|
||||
const onDiscard = () => {
|
||||
// TODO: DESKTOP-8950
|
||||
|
@ -165,13 +146,7 @@ export function PreferencesDonateFlow({
|
|||
return (
|
||||
<>
|
||||
{confirmDiscardModal}
|
||||
|
||||
<PreferencesContent
|
||||
backButton={backButton}
|
||||
contents={content}
|
||||
contentsRef={contentsRef}
|
||||
title={i18n('icu:Preferences__DonateTitle')}
|
||||
/>
|
||||
{content}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,34 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { groupBy, sortBy } from 'lodash';
|
||||
|
||||
import type { MutableRefObject } from 'react';
|
||||
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 { Page, PreferencesContent } from './Preferences';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { PreferencesDonateFlow } from './PreferencesDonateFlow';
|
||||
import type { CardDetail, DonationWorkflow } from '../types/Donations';
|
||||
import type {
|
||||
CardDetail,
|
||||
DonationWorkflow,
|
||||
DonationReceipt,
|
||||
} 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';
|
||||
import type { AnyToast } from '../types/Toast';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import { createLogger } from '../logging/log';
|
||||
import { toLogFormat } from '../types/errors';
|
||||
import { I18n } from './I18n';
|
||||
|
||||
const log = createLogger('PreferencesDonations');
|
||||
|
||||
type PropsExternalType = {
|
||||
contentsRef: MutableRefObject<HTMLDivElement | null>;
|
||||
|
@ -20,6 +39,21 @@ export type PropsDataType = {
|
|||
isStaging: boolean;
|
||||
page: Page;
|
||||
workflow: DonationWorkflow | undefined;
|
||||
userAvatarData: ReadonlyArray<AvatarDataType>;
|
||||
color?: AvatarColorType;
|
||||
firstName?: string;
|
||||
profileAvatarUrl?: string;
|
||||
donationReceipts: ReadonlyArray<DonationReceipt>;
|
||||
saveAttachmentToDisk: (options: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
baseDir?: string | undefined;
|
||||
}) => Promise<{ fullPath: string; name: string } | null>;
|
||||
generateDonationReceiptBlob: (
|
||||
receipt: DonationReceipt,
|
||||
i18n: LocalizerType
|
||||
) => Promise<Blob>;
|
||||
showToast: (toast: AnyToast) => void;
|
||||
};
|
||||
|
||||
type PropsActionType = {
|
||||
|
@ -34,6 +68,335 @@ type PropsActionType = {
|
|||
|
||||
export type PropsType = PropsDataType & PropsActionType & PropsExternalType;
|
||||
|
||||
type DonationPage =
|
||||
| Page.Donations
|
||||
| Page.DonationsDonateFlow
|
||||
| Page.DonationsReceiptList;
|
||||
|
||||
type PreferencesHomeProps = PropsType & {
|
||||
navigateToPage: (newPage: Page) => void;
|
||||
};
|
||||
|
||||
function isDonationPage(page: Page): page is DonationPage {
|
||||
return (
|
||||
page === Page.Donations ||
|
||||
page === Page.DonationsDonateFlow ||
|
||||
page === Page.DonationsReceiptList
|
||||
);
|
||||
}
|
||||
|
||||
function LearnMoreButton(parts: ReactNode): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="PreferencesDonations__description__read-more"
|
||||
onClick={() => {
|
||||
// DESKTOP-8973
|
||||
}}
|
||||
>
|
||||
{parts}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function DonationsHome({
|
||||
i18n,
|
||||
userAvatarData,
|
||||
color,
|
||||
firstName,
|
||||
profileAvatarUrl,
|
||||
navigateToPage,
|
||||
setPage,
|
||||
isStaging,
|
||||
donationReceipts,
|
||||
}: PreferencesHomeProps): JSX.Element {
|
||||
const avatarData = userAvatarData[0];
|
||||
const avatarBuffer = avatarData?.buffer;
|
||||
const hasReceipts = donationReceipts.length > 0;
|
||||
|
||||
return (
|
||||
<div className="PreferencesDonations">
|
||||
<div className="PreferencesDonations__avatar">
|
||||
<AvatarPreview
|
||||
avatarColor={color}
|
||||
avatarUrl={profileAvatarUrl}
|
||||
avatarValue={avatarBuffer}
|
||||
conversationTitle={firstName || i18n('icu:unknownContact')}
|
||||
i18n={i18n}
|
||||
style={{
|
||||
height: 80,
|
||||
width: 80,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="PreferencesDonations__title">
|
||||
{i18n('icu:PreferencesDonations__title')}
|
||||
</div>
|
||||
<div className="PreferencesDonations__description">
|
||||
<I18n
|
||||
components={{
|
||||
learnMoreLink: LearnMoreButton,
|
||||
}}
|
||||
i18n={i18n}
|
||||
id="icu:PreferencesDonations__description"
|
||||
/>
|
||||
</div>
|
||||
{isStaging && (
|
||||
<Button
|
||||
className="PreferencesDonations__donate-button"
|
||||
variant={ButtonVariant.Primary}
|
||||
size={ButtonSize.Medium}
|
||||
onClick={() => {
|
||||
setPage(Page.DonationsDonateFlow);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:PreferencesDonations__donate-button')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<hr className="PreferencesDonations__separator" />
|
||||
|
||||
<ListBox className="PreferencesDonations__list">
|
||||
{hasReceipts && (
|
||||
<ListBoxItem
|
||||
className="PreferencesDonations__list-item"
|
||||
onAction={() => {
|
||||
navigateToPage(Page.DonationsReceiptList);
|
||||
}}
|
||||
>
|
||||
<span className="PreferencesDonations__list-item__icon PreferencesDonations__list-item__icon--receipts" />
|
||||
<span className="PreferencesDonations__list-item__text">
|
||||
{i18n('icu:PreferencesDonations__receipts')}
|
||||
</span>
|
||||
<span className="PreferencesDonations__list-item__chevron" />
|
||||
</ListBoxItem>
|
||||
)}
|
||||
<ListBoxItem
|
||||
className="PreferencesDonations__list-item"
|
||||
onAction={() => {
|
||||
// TODO: Handle donation FAQs action
|
||||
}}
|
||||
>
|
||||
<span className="PreferencesDonations__list-item__icon PreferencesDonations__list-item__icon--faqs" />
|
||||
<span className="PreferencesDonations__list-item__text">
|
||||
{i18n('icu:PreferencesDonations__faqs')}
|
||||
</span>
|
||||
<span className="PreferencesDonations__list-item__chevron" />
|
||||
</ListBoxItem>
|
||||
</ListBox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreferencesReceiptList({
|
||||
i18n,
|
||||
donationReceipts,
|
||||
saveAttachmentToDisk,
|
||||
generateDonationReceiptBlob,
|
||||
showToast,
|
||||
}: {
|
||||
i18n: LocalizerType;
|
||||
donationReceipts: ReadonlyArray<DonationReceipt>;
|
||||
saveAttachmentToDisk: (options: {
|
||||
data: Uint8Array;
|
||||
name: string;
|
||||
baseDir?: string | undefined;
|
||||
}) => Promise<{ fullPath: string; name: string } | null>;
|
||||
generateDonationReceiptBlob: (
|
||||
receipt: DonationReceipt,
|
||||
i18n: LocalizerType
|
||||
) => Promise<Blob>;
|
||||
showToast: (toast: AnyToast) => void;
|
||||
}): JSX.Element {
|
||||
const [showReceiptModal, setShowReceiptModal] = useState(false);
|
||||
const [selectedReceipt, setSelectedReceipt] =
|
||||
useState<DonationReceipt | null>(null);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const sortedReceipts = sortBy(
|
||||
donationReceipts,
|
||||
receipt => -receipt.timestamp
|
||||
);
|
||||
const receiptsByYear = groupBy(sortedReceipts, receipt =>
|
||||
new Date(receipt.timestamp).getFullYear()
|
||||
);
|
||||
|
||||
const dateFormatter = getDateTimeFormatter({
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
|
||||
const preferredSystemLocales =
|
||||
window.SignalContext.getPreferredSystemLocales();
|
||||
const localeOverride = window.SignalContext.getLocaleOverride();
|
||||
const locales =
|
||||
localeOverride != null ? [localeOverride] : preferredSystemLocales;
|
||||
|
||||
const getCurrencyFormatter = (currencyType: string) =>
|
||||
new Intl.NumberFormat(locales, {
|
||||
style: 'currency',
|
||||
currency: currencyType,
|
||||
});
|
||||
|
||||
const hasReceipts = Object.keys(receiptsByYear).length > 0;
|
||||
|
||||
const handleDownloadReceipt = useCallback(async () => {
|
||||
if (!selectedReceipt) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDownloading(true);
|
||||
try {
|
||||
const blob = await generateDonationReceiptBlob(selectedReceipt, i18n);
|
||||
const buffer = await blob.arrayBuffer();
|
||||
|
||||
const result = await saveAttachmentToDisk({
|
||||
name: `Signal_Receipt_${new Date(selectedReceipt.timestamp).toISOString().split('T')[0]}.png`,
|
||||
data: new Uint8Array(buffer),
|
||||
});
|
||||
|
||||
if (result) {
|
||||
setShowReceiptModal(false);
|
||||
showToast({
|
||||
toastType: ToastType.ReceiptSaved,
|
||||
parameters: { fullPath: result.fullPath },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Failed to generate receipt: ', toLogFormat(error));
|
||||
showToast({
|
||||
toastType: ToastType.ReceiptSaveFailed,
|
||||
});
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
}, [
|
||||
selectedReceipt,
|
||||
generateDonationReceiptBlob,
|
||||
i18n,
|
||||
saveAttachmentToDisk,
|
||||
showToast,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="PreferencesDonations PreferencesDonations--receiptList">
|
||||
{hasReceipts ? (
|
||||
<>
|
||||
<div className="PreferencesDonations--receiptList__info">
|
||||
<div className="PreferencesDonations--receiptList__info__text">
|
||||
{i18n('icu:PreferencesDonations--receiptList__info')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.entries(receiptsByYear).map(([year, receipts]) => (
|
||||
<div
|
||||
key={year}
|
||||
className="PreferencesDonations--receiptList-yearContainer"
|
||||
>
|
||||
<div className="PreferencesDonations--receiptList__year-header">
|
||||
{year}
|
||||
</div>
|
||||
<ListBox className="PreferencesDonations--receiptList__list">
|
||||
{receipts.map(receipt => (
|
||||
<ListBoxItem
|
||||
key={receipt.id}
|
||||
className="PreferencesDonations--receiptList__receipt-item"
|
||||
onAction={() => {
|
||||
setSelectedReceipt(receipt);
|
||||
setShowReceiptModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="PreferencesDonations--receiptList__receipt-item__icon" />
|
||||
<div className="PreferencesDonations--receiptList__receipt-item__details">
|
||||
<div className="PreferencesDonations--receiptList__receipt-item__date">
|
||||
{dateFormatter.format(new Date(receipt.timestamp))}
|
||||
</div>
|
||||
<div className="PreferencesDonations--receiptList__receipt-item__type">
|
||||
{i18n('icu:DonationReceipt__type-value--one-time')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="PreferencesDonations--receiptList__receipt-item__amount">
|
||||
{getCurrencyFormatter(receipt.currencyType).format(
|
||||
receipt.paymentAmount / 100
|
||||
)}
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="PreferencesDonations--receiptList__empty-state">
|
||||
<div className="PreferencesDonations--receiptList__empty-state__title">
|
||||
{i18n('icu:PreferencesDonations--receiptList__empty-title')}
|
||||
</div>
|
||||
<div className="PreferencesDonations--receiptList__empty-state__description">
|
||||
{i18n('icu:PreferencesDonations--receiptList__info')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showReceiptModal && selectedReceipt && (
|
||||
<Modal
|
||||
i18n={i18n}
|
||||
modalName="ReceiptDetailsModal"
|
||||
moduleClassName="PreferencesDonations__ReceiptModal"
|
||||
hasXButton
|
||||
title={i18n('icu:PreferencesDonations__ReceiptModal--title')}
|
||||
onClose={() => setShowReceiptModal(false)}
|
||||
modalFooter={
|
||||
<Button
|
||||
variant={ButtonVariant.Primary}
|
||||
onClick={handleDownloadReceipt}
|
||||
disabled={isDownloading}
|
||||
>
|
||||
{isDownloading ? (
|
||||
<Spinner size="24px" svgSize="small" />
|
||||
) : (
|
||||
i18n('icu:PreferencesDonations__ReceiptModal--download')
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="PreferencesDonations__ReceiptModal__content">
|
||||
<div className="PreferencesDonations__ReceiptModal__logo-container">
|
||||
<div className="PreferencesDonations__ReceiptModal__logo" />
|
||||
</div>
|
||||
<div className="PreferencesDonations__ReceiptModal__amount">
|
||||
{getCurrencyFormatter(selectedReceipt.currencyType).format(
|
||||
selectedReceipt.paymentAmount / 100
|
||||
)}
|
||||
</div>
|
||||
<hr className="PreferencesDonations__ReceiptModal__separator" />
|
||||
<div className="PreferencesDonations__ReceiptModal__details">
|
||||
<div className="PreferencesDonations__ReceiptModal__detail-item">
|
||||
<div className="PreferencesDonations__ReceiptModal__detail-label">
|
||||
{i18n('icu:PreferencesDonations__ReceiptModal--type-label')}
|
||||
</div>
|
||||
<div className="PreferencesDonations__ReceiptModal__detail-value">
|
||||
{i18n('icu:DonationReceipt__type-value--one-time')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="PreferencesDonations__ReceiptModal__detail-item">
|
||||
<div className="PreferencesDonations__ReceiptModal__detail-label">
|
||||
{i18n(
|
||||
'icu:PreferencesDonations__ReceiptModal--date-paid-label'
|
||||
)}
|
||||
</div>
|
||||
<div className="PreferencesDonations__ReceiptModal__detail-value">
|
||||
{dateFormatter.format(new Date(selectedReceipt.timestamp))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreferencesDonations({
|
||||
contentsRef,
|
||||
i18n,
|
||||
|
@ -43,38 +406,119 @@ export function PreferencesDonations({
|
|||
clearWorkflow,
|
||||
setPage,
|
||||
submitDonation,
|
||||
}: PropsType): JSX.Element {
|
||||
userAvatarData,
|
||||
color,
|
||||
firstName,
|
||||
profileAvatarUrl,
|
||||
donationReceipts,
|
||||
saveAttachmentToDisk,
|
||||
generateDonationReceiptBlob,
|
||||
showToast,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const PAGE_CONFIG = useMemo<
|
||||
Record<DonationPage, { title: string | undefined; goBackTo: Page | null }>
|
||||
>(() => {
|
||||
return {
|
||||
[Page.Donations]: {
|
||||
title: i18n('icu:Preferences__DonateTitle'),
|
||||
goBackTo: null,
|
||||
},
|
||||
[Page.DonationsReceiptList]: {
|
||||
title: i18n('icu:PreferencesDonations__receipts'),
|
||||
goBackTo: Page.Donations,
|
||||
},
|
||||
[Page.DonationsDonateFlow]: {
|
||||
title: undefined,
|
||||
goBackTo: Page.Donations,
|
||||
},
|
||||
} as const;
|
||||
}, [i18n]);
|
||||
|
||||
const navigateToPage = useCallback(
|
||||
(newPage: Page) => {
|
||||
setPage(newPage);
|
||||
},
|
||||
[setPage]
|
||||
);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
if (!isDonationPage(page)) {
|
||||
log.error(
|
||||
'Donations page back button tried to go to a non-donations page, ignoring'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { goBackTo } = PAGE_CONFIG[page];
|
||||
if (goBackTo) {
|
||||
setPage(goBackTo);
|
||||
}
|
||||
}, [PAGE_CONFIG, page, setPage]);
|
||||
|
||||
if (!isDonationPage(page)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let content;
|
||||
if (page === Page.DonationsDonateFlow) {
|
||||
return (
|
||||
content = (
|
||||
<PreferencesDonateFlow
|
||||
contentsRef={contentsRef}
|
||||
i18n={i18n}
|
||||
workflow={workflow}
|
||||
clearWorkflow={clearWorkflow}
|
||||
onBack={() => setPage(Page.Donations)}
|
||||
submitDonation={submitDonation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (page === Page.Donations) {
|
||||
content = (
|
||||
<DonationsHome
|
||||
contentsRef={contentsRef}
|
||||
i18n={i18n}
|
||||
userAvatarData={userAvatarData}
|
||||
color={color}
|
||||
firstName={firstName}
|
||||
profileAvatarUrl={profileAvatarUrl}
|
||||
navigateToPage={navigateToPage}
|
||||
donationReceipts={donationReceipts}
|
||||
saveAttachmentToDisk={saveAttachmentToDisk}
|
||||
generateDonationReceiptBlob={generateDonationReceiptBlob}
|
||||
showToast={showToast}
|
||||
isStaging={isStaging}
|
||||
page={page}
|
||||
workflow={workflow}
|
||||
clearWorkflow={clearWorkflow}
|
||||
setPage={setPage}
|
||||
submitDonation={submitDonation}
|
||||
/>
|
||||
);
|
||||
} else if (page === Page.DonationsReceiptList) {
|
||||
content = (
|
||||
<PreferencesReceiptList
|
||||
i18n={i18n}
|
||||
donationReceipts={donationReceipts}
|
||||
saveAttachmentToDisk={saveAttachmentToDisk}
|
||||
generateDonationReceiptBlob={generateDonationReceiptBlob}
|
||||
showToast={showToast}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div className="PreferencesDonations">
|
||||
{isStaging && (
|
||||
<Button
|
||||
onClick={() => setPage(Page.DonationsDonateFlow)}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
Donate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
// Show back button based on page configuration
|
||||
const backButton = PAGE_CONFIG[page].goBackTo ? (
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="Preferences__back-icon"
|
||||
onClick={handleBack}
|
||||
type="button"
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<PreferencesContent
|
||||
backButton={backButton}
|
||||
contents={content}
|
||||
contentsRef={contentsRef}
|
||||
title={i18n('icu:Preferences__DonateTitle')}
|
||||
title={PAGE_CONFIG[page].title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -150,6 +150,13 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
return { toastType: ToastType.PinnedConversationsFull };
|
||||
case ToastType.ReactionFailed:
|
||||
return { toastType: ToastType.ReactionFailed };
|
||||
case ToastType.ReceiptSaved:
|
||||
return {
|
||||
toastType: ToastType.ReceiptSaved,
|
||||
parameters: { fullPath: '/image.png' },
|
||||
};
|
||||
case ToastType.ReceiptSaveFailed:
|
||||
return { toastType: ToastType.ReceiptSaveFailed };
|
||||
case ToastType.ReportedSpam:
|
||||
return { toastType: ToastType.ReportedSpam };
|
||||
case ToastType.ReportedSpamAndBlocked:
|
||||
|
|
|
@ -491,6 +491,28 @@ export function renderToast({
|
|||
return <Toast onClose={hideToast}>{i18n('icu:Reactions--error')}</Toast>;
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ReceiptSaved) {
|
||||
return (
|
||||
<Toast
|
||||
onClose={hideToast}
|
||||
toastAction={{
|
||||
label: i18n('icu:attachmentSavedShow'),
|
||||
onClick: () => {
|
||||
openFileInFolder(toast.parameters.fullPath);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{i18n('icu:Toast--ReceiptSaved')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ReceiptSaveFailed) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>{i18n('icu:Toast--ReceiptSaveFailed')}</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ReportedSpam) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
|
|
|
@ -7,11 +7,14 @@ import { useSelector } from 'react-redux';
|
|||
import type { MutableRefObject } from 'react';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getMe } from '../selectors/conversations';
|
||||
import { PreferencesDonations } from '../../components/PreferencesDonations';
|
||||
import type { Page } from '../../components/Preferences';
|
||||
import { useDonationsActions } from '../ducks/donations';
|
||||
import type { StateType } from '../reducer';
|
||||
import { isStagingServer } from '../../util/isStagingServer';
|
||||
import { generateDonationReceiptBlob } from '../../util/generateDonationReceipt';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
|
||||
export const SmartPreferencesDonations = memo(
|
||||
function SmartPreferencesDonations({
|
||||
|
@ -25,15 +28,36 @@ export const SmartPreferencesDonations = memo(
|
|||
}) {
|
||||
const isStaging = isStagingServer();
|
||||
const i18n = useSelector(getIntl);
|
||||
|
||||
const workflow = useSelector(
|
||||
(state: StateType) => state.donations.currentWorkflow
|
||||
);
|
||||
const { clearWorkflow, submitDonation } = useDonationsActions();
|
||||
|
||||
const {
|
||||
avatars: userAvatarData = [],
|
||||
color,
|
||||
firstName,
|
||||
profileAvatarUrl,
|
||||
} = useSelector(getMe);
|
||||
const { showToast } = useToastActions();
|
||||
const donationReceipts = useSelector(
|
||||
(state: StateType) => state.donations.receipts
|
||||
);
|
||||
const { saveAttachmentToDisk } = window.Signal.Migrations;
|
||||
|
||||
return (
|
||||
<PreferencesDonations
|
||||
contentsRef={contentsRef}
|
||||
i18n={i18n}
|
||||
userAvatarData={userAvatarData}
|
||||
color={color}
|
||||
firstName={firstName}
|
||||
profileAvatarUrl={profileAvatarUrl}
|
||||
donationReceipts={donationReceipts}
|
||||
saveAttachmentToDisk={saveAttachmentToDisk}
|
||||
generateDonationReceiptBlob={generateDonationReceiptBlob}
|
||||
showToast={showToast}
|
||||
contentsRef={contentsRef}
|
||||
isStaging={isStaging}
|
||||
page={page}
|
||||
workflow={workflow}
|
||||
|
|
|
@ -53,6 +53,8 @@ export enum ToastType {
|
|||
OriginalMessageNotFound = 'OriginalMessageNotFound',
|
||||
PinnedConversationsFull = 'PinnedConversationsFull',
|
||||
ReactionFailed = 'ReactionFailed',
|
||||
ReceiptSaved = 'ReceiptSaved',
|
||||
ReceiptSaveFailed = 'ReceiptSaveFailed',
|
||||
ReportedSpam = 'ReportedSpam',
|
||||
ReportedSpamAndBlocked = 'ReportedSpamAndBlocked',
|
||||
SQLError = 'SQLError',
|
||||
|
@ -153,6 +155,11 @@ export type AnyToast =
|
|||
| { toastType: ToastType.OriginalMessageNotFound }
|
||||
| { toastType: ToastType.PinnedConversationsFull }
|
||||
| { toastType: ToastType.ReactionFailed }
|
||||
| {
|
||||
toastType: ToastType.ReceiptSaved;
|
||||
parameters: { fullPath: string };
|
||||
}
|
||||
| { toastType: ToastType.ReceiptSaveFailed }
|
||||
| { toastType: ToastType.ReportedSpam }
|
||||
| { toastType: ToastType.ReportedSpamAndBlocked }
|
||||
| { toastType: ToastType.StickerPackInstallFailed }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue