From 35013f0415e47d979947fffbaa53027cffddcc9c Mon Sep 17 00:00:00 2001 From: Stephen McGruer Date: Mon, 6 Apr 2026 11:29:32 -0400 Subject: [PATCH 1/6] Payment Handler: Add test for OperationError propagation This change adds a new manual test to verify that if a web-based payment handler rejects the promise passed to respondWith with an OperationError, the original PaymentRequest.show() call also rejects with an OperationError. For other error types (like SyntaxError), show() should still reject with an AbortError. Spec: https://github.com/w3c/web-based-payment-handler/issues/428 --- .../app-reject-errors.js | 34 +++++++++++++++++ .../payment-app/reject-errors.html | 18 +++++++++ ...ayment-request-reject-errors-manifest.json | 15 ++++++++ ...t-reject-operation-error-manual.https.html | 37 +++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 web-based-payment-handler/app-reject-errors.js create mode 100644 web-based-payment-handler/payment-app/reject-errors.html create mode 100644 web-based-payment-handler/payment-request-reject-errors-manifest.json create mode 100644 web-based-payment-handler/payment-request-reject-operation-error-manual.https.html diff --git a/web-based-payment-handler/app-reject-errors.js b/web-based-payment-handler/app-reject-errors.js new file mode 100644 index 00000000000000..bbcb4c575a9f61 --- /dev/null +++ b/web-based-payment-handler/app-reject-errors.js @@ -0,0 +1,34 @@ +/** + * A payment handler that opens a window and allows the user to trigger + * different types of promise rejections for testing error propagation. + */ +self.addEventListener('canmakepayment', event => { + event.respondWith(true); +}); + +self.addEventListener('paymentrequest', event => { + const methodName = event.methodData[0].supportedMethods; + event.respondWith(new Promise((resolve, reject) => { + const handler = (msgEvent) => { + if (msgEvent.data === 'success') { + resolve({ + methodName: methodName, + details: {status: 'success'}, + }); + } else if (msgEvent.data === 'reject-operation-error') { + reject(new DOMException('Reject with OperationError', 'OperationError')); + } else if (msgEvent.data === 'reject-syntax-error') { + reject(new DOMException('Reject with SyntaxError', 'SyntaxError')); + } else { + return; // Message not for us. + } + self.removeEventListener('message', handler); + }; + self.addEventListener('message', handler); + + event.openWindow('payment-app/reject-errors.html').catch(err => { + self.removeEventListener('message', handler); + reject(err); + }); + })); +}); diff --git a/web-based-payment-handler/payment-app/reject-errors.html b/web-based-payment-handler/payment-app/reject-errors.html new file mode 100644 index 00000000000000..4f7361ee52f3fe --- /dev/null +++ b/web-based-payment-handler/payment-app/reject-errors.html @@ -0,0 +1,18 @@ + + +Reject Error Payment App +

Please click one of the buttons below to complete or reject the payment.

+ + + + + diff --git a/web-based-payment-handler/payment-request-reject-errors-manifest.json b/web-based-payment-handler/payment-request-reject-errors-manifest.json new file mode 100644 index 00000000000000..f77db676eeeb78 --- /dev/null +++ b/web-based-payment-handler/payment-request-reject-errors-manifest.json @@ -0,0 +1,15 @@ +{ + "default_applications": ["payment-request-reject-errors-manifest.json"], + "name": "Reject Errors Payment Handler", + "icons": [ + { + "src": "/images/rgrg-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ], + "serviceworker": { + "src": "app-reject-errors.js", + "scope": "payment-request-reject-errors-payment-app/" + } +} diff --git a/web-based-payment-handler/payment-request-reject-operation-error-manual.https.html b/web-based-payment-handler/payment-request-reject-operation-error-manual.https.html new file mode 100644 index 00000000000000..24052256d83bf0 --- /dev/null +++ b/web-based-payment-handler/payment-request-reject-operation-error-manual.https.html @@ -0,0 +1,37 @@ + + +Manual Tests for rejecting respondWith with errors + + + + + +

This test verifies that if a payment app rejects the promise passed to respondWith with an OperationError, the original PaymentRequest.show() call also rejects with an OperationError. For other error types, it should still reject with an AbortError.

+

Please follow these instructions:

+
    +
  1. For the first test (OperationError), select "Reject Errors Payment Handler" in the payment sheet. When the app window opens, click "Reject with OperationError".
  2. +
  3. For the second test (SyntaxError), select "Reject Errors Payment Handler" in the payment sheet. When the app window opens, click "Reject with SyntaxError".
  4. +
+ + From fc19aa85196ded78196efcbc0141eda65005da73 Mon Sep 17 00:00:00 2001 From: Stephen McGruer Date: Mon, 6 Apr 2026 12:14:05 -0400 Subject: [PATCH 2/6] Fixup --- .../app-reject-errors.js | 50 ++++++++++++------- ...equest-reject-errors-manifest.json.headers | 1 + 2 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 web-based-payment-handler/payment-request-reject-errors-manifest.json.headers diff --git a/web-based-payment-handler/app-reject-errors.js b/web-based-payment-handler/app-reject-errors.js index bbcb4c575a9f61..3d2b30e96d2877 100644 --- a/web-based-payment-handler/app-reject-errors.js +++ b/web-based-payment-handler/app-reject-errors.js @@ -2,32 +2,46 @@ * A payment handler that opens a window and allows the user to trigger * different types of promise rejections for testing error propagation. */ + +let resolver = null; +let rejecter = null; +let activeMethodName = null; + self.addEventListener('canmakepayment', event => { event.respondWith(true); }); +self.addEventListener('message', msgEvent => { + if (!resolver || !rejecter) return; + + if (msgEvent.data === 'success') { + resolver({ + methodName: activeMethodName, + details: {status: 'success'}, + }); + } else if (msgEvent.data === 'reject-operation-error') { + rejecter(new DOMException('Reject with OperationError', 'OperationError')); + } else if (msgEvent.data === 'reject-syntax-error') { + rejecter(new DOMException('Reject with SyntaxError', 'SyntaxError')); + } else { + return; // Message not for us. + } + + resolver = null; + rejecter = null; + activeMethodName = null; +}); + self.addEventListener('paymentrequest', event => { - const methodName = event.methodData[0].supportedMethods; + activeMethodName = event.methodData[0].supportedMethods; event.respondWith(new Promise((resolve, reject) => { - const handler = (msgEvent) => { - if (msgEvent.data === 'success') { - resolve({ - methodName: methodName, - details: {status: 'success'}, - }); - } else if (msgEvent.data === 'reject-operation-error') { - reject(new DOMException('Reject with OperationError', 'OperationError')); - } else if (msgEvent.data === 'reject-syntax-error') { - reject(new DOMException('Reject with SyntaxError', 'SyntaxError')); - } else { - return; // Message not for us. - } - self.removeEventListener('message', handler); - }; - self.addEventListener('message', handler); + resolver = resolve; + rejecter = reject; event.openWindow('payment-app/reject-errors.html').catch(err => { - self.removeEventListener('message', handler); + resolver = null; + rejecter = null; + activeMethodName = null; reject(err); }); })); diff --git a/web-based-payment-handler/payment-request-reject-errors-manifest.json.headers b/web-based-payment-handler/payment-request-reject-errors-manifest.json.headers new file mode 100644 index 00000000000000..88ddbb0a5e8c57 --- /dev/null +++ b/web-based-payment-handler/payment-request-reject-errors-manifest.json.headers @@ -0,0 +1 @@ +Link: ; rel="payment-method-manifest" From dfc291281fd5ab30d250d712db6cf10cbcfb1cd4 Mon Sep 17 00:00:00 2001 From: Stephen McGruer Date: Mon, 6 Apr 2026 12:22:16 -0400 Subject: [PATCH 3/6] Temporary logging --- .../app-reject-errors.js | 17 ++++++++++++++-- .../payment-app/reject-errors.html | 5 +++++ ...t-reject-operation-error-manual.https.html | 20 +++++++++++++++++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/web-based-payment-handler/app-reject-errors.js b/web-based-payment-handler/app-reject-errors.js index 3d2b30e96d2877..f3a1283305e64d 100644 --- a/web-based-payment-handler/app-reject-errors.js +++ b/web-based-payment-handler/app-reject-errors.js @@ -12,18 +12,26 @@ self.addEventListener('canmakepayment', event => { }); self.addEventListener('message', msgEvent => { - if (!resolver || !rejecter) return; + console.log(`[ServiceWorker] Received message from payment app: ${msgEvent.data}`); + if (!resolver || !rejecter) { + console.error('[ServiceWorker] Received message, but no active payment request found.'); + return; + } if (msgEvent.data === 'success') { + console.log('[ServiceWorker] Resolving payment request with success'); resolver({ methodName: activeMethodName, details: {status: 'success'}, }); } else if (msgEvent.data === 'reject-operation-error') { + console.log('[ServiceWorker] Rejecting payment request with OperationError'); rejecter(new DOMException('Reject with OperationError', 'OperationError')); } else if (msgEvent.data === 'reject-syntax-error') { + console.log('[ServiceWorker] Rejecting payment request with SyntaxError'); rejecter(new DOMException('Reject with SyntaxError', 'SyntaxError')); } else { + console.log(`[ServiceWorker] Unrecognized message data: ${msgEvent.data}`); return; // Message not for us. } @@ -33,12 +41,17 @@ self.addEventListener('message', msgEvent => { }); self.addEventListener('paymentrequest', event => { + console.log('[ServiceWorker] Received paymentrequest event'); activeMethodName = event.methodData[0].supportedMethods; event.respondWith(new Promise((resolve, reject) => { resolver = resolve; rejecter = reject; - event.openWindow('payment-app/reject-errors.html').catch(err => { + console.log('[ServiceWorker] Opening payment app window...'); + event.openWindow('payment-app/reject-errors.html').then(() => { + console.log('[ServiceWorker] Payment app window opened successfully'); + }).catch(err => { + console.error(`[ServiceWorker] Failed to open payment app window: ${err}`); resolver = null; rejecter = null; activeMethodName = null; diff --git a/web-based-payment-handler/payment-app/reject-errors.html b/web-based-payment-handler/payment-app/reject-errors.html index 4f7361ee52f3fe..ec75f57c8d93c3 100644 --- a/web-based-payment-handler/payment-app/reject-errors.html +++ b/web-based-payment-handler/payment-app/reject-errors.html @@ -9,9 +9,14 @@ From 590a6752d84b9ef0a25b979847947fba5d967458 Mon Sep 17 00:00:00 2001 From: Stephen McGruer Date: Mon, 6 Apr 2026 12:35:10 -0400 Subject: [PATCH 4/6] Fix scope --- .../payment-request-reject-errors-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-based-payment-handler/payment-request-reject-errors-manifest.json b/web-based-payment-handler/payment-request-reject-errors-manifest.json index f77db676eeeb78..ea6c372f3988c3 100644 --- a/web-based-payment-handler/payment-request-reject-errors-manifest.json +++ b/web-based-payment-handler/payment-request-reject-errors-manifest.json @@ -10,6 +10,6 @@ ], "serviceworker": { "src": "app-reject-errors.js", - "scope": "payment-request-reject-errors-payment-app/" + "scope": "./" } } From 3237d66b9df49afdf80b02b8c3d2f341072d9da7 Mon Sep 17 00:00:00 2001 From: Stephen McGruer Date: Mon, 6 Apr 2026 12:35:24 -0400 Subject: [PATCH 5/6] Revert "Temporary logging" This reverts commit dfc291281fd5ab30d250d712db6cf10cbcfb1cd4. --- .../app-reject-errors.js | 17 ++-------------- .../payment-app/reject-errors.html | 5 ----- ...t-reject-operation-error-manual.https.html | 20 ++----------------- 3 files changed, 4 insertions(+), 38 deletions(-) diff --git a/web-based-payment-handler/app-reject-errors.js b/web-based-payment-handler/app-reject-errors.js index f3a1283305e64d..3d2b30e96d2877 100644 --- a/web-based-payment-handler/app-reject-errors.js +++ b/web-based-payment-handler/app-reject-errors.js @@ -12,26 +12,18 @@ self.addEventListener('canmakepayment', event => { }); self.addEventListener('message', msgEvent => { - console.log(`[ServiceWorker] Received message from payment app: ${msgEvent.data}`); - if (!resolver || !rejecter) { - console.error('[ServiceWorker] Received message, but no active payment request found.'); - return; - } + if (!resolver || !rejecter) return; if (msgEvent.data === 'success') { - console.log('[ServiceWorker] Resolving payment request with success'); resolver({ methodName: activeMethodName, details: {status: 'success'}, }); } else if (msgEvent.data === 'reject-operation-error') { - console.log('[ServiceWorker] Rejecting payment request with OperationError'); rejecter(new DOMException('Reject with OperationError', 'OperationError')); } else if (msgEvent.data === 'reject-syntax-error') { - console.log('[ServiceWorker] Rejecting payment request with SyntaxError'); rejecter(new DOMException('Reject with SyntaxError', 'SyntaxError')); } else { - console.log(`[ServiceWorker] Unrecognized message data: ${msgEvent.data}`); return; // Message not for us. } @@ -41,17 +33,12 @@ self.addEventListener('message', msgEvent => { }); self.addEventListener('paymentrequest', event => { - console.log('[ServiceWorker] Received paymentrequest event'); activeMethodName = event.methodData[0].supportedMethods; event.respondWith(new Promise((resolve, reject) => { resolver = resolve; rejecter = reject; - console.log('[ServiceWorker] Opening payment app window...'); - event.openWindow('payment-app/reject-errors.html').then(() => { - console.log('[ServiceWorker] Payment app window opened successfully'); - }).catch(err => { - console.error(`[ServiceWorker] Failed to open payment app window: ${err}`); + event.openWindow('payment-app/reject-errors.html').catch(err => { resolver = null; rejecter = null; activeMethodName = null; diff --git a/web-based-payment-handler/payment-app/reject-errors.html b/web-based-payment-handler/payment-app/reject-errors.html index ec75f57c8d93c3..4f7361ee52f3fe 100644 --- a/web-based-payment-handler/payment-app/reject-errors.html +++ b/web-based-payment-handler/payment-app/reject-errors.html @@ -9,14 +9,9 @@ From 5dfa2b6104fe99848c388f8e3a927abd0091f7eb Mon Sep 17 00:00:00 2001 From: Stephen McGruer Date: Mon, 6 Apr 2026 12:36:09 -0400 Subject: [PATCH 6/6] Fixup --- .../payment-request-reject-operation-error-manual.https.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web-based-payment-handler/payment-request-reject-operation-error-manual.https.html b/web-based-payment-handler/payment-request-reject-operation-error-manual.https.html index 24052256d83bf0..7a9575c8b04e08 100644 --- a/web-based-payment-handler/payment-request-reject-operation-error-manual.https.html +++ b/web-based-payment-handler/payment-request-reject-operation-error-manual.https.html @@ -9,8 +9,8 @@

This test verifies that if a payment app rejects the promise passed to respondWith with an OperationError, the original PaymentRequest.show() call also rejects with an OperationError. For other error types, it should still reject with an AbortError.

Please follow these instructions:

    -
  1. For the first test (OperationError), select "Reject Errors Payment Handler" in the payment sheet. When the app window opens, click "Reject with OperationError".
  2. -
  3. For the second test (SyntaxError), select "Reject Errors Payment Handler" in the payment sheet. When the app window opens, click "Reject with SyntaxError".
  4. +
  5. For the first test (OperationError), click "Reject with OperationError".
  6. +
  7. For the second test (SyntaxError), click "Reject with SyntaxError".