Donations: Introduce timeouts in early stages of the workflow
This commit is contained in:
parent
7ef40c64c4
commit
fd794ae90d
42 changed files with 198 additions and 109 deletions
|
@ -5253,7 +5253,7 @@
|
|||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--admin-approval-bounce--pluralized": {
|
||||
"messageformat": "{joinerName} requested and cancelled {numberOfRequests, plural, one {their request} other {# requests}} to join via the group link",
|
||||
"messageformat": "{joinerName} requested and canceled {numberOfRequests, plural, one {their request} other {# requests}} to join via the group link",
|
||||
"description": "Shown in timeline or conversation preview when v2 group changes"
|
||||
},
|
||||
"icu:GroupV2--group-link-add--disabled--you": {
|
||||
|
@ -8902,9 +8902,9 @@
|
|||
"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.",
|
||||
"description": "Footer text shown on donation receipts explaining tax deductibility and Signal's mission"
|
||||
},
|
||||
"icu:Donations__Toast__Cancelled": {
|
||||
"messageformat": "Donation cancelled",
|
||||
"description": "Toast shown when a donation was manually cancelled by the user"
|
||||
"icu:Donations__Toast__Canceled": {
|
||||
"messageformat": "Donation canceled",
|
||||
"description": "Toast shown when a donation was manually canceled by the user"
|
||||
},
|
||||
"icu:Donations__Toast__Completed": {
|
||||
"messageformat": "Donation completed",
|
||||
|
@ -8978,6 +8978,14 @@
|
|||
"messageformat": "Your donation is still being processed. This can take a few minutes.",
|
||||
"description": "An explanation for the 'still processing' dialog"
|
||||
},
|
||||
"icu:Donations__TimedOut": {
|
||||
"messageformat": "Donation canceled",
|
||||
"description": "Title of the dialog shown when the it's been too long since the last step in the donation process"
|
||||
},
|
||||
"icu:Donations__TimedOut__Description": {
|
||||
"messageformat": "It has been too long since you started your donation. Please start over.",
|
||||
"description": "An explanation for the 'donation timed out' dialog"
|
||||
},
|
||||
"icu:Donations__3dsValidationNeeded": {
|
||||
"messageformat": "Additional verification required",
|
||||
"description": "Title of the dialog shown when the user's payment method requires a redirect to a verification website"
|
||||
|
|
|
@ -159,7 +159,7 @@ async function safeDecryptToSink(
|
|||
await Promise.race([
|
||||
// Just use a non-existing event name to wait for an 'error'. We want
|
||||
// to handle errors on `sink` while generating digest in case the whole
|
||||
// request gets cancelled early.
|
||||
// request gets canceled early.
|
||||
once(sink, 'non-error-event', { signal: controller.signal }),
|
||||
decryptAttachmentV2ToSink(options, sink),
|
||||
]);
|
||||
|
|
|
@ -885,7 +885,7 @@ async function createWindow() {
|
|||
);
|
||||
}
|
||||
if (!shouldClose) {
|
||||
updater.onRestartCancelled();
|
||||
updater.onRestartCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1189,7 +1189,7 @@ message GroupJoinRequestCanceledUpdate {
|
|||
bytes requestorAci = 1;
|
||||
}
|
||||
|
||||
// A single requestor has requested to join and cancelled
|
||||
// A single requestor has requested to join and canceled
|
||||
// their request repeatedly with no other updates in between.
|
||||
// The last action encompassed by this update is always a
|
||||
// cancellation; if there was another open request immediately
|
||||
|
|
|
@ -1517,7 +1517,7 @@ export async function startApp(): Promise<void> {
|
|||
);
|
||||
if (!window.textsecure.storage.user.getAci()) {
|
||||
log.info(
|
||||
"Expiration start timestamp cleanup: Cancelling update; we don't have our own UUID"
|
||||
"Expiration start timestamp cleanup: Canceling update; we don't have our own UUID"
|
||||
);
|
||||
} else if (messagesUnexpectedlyMissingExpirationStartTimestamp.length) {
|
||||
const newMessageAttributes =
|
||||
|
|
|
@ -47,3 +47,12 @@ export function PaymentDeclined(): JSX.Element {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TimedOut(): JSX.Element {
|
||||
return (
|
||||
<DonationErrorModal
|
||||
{...defaultProps}
|
||||
errorType={donationErrorTypeSchema.Enum.TimedOut}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -40,6 +40,11 @@ export function DonationErrorModal(props: PropsType): JSX.Element {
|
|||
body = i18n('icu:Donations__PaymentMethodDeclined__Description');
|
||||
break;
|
||||
}
|
||||
case donationErrorTypeSchema.Enum.TimedOut: {
|
||||
title = i18n('icu:Donations__TimedOut');
|
||||
body = i18n('icu:Donations__TimedOut__Description');
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw missingCaseError(props.errorType);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { action } from '@storybook/addon-actions';
|
|||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './DonationVerificationModal';
|
||||
import { DonationVerificationModal } from './DonationVerificationModal';
|
||||
import { SECOND } from '../util/durations';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
|
@ -19,8 +20,13 @@ const defaultProps = {
|
|||
i18n,
|
||||
onCancelDonation: action('onCancelDonation'),
|
||||
onOpenBrowser: action('onOpenBrowser'),
|
||||
onTimedOut: action('onTimedOut'),
|
||||
};
|
||||
|
||||
export function Default(): JSX.Element {
|
||||
return <DonationVerificationModal {...defaultProps} />;
|
||||
}
|
||||
|
||||
export function FiveSecondTimeout(): JSX.Element {
|
||||
return <DonationVerificationModal {...defaultProps} _timeout={5 * SECOND} />;
|
||||
}
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Modal } from './Modal';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { DAY } from '../util/durations';
|
||||
|
||||
export type PropsType = {
|
||||
// Test-only
|
||||
_timeout?: number;
|
||||
i18n: LocalizerType;
|
||||
onCancelDonation: () => unknown;
|
||||
onOpenBrowser: () => unknown;
|
||||
onTimedOut: () => unknown;
|
||||
};
|
||||
|
||||
export function DonationVerificationModal(props: PropsType): JSX.Element {
|
||||
const { i18n, onCancelDonation, onOpenBrowser } = props;
|
||||
const { _timeout, i18n, onCancelDonation, onOpenBrowser, onTimedOut } = props;
|
||||
const [hasOpenedBrowser, setHasOpenedBrowser] = useState(false);
|
||||
|
||||
const titleText = hasOpenedBrowser
|
||||
|
@ -40,6 +44,19 @@ export function DonationVerificationModal(props: PropsType): JSX.Element {
|
|||
</>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout | undefined = setTimeout(() => {
|
||||
timeout = undefined;
|
||||
onTimedOut();
|
||||
}, _timeout ?? DAY);
|
||||
|
||||
return () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
}, [_timeout, onTimedOut]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
i18n={i18n}
|
||||
|
|
|
@ -683,8 +683,8 @@ BackupsPaidActive.args = {
|
|||
},
|
||||
};
|
||||
|
||||
export const BackupsPaidCancelled = Template.bind({});
|
||||
BackupsPaidCancelled.args = {
|
||||
export const BackupsPaidCanceled = Template.bind({});
|
||||
BackupsPaidCanceled.args = {
|
||||
page: SettingsPage.Backups,
|
||||
backupFeatureEnabled: true,
|
||||
backupLocalBackupsEnabled: true,
|
||||
|
|
|
@ -18,7 +18,10 @@ import type {
|
|||
OneTimeDonationHumanAmounts,
|
||||
DonationErrorType,
|
||||
} from '../types/Donations';
|
||||
import { donationStateSchema } from '../types/Donations';
|
||||
import {
|
||||
donationErrorTypeSchema,
|
||||
donationStateSchema,
|
||||
} from '../types/Donations';
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
import { Button, ButtonSize, ButtonVariant } from './Button';
|
||||
import { Modal } from './Modal';
|
||||
|
@ -544,7 +547,7 @@ export function PreferencesDonations({
|
|||
onCancelDonation={() => {
|
||||
clearWorkflow();
|
||||
setPage(SettingsPage.Donations);
|
||||
showToast({ toastType: ToastType.DonationCancelled });
|
||||
showToast({ toastType: ToastType.DonationCanceled });
|
||||
}}
|
||||
onRetryDonation={() => {
|
||||
resumeWorkflow();
|
||||
|
@ -558,11 +561,16 @@ export function PreferencesDonations({
|
|||
onCancelDonation={() => {
|
||||
clearWorkflow();
|
||||
setPage(SettingsPage.Donations);
|
||||
showToast({ toastType: ToastType.DonationCancelled });
|
||||
showToast({ toastType: ToastType.DonationCanceled });
|
||||
}}
|
||||
onOpenBrowser={() => {
|
||||
openLinkInWebBrowser(workflow.redirectTarget);
|
||||
}}
|
||||
onTimedOut={() => {
|
||||
clearWorkflow();
|
||||
updateLastError(donationErrorTypeSchema.Enum.TimedOut);
|
||||
setPage(SettingsPage.Donations);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
|
|
|
@ -100,8 +100,10 @@ function getToast(toastType: ToastType): AnyToast {
|
|||
};
|
||||
case ToastType.DeleteForEveryoneFailed:
|
||||
return { toastType: ToastType.DeleteForEveryoneFailed };
|
||||
case ToastType.DonationCancelled:
|
||||
return { toastType: ToastType.DonationCancelled };
|
||||
case ToastType.DonationCanceled:
|
||||
return { toastType: ToastType.DonationCanceled };
|
||||
case ToastType.DonationCanceledWithView:
|
||||
return { toastType: ToastType.DonationCanceledWithView };
|
||||
case ToastType.DonationCompleted:
|
||||
return { toastType: ToastType.DonationCompleted };
|
||||
case ToastType.DonationConfirmationNeeded:
|
||||
|
|
|
@ -280,10 +280,10 @@ export function renderToast({
|
|||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.DonationCancelled) {
|
||||
if (toastType === ToastType.DonationCanceled) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
{i18n('icu:Donations__Toast__Cancelled')}
|
||||
{i18n('icu:Donations__Toast__Canceled')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
@ -337,12 +337,16 @@ export function renderToast({
|
|||
}
|
||||
|
||||
if (
|
||||
toastType === ToastType.DonationCanceledWithView ||
|
||||
toastType === ToastType.DonationConfirmationNeeded ||
|
||||
toastType === ToastType.DonationError ||
|
||||
toastType === ToastType.DonationVerificationFailed ||
|
||||
toastType === ToastType.DonationVerificationNeeded
|
||||
) {
|
||||
const mapping = {
|
||||
[ToastType.DonationCanceledWithView]: i18n(
|
||||
'icu:Donations__Toast__Canceled'
|
||||
),
|
||||
[ToastType.DonationConfirmationNeeded]: i18n(
|
||||
'icu:Donations__Toast__ConfirmationNeeded'
|
||||
),
|
||||
|
|
|
@ -478,7 +478,7 @@ async function runDownloadAttachmentJob({
|
|||
} catch (error) {
|
||||
if (options.abortSignal.aborted) {
|
||||
log.info(
|
||||
`${logId}: Cancelled attempt ${job.attempts}. Not scheduling a retry.`
|
||||
`${logId}: Canceled attempt ${job.attempts}. Not scheduling a retry. Error:`
|
||||
);
|
||||
// Remove `pending` flag from the attachment. User can retry later.
|
||||
await addAttachmentToMessage(
|
||||
|
|
|
@ -414,7 +414,7 @@ export abstract class JobManager<CoreJobType> {
|
|||
abortController.abort();
|
||||
|
||||
// First tell those waiting for the job that it's not happening
|
||||
const rejectionError = new Error('Cancelled at JobManager.cancelJobs');
|
||||
const rejectionError = new Error('Canceled at JobManager.cancelJobs');
|
||||
const idWithAttempts = this.#getJobIdIncludingAttempts(job);
|
||||
this.#jobCompletePromises.get(idWithAttempts)?.reject(rejectionError);
|
||||
this.#jobCompletePromises.delete(idWithAttempts);
|
||||
|
@ -438,7 +438,7 @@ export abstract class JobManager<CoreJobType> {
|
|||
})
|
||||
);
|
||||
|
||||
log.warn(`${logId}: Successfully cancelled ${jobs.length} jobs`);
|
||||
log.warn(`${logId}: Successfully canceled ${jobs.length} jobs`);
|
||||
}
|
||||
|
||||
#addRunningJob(
|
||||
|
|
|
@ -868,7 +868,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
|||
) {
|
||||
if (type === conversationQueueJobEnum.enum.ProfileKey) {
|
||||
log.warn(
|
||||
"Cancelling profile share, we don't want to wait for pending verification."
|
||||
"Canceling profile share, we don't want to wait for pending verification."
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
@ -895,18 +895,16 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
|||
|
||||
if (
|
||||
verificationData.type ===
|
||||
ConversationVerificationState.VerificationCancelled
|
||||
ConversationVerificationState.VerificationCanceled
|
||||
) {
|
||||
if (verificationData.canceledAt >= timestamp) {
|
||||
log.info(
|
||||
'cancelling job; user cancelled out of verification dialog.'
|
||||
);
|
||||
log.info('canceling job; user canceled out of verification dialog.');
|
||||
shouldContinue = false;
|
||||
} else {
|
||||
log.info(
|
||||
'clearing cancellation tombstone; continuing ahead with job'
|
||||
);
|
||||
window.reduxActions.conversations.clearCancelledConversationVerification(
|
||||
window.reduxActions.conversations.clearCanceledConversationVerification(
|
||||
conversation.id
|
||||
);
|
||||
}
|
||||
|
@ -984,7 +982,7 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
|||
// Note: This should never happen, because the zod call in parseData wouldn't
|
||||
// accept data that doesn't look like our type specification.
|
||||
const problem: never = type;
|
||||
log.error(`Got job with type ${problem}; Cancelling job.`);
|
||||
log.error(`Got job with type ${problem}; Canceling job.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1056,14 +1054,14 @@ export class ConversationJobQueue extends JobQueue<ConversationQueueJobData> {
|
|||
if (untrustedServiceIds.length) {
|
||||
if (type === jobSet.ProfileKey || type === jobSet.ProfileKeyForCall) {
|
||||
log.warn(
|
||||
`Cancelling profile share, since there were ${untrustedServiceIds.length} untrusted send targets.`
|
||||
`Canceling profile share, since there were ${untrustedServiceIds.length} untrusted send targets.`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (type === jobSet.Receipts) {
|
||||
log.warn(
|
||||
`Cancelling receipt send, since there were ${untrustedServiceIds.length} untrusted send targets.`
|
||||
`Canceling receipt send, since there were ${untrustedServiceIds.length} untrusted send targets.`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ export async function sendCallingMessage(
|
|||
error instanceof UnregisteredUserError
|
||||
) {
|
||||
log.info(
|
||||
`${logId}: Send failure was OutgoingIdentityKeyError or UnregisteredUserError. Cancelling job.`
|
||||
`${logId}: Send failure was OutgoingIdentityKeyError or UnregisteredUserError. Canceling job.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ export async function sendDirectExpirationTimerUpdate(
|
|||
|
||||
if (!isDirectConversation(conversation.attributes)) {
|
||||
log.error(
|
||||
`Conversation ${conversation.idForLogging()} is not a 1:1 conversation; cancelling expiration timer job.`
|
||||
`Conversation ${conversation.idForLogging()} is not a 1:1 conversation; canceling expiration timer job.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ export async function sendDirectExpirationTimerUpdate(
|
|||
|
||||
if (!proto.dataMessage) {
|
||||
log.error(
|
||||
"ContentMessage proto didn't have a data message; cancelling job."
|
||||
"ContentMessage proto didn't have a data message; canceling job."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ export async function sendNullMessage(
|
|||
error instanceof UnregisteredUserError
|
||||
) {
|
||||
log.info(
|
||||
'Send failure was OutgoingIdentityKeyError or UnregisteredUserError. Cancelling job.'
|
||||
'Send failure was OutgoingIdentityKeyError or UnregisteredUserError. Canceling job.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -79,13 +79,13 @@ export async function sendProfileKey(
|
|||
}
|
||||
|
||||
if (!data?.isOneTimeSend && !conversation.get('profileSharing')) {
|
||||
log.info('No longer sharing profile. Cancelling job.');
|
||||
log.info('No longer sharing profile. Canceling job.');
|
||||
return;
|
||||
}
|
||||
|
||||
const profileKey = await ourProfileKeyService.get();
|
||||
if (!profileKey) {
|
||||
log.info('Unable to fetch profile. Cancelling job.');
|
||||
log.info('Unable to fetch profile. Canceling job.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -76,12 +76,12 @@ export async function sendResendRequest(
|
|||
);
|
||||
|
||||
if (!isDirectConversation(conversation.attributes)) {
|
||||
log.error('conversation is not direct, cancelling job.');
|
||||
log.error('conversation is not direct, canceling job.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isConversationUnregistered(conversation.attributes)) {
|
||||
log.error('conversation is unregistered, cancelling job.');
|
||||
log.error('conversation is unregistered, canceling job.');
|
||||
failoverToLocalReset(log, data);
|
||||
return;
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ export async function sendResendRequest(
|
|||
// Any needed blocking should still apply once the decryption error is fixed.
|
||||
|
||||
if (conversation.getAci() !== senderAci) {
|
||||
log.error('conversation was missing a aci, cancelling job.');
|
||||
log.error('conversation was missing a aci, canceling job.');
|
||||
failoverToLocalReset(log, data);
|
||||
return;
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ export async function sendResendRequest(
|
|||
error instanceof UnregisteredUserError
|
||||
) {
|
||||
log.info(
|
||||
'Group send failures were all OutgoingIdentityKeyError or UnregisteredUserError. Cancelling job.'
|
||||
'Group send failures were all OutgoingIdentityKeyError or UnregisteredUserError. Canceling job.'
|
||||
);
|
||||
|
||||
return;
|
||||
|
|
|
@ -61,7 +61,7 @@ export async function sendSavedProto(
|
|||
const serviceId = conversation.getServiceId();
|
||||
if (!serviceId) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} was missing serviceId, cancelling job.`
|
||||
`conversation ${conversation.idForLogging()} was missing serviceId, canceling job.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ export async function sendSavedProto(
|
|||
error instanceof UnregisteredUserError
|
||||
) {
|
||||
log.info(
|
||||
'Send failure was OutgoingIdentityKeyError or UnregisteredUserError. Cancelling job.'
|
||||
'Send failure was OutgoingIdentityKeyError or UnregisteredUserError. Canceling job.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -73,14 +73,14 @@ export async function sendSenderKeyDistribution(
|
|||
|
||||
if (!distributionId) {
|
||||
log.info(
|
||||
`group ${group?.idForLogging()} had no distributionid, cancelling job.`
|
||||
`group ${group?.idForLogging()} had no distributionid, canceling job.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!serviceId) {
|
||||
log.info(
|
||||
`conversation ${conversation.idForLogging()} was missing serviceId, cancelling job.`
|
||||
`conversation ${conversation.idForLogging()} was missing serviceId, canceling job.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ export async function sendSenderKeyDistribution(
|
|||
error instanceof UnregisteredUserError
|
||||
) {
|
||||
log.info(
|
||||
'Send failure was NoSenderKeyError, OutgoingIdentityKeyError or UnregisteredUserError. Cancelling job.'
|
||||
'Send failure was NoSenderKeyError, OutgoingIdentityKeyError or UnregisteredUserError. Canceling job.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -87,6 +87,21 @@ export async function initialize(): Promise<void> {
|
|||
|
||||
const shouldShowToast =
|
||||
didResumeWorkflowAtStartup() && !isDonationPageVisible();
|
||||
const isTooOld = isOlderThan(workflow.timestamp, DAY);
|
||||
|
||||
if (
|
||||
isTooOld &&
|
||||
(workflow.type === donationStateSchema.Enum.INTENT_METHOD ||
|
||||
workflow.type === donationStateSchema.Enum.INTENT_REDIRECT)
|
||||
) {
|
||||
log.info(
|
||||
`initialize: Workflow at ${workflow.type} is too old, canceling donation.`
|
||||
);
|
||||
await clearDonation();
|
||||
await failDonation(donationErrorTypeSchema.Enum.TimedOut);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (workflow.type === donationStateSchema.Enum.INTENT_METHOD) {
|
||||
if (shouldShowToast) {
|
||||
|
@ -783,6 +798,13 @@ async function failDonation(errorType: DonationErrorType): Promise<void> {
|
|||
window.reduxActions.toast.showToast({
|
||||
toastType: ToastType.DonationVerificationFailed,
|
||||
});
|
||||
} else if (errorType === donationErrorTypeSchema.Enum.TimedOut) {
|
||||
log.info(
|
||||
`${logId}: Donation page not visible. Showing 'donation canceled w/view' toast.`
|
||||
);
|
||||
window.reduxActions.toast.showToast({
|
||||
toastType: ToastType.DonationCanceledWithView,
|
||||
});
|
||||
} else {
|
||||
log.info(
|
||||
`${logId}: Donation page not visible. Showing 'error processing donation' toast.`
|
||||
|
@ -891,7 +913,8 @@ function isDonationPageVisible() {
|
|||
const { selectedLocation } = window.reduxStore.getState().nav;
|
||||
return (
|
||||
selectedLocation.tab === NavTab.Settings &&
|
||||
(selectedLocation.details.page === SettingsPage.DonationsDonateFlow ||
|
||||
(selectedLocation.details.page === SettingsPage.Donations ||
|
||||
selectedLocation.details.page === SettingsPage.DonationsDonateFlow ||
|
||||
selectedLocation.details.page === SettingsPage.DonationsReceiptList)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -200,9 +200,7 @@ export class ProfileService {
|
|||
|
||||
this.#jobsByConversationId.forEach(job => {
|
||||
job.reject(
|
||||
new Error(
|
||||
`ProfileService.clearAll: job cancelled because '${reason}'`
|
||||
)
|
||||
new Error(`ProfileService.clearAll: job canceled because '${reason}'`)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -483,12 +483,12 @@ export function toAccountRecord(
|
|||
if (typeof subscriberCurrencyCode === 'string') {
|
||||
accountRecord.donorSubscriberCurrencyCode = subscriberCurrencyCode;
|
||||
}
|
||||
const donorSubscriptionManuallyCancelled = window.storage.get(
|
||||
const donorSubscriptionManuallyCanceled = window.storage.get(
|
||||
'donorSubscriptionManuallyCancelled'
|
||||
);
|
||||
if (typeof donorSubscriptionManuallyCancelled === 'boolean') {
|
||||
if (typeof donorSubscriptionManuallyCanceled === 'boolean') {
|
||||
accountRecord.donorSubscriptionManuallyCancelled =
|
||||
donorSubscriptionManuallyCancelled;
|
||||
donorSubscriptionManuallyCanceled;
|
||||
}
|
||||
|
||||
accountRecord.backupSubscriberData = generateBackupsSubscriberData();
|
||||
|
|
|
@ -506,7 +506,7 @@ export type ConversationVerificationData = ReadonlyDeep<
|
|||
>;
|
||||
}
|
||||
| {
|
||||
type: ConversationVerificationState.VerificationCancelled;
|
||||
type: ConversationVerificationState.VerificationCanceled;
|
||||
canceledAt: number;
|
||||
}
|
||||
>;
|
||||
|
@ -618,8 +618,7 @@ export const getConversationCallMode = (
|
|||
|
||||
const CANCEL_CONVERSATION_PENDING_VERIFICATION =
|
||||
'conversations/CANCEL_CONVERSATION_PENDING_VERIFICATION';
|
||||
const CLEAR_CANCELLED_VERIFICATION =
|
||||
'conversations/CLEAR_CANCELLED_VERIFICATION';
|
||||
const CLEAR_CANCELED_VERIFICATION = 'conversations/CLEAR_CANCELED_VERIFICATION';
|
||||
const CLEAR_CONVERSATIONS_PENDING_VERIFICATION =
|
||||
'conversations/CLEAR_CONVERSATIONS_PENDING_VERIFICATION';
|
||||
export const COLORS_CHANGED = 'conversations/COLORS_CHANGED';
|
||||
|
@ -668,8 +667,8 @@ type ClearInvitedServiceIdsForNewlyCreatedGroupActionType = ReadonlyDeep<{
|
|||
type ClearVerificationDataByConversationActionType = ReadonlyDeep<{
|
||||
type: typeof CLEAR_CONVERSATIONS_PENDING_VERIFICATION;
|
||||
}>;
|
||||
type ClearCancelledVerificationActionType = ReadonlyDeep<{
|
||||
type: typeof CLEAR_CANCELLED_VERIFICATION;
|
||||
type ClearCanceledVerificationActionType = ReadonlyDeep<{
|
||||
type: typeof CLEAR_CANCELED_VERIFICATION;
|
||||
payload: {
|
||||
conversationId: string;
|
||||
};
|
||||
|
@ -1048,7 +1047,7 @@ export type ConsumePreloadDataActionType = ReadonlyDeep<{
|
|||
export type ConversationActionType =
|
||||
| AddPreloadDataActionType
|
||||
| CancelVerificationDataByConversationActionType
|
||||
| ClearCancelledVerificationActionType
|
||||
| ClearCanceledVerificationActionType
|
||||
| ClearGroupCreationErrorActionType
|
||||
| ClearInvitedServiceIdsForNewlyCreatedGroupActionType
|
||||
| ClearTargetedMessageActionType
|
||||
|
@ -1134,7 +1133,7 @@ export const actions = {
|
|||
cancelAttachmentDownload,
|
||||
cancelConversationVerification,
|
||||
changeHasGroupLink,
|
||||
clearCancelledConversationVerification,
|
||||
clearCanceledConversationVerification,
|
||||
clearGroupCreationError,
|
||||
clearInvitedServiceIdsForNewlyCreatedGroup,
|
||||
clearTargetedMessage,
|
||||
|
@ -2783,11 +2782,11 @@ function verifyConversationsStoppingSend(): ThunkAction<
|
|||
};
|
||||
}
|
||||
|
||||
export function clearCancelledConversationVerification(
|
||||
export function clearCanceledConversationVerification(
|
||||
conversationId: string
|
||||
): ClearCancelledVerificationActionType {
|
||||
): ClearCanceledVerificationActionType {
|
||||
return {
|
||||
type: CLEAR_CANCELLED_VERIFICATION,
|
||||
type: CLEAR_CANCELED_VERIFICATION,
|
||||
payload: {
|
||||
conversationId,
|
||||
},
|
||||
|
@ -5229,7 +5228,7 @@ function getVerificationDataForConversation({
|
|||
|
||||
if (
|
||||
!existing ||
|
||||
existing.type === ConversationVerificationState.VerificationCancelled
|
||||
existing.type === ConversationVerificationState.VerificationCanceled
|
||||
) {
|
||||
return {
|
||||
[conversationId]: {
|
||||
|
@ -5527,7 +5526,7 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === CLEAR_CANCELLED_VERIFICATION) {
|
||||
if (action.type === CLEAR_CANCELED_VERIFICATION) {
|
||||
const { conversationId } = action.payload;
|
||||
const { verificationDataByConversation } = state;
|
||||
|
||||
|
@ -5568,13 +5567,13 @@ export function reducer(
|
|||
|
||||
for (const [conversationId, data] of entries) {
|
||||
if (
|
||||
data.type === ConversationVerificationState.VerificationCancelled &&
|
||||
data.type === ConversationVerificationState.VerificationCanceled &&
|
||||
data.canceledAt > canceledAt
|
||||
) {
|
||||
newverificationDataByConversation[conversationId] = data;
|
||||
} else {
|
||||
newverificationDataByConversation[conversationId] = {
|
||||
type: ConversationVerificationState.VerificationCancelled,
|
||||
type: ConversationVerificationState.VerificationCanceled,
|
||||
canceledAt,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export enum OneTimeModalState {
|
|||
|
||||
export enum ConversationVerificationState {
|
||||
PendingVerification = 'PendingVerification',
|
||||
VerificationCancelled = 'VerificationCancelled',
|
||||
VerificationCanceled = 'VerificationCanceled',
|
||||
}
|
||||
|
||||
export enum TargetedMessageSource {
|
||||
|
|
|
@ -261,8 +261,15 @@ export function reducer(
|
|||
if (action.type === UPDATE_WORKFLOW) {
|
||||
const { nextWorkflow } = action.payload;
|
||||
|
||||
// If we've cleared the workflow or are starting afresh, we clear the startup flag
|
||||
const didResumeWorkflowAtStartup =
|
||||
!nextWorkflow || nextWorkflow.type === donationStateSchema.Enum.INTENT
|
||||
? false
|
||||
: state.didResumeWorkflowAtStartup;
|
||||
|
||||
return {
|
||||
...state,
|
||||
didResumeWorkflowAtStartup,
|
||||
currentWorkflow: nextWorkflow,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ export function changeLocation(
|
|||
});
|
||||
|
||||
if (needToCancel) {
|
||||
log.info(`${logId}: Cancelling navigation`);
|
||||
log.info(`${logId}: Canceling navigation`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -54,10 +54,10 @@ describe('util/profiles', () => {
|
|||
|
||||
service.clearAll('testing');
|
||||
|
||||
await assert.isRejected(promise1, 'job cancelled');
|
||||
await assert.isRejected(promise2, 'job cancelled');
|
||||
await assert.isRejected(promise3, 'job cancelled');
|
||||
await assert.isRejected(promise4, 'job cancelled');
|
||||
await assert.isRejected(promise1, 'job canceled');
|
||||
await assert.isRejected(promise2, 'job canceled');
|
||||
await assert.isRejected(promise3, 'job canceled');
|
||||
await assert.isRejected(promise4, 'job canceled');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -147,9 +147,9 @@ describe('util/profiles', () => {
|
|||
// Never queued
|
||||
const promise5 = service.get(SERVICE_ID_5, null);
|
||||
|
||||
await assert.isRejected(promise2, 'job cancelled');
|
||||
await assert.isRejected(promise3, 'job cancelled');
|
||||
await assert.isRejected(promise4, 'job cancelled');
|
||||
await assert.isRejected(promise2, 'job canceled');
|
||||
await assert.isRejected(promise3, 'job canceled');
|
||||
await assert.isRejected(promise4, 'job canceled');
|
||||
await assert.isRejected(promise5, 'paused queue');
|
||||
|
||||
assert.strictEqual(runCount, 3, 'after await');
|
||||
|
@ -184,9 +184,9 @@ describe('util/profiles', () => {
|
|||
// Queued, because we aren't pausing
|
||||
const promise5 = service.get(SERVICE_ID_5, null);
|
||||
|
||||
await assert.isRejected(promise2, 'job cancelled');
|
||||
await assert.isRejected(promise3, 'job cancelled');
|
||||
await assert.isRejected(promise4, 'job cancelled');
|
||||
await assert.isRejected(promise2, 'job canceled');
|
||||
await assert.isRejected(promise3, 'job canceled');
|
||||
await assert.isRejected(promise4, 'job canceled');
|
||||
|
||||
// It didn't succeed, but we log and resolve as normal
|
||||
await assert.isFulfilled(promise5);
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
TARGETED_CONVERSATION_CHANGED,
|
||||
actions,
|
||||
cancelConversationVerification,
|
||||
clearCancelledConversationVerification,
|
||||
clearCanceledConversationVerification,
|
||||
getConversationCallMode,
|
||||
getEmptyState,
|
||||
reducer,
|
||||
|
@ -940,12 +940,12 @@ describe('both/state/ducks/conversations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('stomps on VerificationCancelled state', () => {
|
||||
it('stomps on VerificationCanceled state', () => {
|
||||
const state: ConversationsStateType = {
|
||||
...getEmptyState(),
|
||||
verificationDataByConversation: {
|
||||
'convo A': {
|
||||
type: ConversationVerificationState.VerificationCancelled,
|
||||
type: ConversationVerificationState.VerificationCanceled,
|
||||
canceledAt: Date.now(),
|
||||
},
|
||||
},
|
||||
|
@ -1111,19 +1111,19 @@ describe('both/state/ducks/conversations', () => {
|
|||
|
||||
assert.deepStrictEqual(actual.verificationDataByConversation, {
|
||||
'convo A': {
|
||||
type: ConversationVerificationState.VerificationCancelled,
|
||||
type: ConversationVerificationState.VerificationCanceled,
|
||||
canceledAt: now,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('updates timestamp for existing VerificationCancelled state', () => {
|
||||
it('updates timestamp for existing VerificationCanceled state', () => {
|
||||
const now = Date.now();
|
||||
const state: ConversationsStateType = {
|
||||
...getEmptyState(),
|
||||
verificationDataByConversation: {
|
||||
'convo A': {
|
||||
type: ConversationVerificationState.VerificationCancelled,
|
||||
type: ConversationVerificationState.VerificationCanceled,
|
||||
canceledAt: now - 1,
|
||||
},
|
||||
},
|
||||
|
@ -1133,19 +1133,19 @@ describe('both/state/ducks/conversations', () => {
|
|||
|
||||
assert.deepStrictEqual(actual.verificationDataByConversation, {
|
||||
'convo A': {
|
||||
type: ConversationVerificationState.VerificationCancelled,
|
||||
type: ConversationVerificationState.VerificationCanceled,
|
||||
canceledAt: now,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('uses newest timestamp when updating existing VerificationCancelled state', () => {
|
||||
it('uses newest timestamp when updating existing VerificationCanceled state', () => {
|
||||
const now = Date.now();
|
||||
const state: ConversationsStateType = {
|
||||
...getEmptyState(),
|
||||
verificationDataByConversation: {
|
||||
'convo A': {
|
||||
type: ConversationVerificationState.VerificationCancelled,
|
||||
type: ConversationVerificationState.VerificationCanceled,
|
||||
canceledAt: now,
|
||||
},
|
||||
},
|
||||
|
@ -1155,7 +1155,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
|
||||
assert.deepStrictEqual(actual.verificationDataByConversation, {
|
||||
'convo A': {
|
||||
type: ConversationVerificationState.VerificationCancelled,
|
||||
type: ConversationVerificationState.VerificationCanceled,
|
||||
canceledAt: now,
|
||||
},
|
||||
});
|
||||
|
@ -1171,20 +1171,20 @@ describe('both/state/ducks/conversations', () => {
|
|||
});
|
||||
|
||||
describe('CANCEL_CONVERSATION_PENDING_VERIFICATION', () => {
|
||||
it('removes existing VerificationCancelled state', () => {
|
||||
it('removes existing VerificationCanceled state', () => {
|
||||
const now = Date.now();
|
||||
const state: ConversationsStateType = {
|
||||
...getEmptyState(),
|
||||
verificationDataByConversation: {
|
||||
'convo A': {
|
||||
type: ConversationVerificationState.VerificationCancelled,
|
||||
type: ConversationVerificationState.VerificationCanceled,
|
||||
canceledAt: now,
|
||||
},
|
||||
},
|
||||
};
|
||||
const actual = reducer(
|
||||
state,
|
||||
clearCancelledConversationVerification('convo A')
|
||||
clearCanceledConversationVerification('convo A')
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(actual.verificationDataByConversation, {});
|
||||
|
@ -1202,7 +1202,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
};
|
||||
const actual = reducer(
|
||||
state,
|
||||
clearCancelledConversationVerification('convo A')
|
||||
clearCanceledConversationVerification('convo A')
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(actual, state);
|
||||
|
@ -1212,7 +1212,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
const state: ConversationsStateType = getEmptyState();
|
||||
const actual = reducer(
|
||||
state,
|
||||
clearCancelledConversationVerification('convo A')
|
||||
clearCanceledConversationVerification('convo A')
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(actual, state);
|
||||
|
|
|
@ -234,11 +234,11 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
...state.conversations,
|
||||
verificationDataByConversation: {
|
||||
direct1: {
|
||||
type: ConversationVerificationState.VerificationCancelled,
|
||||
type: ConversationVerificationState.VerificationCanceled,
|
||||
canceledAt: Date.now(),
|
||||
},
|
||||
direct2: {
|
||||
type: ConversationVerificationState.VerificationCancelled,
|
||||
type: ConversationVerificationState.VerificationCanceled,
|
||||
canceledAt: Date.now(),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -424,7 +424,7 @@ describe('backups', function (this: Mocha.Suite) {
|
|||
await window.locator('.module-message >> "Message 33"').waitFor();
|
||||
});
|
||||
|
||||
it('handles remote ephemeral backup cancelation', async function () {
|
||||
it('handles remote ephemeral backup cancellation', async function () {
|
||||
const ephemeralBackupKey = randomBytes(32);
|
||||
|
||||
const { phone, server } = bootstrap;
|
||||
|
|
|
@ -282,7 +282,7 @@ export class SocketManager extends EventListener {
|
|||
try {
|
||||
await sleep(timeout, reconnectController.signal);
|
||||
} catch {
|
||||
log.info('reconnect cancelled');
|
||||
log.info('reconnect canceled');
|
||||
return;
|
||||
} finally {
|
||||
if (this.#reconnectController === reconnectController) {
|
||||
|
|
|
@ -2108,10 +2108,10 @@ export function initialize({
|
|||
}
|
||||
function cancelInflightRequests(reason: string) {
|
||||
const logId = `cancelInflightRequests/${reason}`;
|
||||
log.warn(`${logId}: Cancelling ${inflightRequests.size} requests`);
|
||||
log.warn(`${logId}: Canceling ${inflightRequests.size} requests`);
|
||||
for (const request of inflightRequests) {
|
||||
try {
|
||||
request(new Error(`${logId}: Cancelled!`));
|
||||
request(new Error(`${logId}: Canceled!`));
|
||||
} catch (error: unknown) {
|
||||
log.error(
|
||||
`${logId}: Failed to cancel request: ${toLogFormat(error)}`
|
||||
|
|
|
@ -21,6 +21,8 @@ export const donationErrorTypeSchema = z.enum([
|
|||
'GeneralError',
|
||||
// Any 4xx error when adding payment method or confirming intent
|
||||
'PaymentDeclined',
|
||||
// When it's been too long since the last step of the donation, and card wasn't charged
|
||||
'TimedOut',
|
||||
]);
|
||||
export type DonationErrorType = z.infer<typeof donationErrorTypeSchema>;
|
||||
|
||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -180,6 +180,7 @@ export type StorageAccessType = {
|
|||
areWeASubscriber: boolean;
|
||||
subscriberId: Uint8Array;
|
||||
subscriberCurrencyCode: string;
|
||||
// Note: for historical reasons, this has two l's
|
||||
donorSubscriptionManuallyCancelled: boolean;
|
||||
backupsSubscriberId: Uint8Array;
|
||||
backupsSubscriberPurchaseToken: string;
|
||||
|
|
|
@ -31,7 +31,8 @@ export enum ToastType {
|
|||
DecryptionError = 'DecryptionError',
|
||||
DebugLogError = 'DebugLogError',
|
||||
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
|
||||
DonationCancelled = 'DonationCancelled',
|
||||
DonationCanceled = 'DonationCanceled',
|
||||
DonationCanceledWithView = 'DonationCanceledWithView',
|
||||
DonationCompleted = 'DonationCompleted',
|
||||
DonationConfirmationNeeded = 'DonationConfirmationNeeded',
|
||||
DonationError = 'DonationError',
|
||||
|
@ -127,7 +128,8 @@ export type AnyToast =
|
|||
| { toastType: ToastType.DangerousFileType }
|
||||
| { toastType: ToastType.DebugLogError }
|
||||
| { toastType: ToastType.DeleteForEveryoneFailed }
|
||||
| { toastType: ToastType.DonationCancelled }
|
||||
| { toastType: ToastType.DonationCanceled }
|
||||
| { toastType: ToastType.DonationCanceledWithView }
|
||||
| { toastType: ToastType.DonationCompleted }
|
||||
| { toastType: ToastType.DonationConfirmationNeeded }
|
||||
| { toastType: ToastType.DonationError }
|
||||
|
|
|
@ -192,15 +192,15 @@ export abstract class Updater {
|
|||
return this.#checkForUpdatesMaybeInstall(CheckType.ForceDownload);
|
||||
}
|
||||
|
||||
// If the updater was about to restart the app but the user cancelled it, show dialog
|
||||
// If the updater was about to restart the app but the user canceled it, show dialog
|
||||
// to let them retry the restart
|
||||
public onRestartCancelled(): void {
|
||||
public onRestartCanceled(): void {
|
||||
if (!this.#restarting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
'updater/onRestartCancelled: restart was cancelled. forcing update to reset updater state'
|
||||
'updater/onRestartCanceled: restart was canceled. forcing update to reset updater state'
|
||||
);
|
||||
this.#restarting = false;
|
||||
markShouldNotQuit();
|
||||
|
|
|
@ -56,9 +56,9 @@ export async function force(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
export function onRestartCancelled(): void {
|
||||
export function onRestartCanceled(): void {
|
||||
if (updater) {
|
||||
updater.onRestartCancelled();
|
||||
updater.onRestartCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -291,7 +291,7 @@ export function createIPCEvents(
|
|||
);
|
||||
return true;
|
||||
} catch {
|
||||
log.info('requestCloseConfirmation: Close cancelled by user.');
|
||||
log.info('requestCloseConfirmation: Close canceled by user.');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue