// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useState, useCallback } from 'react'; import classNames from 'classnames'; import { v4 as uuid } from 'uuid'; import type { LocalizerType } from '../types/I18N'; import { toLogFormat } from '../types/errors'; import { formatFileSize } from '../util/formatFileSize'; import { SECOND } from '../util/durations'; import type { ValidationResultType as BackupValidationResultType } from '../services/backups'; import { SettingsRow, FlowingSettingsControl } from './PreferencesUtil'; import { Button, ButtonVariant } from './Button'; import { Spinner } from './Spinner'; import type { MessageCountBySchemaVersionType } from '../sql/Interface'; import type { MessageAttributesType } from '../model-types'; import type { DonationReceipt } from '../types/Donations'; import { createLogger } from '../logging/log'; import { isStagingServer } from '../util/isStagingServer'; import { getHumanDonationAmount } from '../util/currency'; const log = createLogger('PreferencesInternal'); export function PreferencesInternal({ i18n, exportLocalBackup: doExportLocalBackup, validateBackup: doValidateBackup, getMessageCountBySchemaVersion, getMessageSampleForSchemaVersion, donationReceipts, internalAddDonationReceipt, saveAttachmentToDisk, generateDonationReceiptBlob, }: { i18n: LocalizerType; exportLocalBackup: () => Promise; validateBackup: () => Promise; getMessageCountBySchemaVersion: () => Promise; getMessageSampleForSchemaVersion: ( version: number ) => Promise>; donationReceipts: ReadonlyArray; internalAddDonationReceipt: (receipt: DonationReceipt) => void; saveAttachmentToDisk: (options: { data: Uint8Array; name: string; baseDir?: string | undefined; }) => Promise<{ fullPath: string; name: string } | null>; generateDonationReceiptBlob: ( receipt: DonationReceipt, i18n: LocalizerType ) => Promise; }): JSX.Element { const [isExportPending, setIsExportPending] = useState(false); const [exportResult, setExportResult] = useState< BackupValidationResultType | undefined >(); const [messageCountBySchemaVersion, setMessageCountBySchemaVersion] = useState(); const [messageSampleForVersions, setMessageSampleForVersions] = useState<{ [schemaVersion: number]: Array; }>(); const [isValidationPending, setIsValidationPending] = useState(false); const [validationResult, setValidationResult] = useState< BackupValidationResultType | undefined >(); const validateBackup = useCallback(async () => { setIsValidationPending(true); setValidationResult(undefined); try { setValidationResult(await doValidateBackup()); } catch (error) { setValidationResult({ error: toLogFormat(error) }); } finally { setIsValidationPending(false); } }, [doValidateBackup]); const renderValidationResult = useCallback( ( backupResult: BackupValidationResultType | undefined ): JSX.Element | undefined => { if (backupResult == null) { return; } if ('result' in backupResult) { const { result: { totalBytes, stats, duration }, } = backupResult; let snapshotDirEl: JSX.Element | undefined; if ('snapshotDir' in backupResult.result) { snapshotDirEl = (

Backup path:

                {backupResult.result.snapshotDir}
              

); } return (
{snapshotDirEl}

Main file size: {formatFileSize(totalBytes)}

Duration: {Math.round(duration / SECOND)}s

              {JSON.stringify(stats, null, 2)}
            
); } const { error } = backupResult; return (
            {error}
          
); }, [] ); const exportLocalBackup = useCallback(async () => { setIsExportPending(true); setExportResult(undefined); try { setExportResult(await doExportLocalBackup()); } catch (error) { setExportResult({ error: toLogFormat(error) }); } finally { setIsExportPending(false); } }, [doExportLocalBackup]); // Donation receipt states const [isGeneratingReceipt, setIsGeneratingReceipt] = useState(false); const handleAddTestReceipt = useCallback(async () => { const testReceipt: DonationReceipt = { id: uuid(), currencyType: 'USD', paymentAmount: Math.floor(Math.random() * 10000) + 100, // Random amount between $1 and $100 (in cents) timestamp: Date.now(), }; try { await internalAddDonationReceipt(testReceipt); } catch (error) { log.error('Error adding test receipt:', toLogFormat(error)); } }, [internalAddDonationReceipt]); const handleGenerateReceipt = useCallback( async (receipt: DonationReceipt) => { setIsGeneratingReceipt(true); try { const blob = await generateDonationReceiptBlob(receipt, i18n); const buffer = await blob.arrayBuffer(); const result = await saveAttachmentToDisk({ name: `Signal_Receipt_${new Date(receipt.timestamp).toISOString().split('T')[0]}.png`, data: new Uint8Array(buffer), }); if (result) { log.info('Receipt saved to:', result.fullPath); } } catch (error) { log.error('Error generating receipt:', toLogFormat(error)); } finally { setIsGeneratingReceipt(false); } }, [i18n, saveAttachmentToDisk, generateDonationReceiptBlob] ); return (
{i18n('icu:Preferences__internal__validate-backup--description')}
{renderValidationResult(validationResult)}
{i18n( 'icu:Preferences__internal__export-local-backup--description' )}
{renderValidationResult(exportResult)}
Check message schema versions
{messageCountBySchemaVersion ? (
              
                  {messageCountBySchemaVersion.map(
                    ({ schemaVersion, count }) => {
                      return (
                        
                          
                          {messageSampleForVersions?.[schemaVersion] ? (
                            
                          ) : null}
                        
                      );
                    }
                  )}
                
Schema version # Messages
{schemaVersion} {count}
{JSON.stringify( messageSampleForVersions[schemaVersion], null, 2 )}
) : null}
{isStagingServer() && (
Test donation receipt generation functionality
{donationReceipts.length > 0 ? (

Receipts ({donationReceipts.length})

{donationReceipts.map(receipt => ( ))}
Date Amount Last 4 ID Actions
{new Date(receipt.timestamp).toLocaleDateString()} {getHumanDonationAmount(receipt)} {receipt.currencyType} {receipt.id.substring(0, 8)}...
) : (

No receipts found. Add some test receipts above.

)}
)}
); }