feat: add subscription events#1632
Conversation
🚫 Missing Linked IssueHi 👋 This pull request does not appear to be linked to any open issue yet. Linking your PR to an issue helps keep the project tidy and ensures the issue is closed automatically. ✔️ How to fix this
Once linked, this check will pass automatically on your next push or when you re-run the workflow. Thanks for helping maintainers! 🙌 |
Code Review: Subscription Events Implementation[should-fix] Effect dependency array incompleteLocation: The effect that emits CVC completion status likely depends on React.useEffect(() => {
emitCvcInfo(~isCvcEmpty)
emitter.emitCvcStatus({complete: isCvcComplete, empty: isCvcEmpty})
None
}, [cvcNumber, isCvcEmpty, isCvcComplete]) // Verify all dependencies listedRisk: Stale closure capturing old state if dependencies are incomplete. [nit] Magic number for CVC length validationLocation: let isCvcComplete = cvcNumber->String.length >= 3Suggestion: This assumes all cards have 3-digit CVCs. American Express uses 4 digits. Consider using a card-type aware validator: let isCvcComplete = CardUtils.isCvcValid(cvcNumber, cardBrand)✓ Hook composition patternGood separation between General observationThe PR migrates 40+ files to the new event system. Ensure:
|
Review Summary: Subscription Events FeatureThis PR introduces a subscription events system that allows merchants to opt into specific events via a Overall AssessmentThe implementation follows the new hook-based event emission pattern consistently across payment components. However, I've identified several areas for improvement: [should-fix] Missing Input Validation for
|
|
[should-fix] Event type validation accepts unknown events silently In
Consider failing fast with a clear error when invalid subscription events are configured, rather than silently filtering. |
|
What will happen if merchant provides the subscription events list, and also add onChange callback? He will get all the subscribed events inside onChange as well? |
|
Refactor and other discussion will be picked up later |
Type of Change
Description
subscriptionEventsconfig option, replacing the old unconditional event emission patternsurchargeInfosubscription event that emits the full eligibility surcharge details object when surcharge data changesLoaderPaymentElementso multiple payment elements on the same page get correct per-instance event routingSubscription Events
Merchants configure which events they want to receive via the
subscriptionEventsoption:subscriptionEventsis not set (or empty), all events are emitted (backward compatible)Events Reference
Existing Events (migrated to subscription system)
elementTypePAYMENT_METHOD_INFO_CARDpaymentbin(string|null),last4(string|null),brand(string|null),expiryMonth(string|null),expiryYear(string|null),formattedExpiry(string|null),isCardNumberComplete(bool),isCvcComplete(bool),isExpiryComplete(bool),isCardNumberValid(bool),isExpiryValid(bool)PAYMENT_METHOD_STATUSpaymentpaymentMethod(string),paymentMethodType(string),isSavedPaymentMethod(bool),isOneClickWallet(bool)FORM_STATUSpaymentstatus(string:"EMPTY"|"FILLING"|"COMPLETE")PAYMENT_METHOD_INFO_BILLING_ADDRESSpaymentcountry(string),state(string),postalCode(string)CVC_STATUScardCvciframeId(string),isCvcEmpty(bool),isCvcComplete(bool)New Event
elementTypeSURCHARGEpaymentsurcharge(object:{ type: string, value: number }),taxOnSurcharge(number|null),displaySurchargeAmount(number),displayTaxOnSurchargeAmount(number),displayTotalSurchargeAmount(number)Lifecycle Events (unchanged, always emitted)
readyelementType(string),iframeId(string)focuselementType(string),iframeId(string)blurelementType(string),iframeId(string)changeelementType(string),iframeId(string),complete(bool),empty(bool)isLegacy)Multi-Instance Support
LoaderPaymentElementnow supports multiple instances of the same element type on one page:elementInstanceIdto scope event listener activity namesmatchesInstancefilters events byelementType+iframeIdso events route to the correct instance.mount()get theiriframeIdresolved by a sibling's mountBackward Compatibility
subscriptionEventsis not configured, all events fire as before (legacy mode)useLegacyEventshook gates old-style unconditional events behindisLegacyflag.on()event types (ready, focus, blur, change, click, confirmPayment, etc.) continue to workFiles Changed
PaymentEventTypes.res,PaymentEventData.res,SubscriptionEventTypes.resSurchargeevent type, surcharge event builder, JSON encoder, payload creatorSubscriptionEventHooks.resuseSubscriptionEventEmitter,useEmitFormStatus,useEmitBillingAddress,useEmitPaymentMethodStatus,useEmitSurcharge,useLegacyEventsLoaderPaymentElement.res,Types.res,Elements.ressubscriptionEventspassthroughCardPayment.res,ApplePay.res,GPay.res,PayPal.res,KlarnaSDK.res,SamsungPayComponent.res,PazeButton.res,PaypalSDKHelpers.res, bank transfers,SavedMethods.res, etc.useEmitFormStatus/useEmitSurchargecallsUtils.res,PaymentUtils.res,UtilityHooks.ressanitizeEventData,iframeIdin focus/blur/postMessage events,isLegacygatingHow did you test it?
case 1: if subscriptionEvents: is not passed,
expectation: all events should emit
Screen.Recording.2026-06-23.at.9.44.50.pm.mov
case 2: PAYMENT_METHOD_INFO_CARD event
Screen.Recording.2026-06-23.at.9.50.30.pm.mov
case 3: PAYMENT_METHOD_STATUS event
Screen.Recording.2026-06-23.at.9.52.29.pm.mov
case 4: FORM_STATUS event
Screen.Recording.2026-06-23.at.9.54.23.pm.mov
case 5: PAYMENT_METHOD_INFO_BILLING_ADDRESS event
Screen.Recording.2026-06-23.at.10.11.59.pm.mov
case 6a: CVC_STATUS event (only for cvc widget) -> multiple cvc widget
Screen.Recording.2026-06-23.at.10.18.43.pm.mov
case 6b: CVC_STATUS event (only for cvc widget) -> single cvc widget
Screen.Recording.2026-06-23.at.10.21.57.pm.mov
case 7: SURCHARGE event
Screen.Recording.2026-06-23.at.10.33.11.pm.mov
case 8: payment widget + 2 cvc widget (subscribed events are FORM_STATUS, CVC_STATUS)
Screen.Recording.2026-06-23.at.10.35.58.pm.mov
case 9: using .on("change", ev=> ...), no subscription event passed
Screen.Recording.2026-06-23.at.10.41.46.pm.mov
Screen.Recording.2026-06-23.at.10.42.54.pm.mov
case 10: using .on("change", ev=> ...), PAYMENT_METHOD_INFO_CARD event
Screen.Recording.2026-06-23.at.10.44.48.pm.mov
case 11: using .on("change", ev=> ...), PAYMENT_METHOD_STATUS event
Screen.Recording.2026-06-23.at.10.46.49.pm.mov
case 12: using .on("change", ev=> ...), FORM_STATUS event
Screen.Recording.2026-06-23.at.10.48.27.pm.mov
case 13: using .on("change", ev=> ...), PAYMENT_METHOD_INFO_BILLING_ADDRESS event
Screen.Recording.2026-06-23.at.10.50.09.pm.mov
case 14: using .on("change", ev=> ...), SURCHARGE event
Screen.Recording.2026-06-23.at.10.52.00.pm.mov
case 15: using .on("change", ev=> ...), CVC_STATUS event
https://github.com/user-attachments/assets/263cd4ef-8ae2-4dcf-b080-1058b100a87d
case 16: using .on("PAYMENT_METHOD_INFO_CARD", ev=> ...)
Screen.Recording.2026-06-23.at.10.56.42.pm.mov
case 17: using .on("PAYMENT_METHOD_STATUS", ev=> ...)
Screen.Recording.2026-06-23.at.10.58.00.pm.mov
case 18: using .on("FORM_STATUS", ev=> ...)
Screen.Recording.2026-06-23.at.11.01.26.pm.mov
case 19: using .on("PAYMENT_METHOD_INFO_BILLING_ADDRESS", ev=> ...)
Screen.Recording.2026-06-23.at.11.03.11.pm.mov
case 20: using .on("SURCHARGE", ev=> ...)
Screen.Recording.2026-06-23.at.11.04.46.pm.mov
case 21 using .on("CVC_STATUS", ev=> ...)
Screen.Recording.2026-06-23.at.11.10.23.pm.mov
case 22 using .on("ready", ev=> ...) and onReady(ev=>...)


case 23a subscriptionEvents: ["CVC_STATUS"], .on("FORM_STATUS", (event)=> ...)
Screen.Recording.2026-06-24.at.9.09.02.am.mov
Screen.Recording.2026-06-24.at.9.09.51.am.mov
case 23b: react integration, event "FORM_STATUS" (which is only for payment element)
Screen.Recording.2026-06-24.at.9.13.10.am.mov
case 24: subscriptionEvents: ["PAYMENT_METHOD_INFO_CARD", "FORM_STATUS"], .on("PAYMENT_METHOD_INFO_CARD", (event) => ..., .on("FORM_STATUS", (event)=> ...
Screen.Recording.2026-06-24.at.9.26.24.am.mov
case 25: multiple widget, js integration
Screen.Recording.2026-06-24.at.9.23.04.am.mov
Checklist
npm run re:build