Handle multiple visits to Paypal approval return URI

This commit is contained in:
ayumi-signal
2026-02-11 06:15:13 -08:00
committed by GitHub
parent 291000f297
commit 09b006e14b
4 changed files with 64 additions and 12 deletions

View File

@@ -262,12 +262,34 @@ export async function approvePaypalPayment({
try {
const existing = _getWorkflowFromRedux();
const lastReturnToken = _getLastReturnTokenFromRedux();
if (!existing) {
// This can happen if after you finished a Paypal donation, but you go back to
// the Paypal website and click Return to Signal again.
if (returnToken === lastReturnToken) {
if (!isDonationPageVisible()) {
redirectToPage(SettingsPage.Donations);
}
return;
}
throw new Error(
'approvePaypalPayment: Cannot finish nonexistent workflow!'
);
}
// If you visit the approval link twice in succession, this can happen
if (isPaypalAlreadyApproved(existing)) {
log.warn(
'approvePaypalPayment: Existing workflow already approved, not trying to approve again'
);
if (!isDonationPageVisible()) {
redirectToPage(SettingsPage.Donations);
}
return;
}
if (payerId == null || paymentToken == null) {
throw new Error(
'approvePaypalPayment: payerId or paymentToken are missing'
@@ -296,15 +318,20 @@ export async function cancelPaypalPayment(_returnToken: string): Promise<void> {
log.info(`${logId}: User visited PayPal cancel URI, showing donate flow`);
if (!isDonationPageVisible()) {
window.reduxActions.nav.changeLocation({
tab: NavTab.Settings,
details: {
page: SettingsPage.DonationsDonateFlow,
},
});
redirectToPage(SettingsPage.DonationsDonateFlow);
}
}
function isPaypalAlreadyApproved(workflow: DonationWorkflow): boolean {
const { type } = workflow;
return (
type === donationStateSchema.Enum.PAYPAL_APPROVED ||
type === donationStateSchema.Enum.PAYMENT_CONFIRMED ||
type === donationStateSchema.Enum.RECEIPT ||
type === donationStateSchema.Enum.DONE
);
}
export async function clearDonation(): Promise<void> {
runDonationAbortController?.abort();
await _saveWorkflow(undefined);
@@ -531,12 +558,7 @@ export async function _runDonationWorkflow(): Promise<void> {
} else if (type === donationStateSchema.Enum.DONE) {
if (isDonationPageVisible()) {
if (isDonationsDonateFlowVisible()) {
window.reduxActions.nav.changeLocation({
tab: NavTab.Settings,
details: {
page: SettingsPage.Donations,
},
});
redirectToPage(SettingsPage.Donations);
}
} else {
log.info(
@@ -1178,6 +1200,9 @@ async function _saveWorkflow(
await _saveWorkflowToStorage(workflow);
_saveWorkflowToRedux(workflow);
}
export function _getLastReturnTokenFromRedux(): string | undefined {
return window.reduxStore.getState().donations.lastReturnToken;
}
export function _getWorkflowFromRedux(): DonationWorkflow | undefined {
return window.reduxStore.getState().donations.currentWorkflow;
}
@@ -1283,6 +1308,20 @@ function isDonationsDonateFlowVisible() {
);
}
function redirectToPage(
page:
| SettingsPage.Donations
| SettingsPage.DonationsDonateFlow
| SettingsPage.DonationsReceiptList
) {
window.reduxActions.nav.changeLocation({
tab: NavTab.Settings,
details: {
page,
},
});
}
// Working with zkgroup receipts
function getServerPublicParams(): ServerPublicParams {

View File

@@ -26,6 +26,7 @@ export function getDonationsForRedux(): DonationsStateType {
currentWorkflow,
didResumeWorkflowAtStartup: Boolean(currentWorkflow),
lastError: undefined,
lastReturnToken: undefined,
receipts: donationReceipts,
configCache: undefined,
};

View File

@@ -45,6 +45,7 @@ export type DonationsStateType = ReadonlyDeep<{
currentWorkflow: DonationWorkflow | undefined;
didResumeWorkflowAtStartup: boolean;
lastError: DonationErrorType | undefined;
lastReturnToken: string | undefined;
receipts: Array<DonationReceipt>;
configCache: OneTimeDonationHumanAmounts | undefined;
}>;
@@ -435,6 +436,7 @@ export function getEmptyState(): DonationsStateType {
currentWorkflow: undefined,
didResumeWorkflowAtStartup: false,
lastError: undefined,
lastReturnToken: undefined,
receipts: [],
configCache: undefined,
};
@@ -475,6 +477,14 @@ export function reducer(
if (action.type === UPDATE_WORKFLOW) {
const { nextWorkflow } = action.payload;
let lastReturnToken: string | undefined;
const { currentWorkflow } = state;
if (currentWorkflow && 'returnToken' in currentWorkflow) {
lastReturnToken = currentWorkflow.returnToken;
} else {
lastReturnToken = state.lastReturnToken;
}
// If we've cleared the workflow or are starting afresh, we clear the startup flag
const didResumeWorkflowAtStartup =
!nextWorkflow || nextWorkflow.type === donationStateSchema.Enum.INTENT
@@ -485,6 +495,7 @@ export function reducer(
...state,
didResumeWorkflowAtStartup,
currentWorkflow: nextWorkflow,
lastReturnToken,
};
}

View File

@@ -137,6 +137,7 @@ window.testUtilities = {
currentWorkflow: undefined,
didResumeWorkflowAtStartup: false,
lastError: undefined,
lastReturnToken: undefined,
receipts: [],
configCache: undefined,
},