fix(window): make off() reject event names so listeners stop leaking - #2007
Open
AngelPaella wants to merge 1 commit into
Open
fix(window): make off() reject event names so listeners stop leaking#2007AngelPaella wants to merge 1 commit into
AngelPaella wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: a949f37 The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
on() returns a branded ListenerId and off() only takes one. Passing an event name removed nothing and typechecked, so four components leaked their listeners on unmount. useOAuthWindowListener also stacked a listener pair per provider click, and both pairs redeemed the same one-time secret over the shared popup. One cleanup closure now runs on every exit path, and a second click during the OAuth URL fetch cancels the first flow instead of racing it. Fixes ENG4-360.
AngelPaella
force-pushed
the
fix/eng4-360-listener-ids
branch
from
August 6, 2026 00:42
37d5ccd to
a949f37
Compare
Contributor
🔥 Smoke Test Results❌ Status: Failed Statistics
Test DetailsThis is a non-blocking smoke test. Full regression tests run separately. |
AngelPaella
marked this pull request as ready for review
August 6, 2026 18:01
Contributor
|
Reviews (1): Last reviewed commit: "fix(window): make off() reject event nam..." | Re-trigger Greptile |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes ENG4-360.
The bug
off()removes a listener by the idon()handed back. Ten calls across four files passed the event name instead:WindowTransport.removeMessageListenerlooks the argument up in its id map, misses, and returns without throwing. TypeScript allowed it becauseon()returnedstring,off()tookstring, and event names are strings.The
windowmessage listener outlives the component. Mount it again and two listeners parse the same message: the live one, and the one the dead render left behind, which still calls the callbacks it captured.setHeighton a dead component does nothing, butonPaymentMethodSelectedfires the integrator's stale callback. InuseOAuthWindowListenerthe leak compounds without any remount, since each provider click stacks another pair.on()now returns a branded id andoff()accepts only that, so passing an event name fails the build:Call sites the compiler caught
off()calls by namecard-management/CrossmintPaymentMethodManagementIFrame.tsxembed/v3/EmbeddedCheckoutV3IFrame.tsxembed/v3/EmbeddedCheckoutV3WebView.tsxhooks/useOAuthWindowListener.tsuseCrossmintCheckout.tsxcallsoff("order:updated", handler)on a Node-style local emitter with a different signature, so I left it alone.CrossmintPaymentMethodManagementIFrame
This component had both bugs #2003 fixed in
CrossmintIdentityVerificationIFrame, so I copied that fix. The listener effect depends only oniframeClient, which is set once, sopropsnever refreshed andonPaymentMethodSelectedfired the callback captured at mount. Swap the callback after mount and the old one runs. It now reads through alatestPropsref.The third call,
off("agentic-enrollment:created"), is gone.paymentMethodManagementIncomingEventshas no such key and nothing subscribed to it. It compiled becauseofftook astring.useOAuthWindowListener
Capturing ids fixed one of the two problems here. Both are on
main.Each
createPopupAndSetupListenerscall registered two listeners and removed none. Click Google then Apple and the Google pair stays attached; unmount mid-flow and both pairs stay, along with a 2.5s interval. Onecleanup()closure now drops both ids and clears the interval. It runs when auth material arrives, on error, when the user closes the popup, on unmount, and when a new popup replaces one still open.Replacing an open popup needed a second change.
cleanuponly reached the ref afterawait getOAuthUrl(provider)resolved, so a click landing inside that window found nothing to cancel. Both flows registered listeners, and sincePopupWindow.initEmptyopens a window by name they shared one popup, so a singleauthMaterialFromPopupCallbackreached both handlers and redeemed the same one-time secret twice. The hook now claims the flow before the await, and the losing flow returns instead of navigating the popup a second time.Verification
TS2345errors, all at theoff()calls above, and nothing else in the monorepo. Consumers resolve the package throughdist/index.d.ts, so the typecheck runs against built output, not source.crossmint-payment-method-management.test.tsxmirrors the identity-verification suite. Its mock returnslistener-id:${event}, distinct from the event name, so the unmount assertion fails if cleanup passes names again. Both behavior assertions fail against the pre-fix component.use-oauth-window-listener.test.tsdrives two overlapping clicks withgetOAuthUrlheld open. Against the pre-fix hook it counts 4 registered listeners instead of 2.pnpm --filter @crossmint/client-sdk-react-ui test:vitest: 34/34.pnpm lintclean.client-sdk-base,react-uiandreact-native-uibuild.Breaking
Types only, and only for code outside this repo that holds a listener id in a
string-typed variable. Those switch toListenerId, exported from@crossmint/client-sdk-window.onandoffbehave the same at runtime. The changeset marks the packagemajor, andchangeset statusbumps the dependents.No in-repo consumer needed a change beyond the four above. Nothing subclasses or re-declares
on/off, and no listener id is stored in an annotatedstring, so inference covers the rest.