fix(place-order): keep the button disabled until privacy & terms are accepted - #825
Open
acasazza wants to merge 1 commit into
Open
fix(place-order): keep the button disabled until privacy & terms are accepted#825acasazza wants to merge 1 commit into
acasazza wants to merge 1 commit into
Conversation
…accepted
Selecting a payment method could enable `PlaceOrderButton` while the privacy &
terms checkbox was still unchecked.
Acceptance travelled from `<PrivacyAndTermsCheckbox>` to `PlaceOrderButton`
through `localStorage["privacy-terms"]`, and the write that happens at *mount*
notified nobody. A `"true"` left behind by an earlier visit — the unmount
cleanup only runs on a React unmount, never on a tab close or a hard navigation
— was read by `PlaceOrderContainer` as `isPermitted: true`. The checkbox then
mounted and correctly wrote `"false"`, but none of the container's effect
dependencies changed, so `isPermitted` stayed stale. The button's own effect
does re-run on `paymentSource?.id`, i.e. exactly when a payment method is
picked, and it read that stale value.
Acceptance now lives in a module-level store keyed by order id, which notifies
its subscribers. It is deliberately not persisted: a reload starts from
"not accepted", so what the shopper sees can no longer diverge from what gates
the button.
- add `termsAcceptanceStore`: in-memory, per-order, notifying
- add the public `useTermsAndConditions()` hook, so a custom consent control has
a supported channel now that `localStorage` is gone
- `placeOrderPermitted` takes `termsAccepted` as a parameter instead of reading
a global, and reports `termsBlocking`
- warn in development when acceptance is required but no control collects it,
from an effect so a late-mounting checkbox cannot raise a false alarm
- drop `PLACE_ORDER_RECHECK_EVENT`, made redundant by the store
- drop a dead `isFree && !isPermitted` line: `setNotPermitted` is a state
setter, so the branches below always overwrote it in the same effect pass
Two existing tests claimed to cover this and covered nothing: the suite mocks
`getCardDetails` to `{ brand: "" }`, which already falsifies the first factor of
the enabling condition, so they passed on the wrong factor and would have passed
with the gate deleted. Both now run with the condition live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Stacked on #824.
The bug
Selecting a payment method could enable
PlaceOrderButtonwhile the privacy & terms checkbox was still unchecked.Root cause
Acceptance travelled from
<PrivacyAndTermsCheckbox>toPlaceOrderButtonthroughlocalStorage["privacy-terms"], and the write that happens at mount notified nobody — the checkbox only signalled from insidehandleChange."true"left behind by an earlier visit survives: the cleanup that doesremoveItemruns only on a React unmount, never on a tab close or a hard navigation.PlaceOrderContainerreads that stale"true"and computesisPermitted: true."false"— but none of the container's effect dependencies ([order, include, includeLoaded, organizationConfig]) changed, soisPermittedstays stale.paymentSource?.idandcurrentPaymentMethodType— i.e. exactly when a payment method is picked — reads the staleisPermitted: true, seescard.brand, and enables.Because it depends on mount order, it does not reproduce every time.
The fix
Acceptance now lives in a module-level store keyed by order id, following the
usePrices/useSkusidiom already in this repo. The store notifies its subscribers, which is the thing that was missing. It is deliberately not persisted: a reload starts from "not accepted", so what the shopper sees can no longer diverge from what gates the button. Keying by order id also stops consent given on one order from leaking into another.termsAcceptanceStore— in-memory, per-order, notifyinguseTermsAndConditions()hook: withlocalStoragegone, a custom consent control needs a supported channel rather than an unofficial oneplaceOrderPermittedtakestermsAcceptedas a parameter instead of reading a global, and reportstermsBlockingPLACE_ORDER_RECHECK_EVENT, made redundant by the storeisFree && !isPermittedline:setNotPermittedis a state setter, so the branches below always overwrote it within the same effect pass. Verified by removing it against the full suite — no observable behaviour changesTwo tests that covered nothing
The suite mocks
getCardDetailsto{ brand: "" }and never providescurrentPaymentMethodRef, which already falsifies the first factor of the enabling condition((isFree && isPermitted) || onsubmit || card.brand) && isPermitted. Sostays disabled when paymentMethodErrors clear but privacy/terms checkbox is not checkedandis enabled for free order when permittedpassed on the wrong factor and would have passed with the gate deleted entirely. Both now run with the condition live, and the free-order describe asserts a real matrix (missing billing address, missing shipping address, unaccepted terms).Verification
termsAcceptedfrom the container's deps fails 2; removing the diagnostic's count guard fails 2dist/: nolocalStorageaccess for privacy/terms remains; the input'sname="privacy-terms"attribute is unchangedOne mutation is not caught: flipping the
termsAccepted = falsedefault totrue. It is unreachable because every caller passes the value explicitly — left atfalseas the safe direction, but no test protects it.Breaking changes
PLACE_ORDER_RECHECK_EVENTremovedlocalStorage["privacy-terms"]no longer read or writtenNeither had consumers in this repo outside its own tests.
mfe-checkoutneeds no change — it usesPlaceOrderContainer+PrivacyAndTermsCheckbox, both covered — but it pinspkg.pr.new@5884410, so it will not pick this up until published, andcore-components/react-hooks-componentsare pinned to the same commit.🤖 Generated with Claude Code