Donations: Introduce timeouts in early stages of the workflow

This commit is contained in:
Scott Nonnenberg 2025-07-31 07:15:59 +10:00 committed by GitHub
commit fd794ae90d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 198 additions and 109 deletions

View file

@ -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 youre a resident of the United States, please retain this receipt for your tax records. Signal Technology Foundation is a tax-exempt nonprofit organization in the United States under section 501c3 of the Internal Revenue Code. Our Federal Tax ID is 82-4506840.",
"description": "Footer text shown on donation receipts explaining tax deductibility and Signal's mission"
},
"icu:Donations__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"

View file

@ -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),
]);

View file

@ -885,7 +885,7 @@ async function createWindow() {
);
}
if (!shouldClose) {
updater.onRestartCancelled();
updater.onRestartCanceled();
return;
}

View file

@ -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

View file

@ -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 =

View file

@ -47,3 +47,12 @@ export function PaymentDeclined(): JSX.Element {
/>
);
}
export function TimedOut(): JSX.Element {
return (
<DonationErrorModal
{...defaultProps}
errorType={donationErrorTypeSchema.Enum.TimedOut}
/>
);
}

View file

@ -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);

View file

@ -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} />;
}

View file

@ -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}

View file

@ -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,

View file

@ -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 (

View file

@ -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:

View file

@ -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'
),

View file

@ -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(

View file

@ -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(

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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)
);
}

View file

@ -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}'`)
);
});

View file

@ -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();

View file

@ -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,
};
}

View file

@ -23,7 +23,7 @@ export enum OneTimeModalState {
export enum ConversationVerificationState {
PendingVerification = 'PendingVerification',
VerificationCancelled = 'VerificationCancelled',
VerificationCanceled = 'VerificationCanceled',
}
export enum TargetedMessageSource {

View file

@ -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,
};
}

View file

@ -61,7 +61,7 @@ export function changeLocation(
});
if (needToCancel) {
log.info(`${logId}: Cancelling navigation`);
log.info(`${logId}: Canceling navigation`);
return;
}

View file

@ -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);

View file

@ -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);

View file

@ -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(),
},
},

View file

@ -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;

View file

@ -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) {

View file

@ -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)}`

View file

@ -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>;

View file

@ -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;

View file

@ -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 }

View file

@ -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();

View file

@ -56,9 +56,9 @@ export async function force(): Promise<void> {
}
}
export function onRestartCancelled(): void {
export function onRestartCanceled(): void {
if (updater) {
updater.onRestartCancelled();
updater.onRestartCanceled();
}
}

View file

@ -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;
}
},