Repository navigation
Releases: vezmoPay/vezmoPay-wooCommerce
Release list
0.3.14 — hosted checkout redirects again, and the settings screen loads
Update promptly if you use Hosted checkout, or if the VezmoPay settings screen has been showing a critical error.
Hosted checkout redirects to VezmoPay again. Since 0.3.2, hosted mode waited for VezmoPay to confirm the account was activated for payment links. VezmoPay has no such confirmation to give, so the answer was always no, and customers who should have been redirected got the embedded form on your own pay page. Hosted mode now does what it says. Whether a link can actually be paid is checked per payment: if the account isn't activated to receive money yet, the customer stays on your checkout with an error, and the order records why, instead of landing on a dead "No payment method available" page. The "My VezmoPay account is activated for payment links" setting and the vezmopay_force_hosted_mode filter are gone, and no action is needed if you had ticked the box.
The settings screen no longer crashes. Opening WooCommerce → Settings → Payments → VezmoPay could throw a critical error and take down wp-admin. It now always loads.
Slow bank (ACH) payments complete. Linking a bank account inside the payment form can take longer than the checkout's one-minute wait. When it did, the money moved but the customer was left on "Payment initiated". A late answer from the form now completes the order exactly as an on-time one does. Nothing is charged twice.
One Pay button. The extra "Pay" button under the embedded form, directly above WooCommerce's own "Place order", is gone.
Also in this release, never shipped before: a fault where two changes merged on the same day would have broken the five-minute reconciliation job for every hosted-mode store, removing the safety net behind lost webhooks. It was caught and fixed before release.
vezmopay-sha256: aa417b435cfbf2963b02b05a7a98319dc33e91d7a3c0a7b6681510353084a73c
0.3.13 — one payment form, and a failure the store agrees with
"Card form is not ready" on a payment that had already succeeded. Inline payment element mode only — which is the default.
vezmo.js as deployed has mount(), pay() and on(), and no wallet handshake. The 0.3.7 wallet work called vezmo.onWalletAuthorized() unconditionally, so every element-mode mount threw a TypeError on that line. The missing handshake was not the damage — where the throw landed was:
- the fallback in the
.catchmounted a second payment form beside the one the SDK had just mounted, and the checkout then listened to the second while charging through the first; - every
vezmo.on(...)registration below that line never ran, so ready, processing, success, error, cancel and expired were all lost.
So the card was charged, the checkout heard nothing, its submit retry kept firing because no processing ever arrived, and a submit landing on a form whose card element had gone was answered "Card form not ready". The order still completed — the store's own five-minute check and its polling settle it — but the customer was told their payment had failed on the way to the confirmation page.
Fixed three ways, smallest first: the handshake call is guarded, so a build without it simply gets no wallet buttons; the fallback clears the container first, so there can only ever be one form on the page; and a failure is never shown until the store has checked with VezmoPay — the money is what the store can verify, not what the form says. A genuine decline is unaffected and still appears immediately under the card fields.
Verified in element mode: one frame, SDK events arriving, a real decline still shown. Before the fix the same run mounted two frames and logged no SDK events at all.
vezmopay-sha256: d9d4da984478ef6a85d95911b3de86b708e15ede85887e536f52fc9140d7d91d
0.3.12 — count them all, and say what two causes mean
Follow-up to 0.3.11, from replaying the dev store's own log pattern.
The refusal notice restarted its count whenever the cause changed, so a store whose deliveries alternate — unsigned, unsigned, mismatch, unsigned, which is exactly what the dev store logs — would always have read "1 delivery has been refused" no matter how many there were. It now counts every refusal since the last accepted delivery.
It also names what the alternation means: deliveries arriving unsigned while others are signed with a secret this store does not have is what two registered webhook endpoints looks like — one created without a signing secret, one whose secret went somewhere else. Neither message says that on its own.
vezmopay-sha256: 03113b4ca84e0366c4f155c6120094f27f6b9c3e261a9d4969b05402a3b2111b
0.3.11 — why deliveries are being refused, where you can see it
Your store's webhook endpoint is working correctly — the log was telling you so in a way nobody could act on.
The three refusals on the dev store mean two different configuration problems, and the plugin now says which:
- "a webhook secret is configured but the delivery carried no signature" — VezmoPay is delivering these events unsigned, which means the webhook endpoint in your dashboard has no signing secret of its own. Nothing pasted into the store can fix that: the secret is generated once, when the endpoint is created. Delete the endpoint, create it again, paste the new
whsec_…. - "Webhook signature mismatch" — that delivery was signed, with a different secret from the one saved in the store. A second endpoint, or one whose secret was regenerated.
I checked the thing that would have made this a code bug before concluding that: a signature computed exactly the way the platform computes it — HMAC-SHA256 hex over the raw JSON body — verifies here, bare or sha256= prefixed. The verification is right.
So the fix is the missing half: each refusal now carries its remedy in the log line, and the settings screen shows a notice naming the reason, how many deliveries have been refused, when the last one was, and what to do. It clears itself the moment one delivery is accepted. Statuses and reasons are unchanged.
vezmopay-sha256: 4e654a0b3f08a0a570828d80b4c4b232290bc5ceb556fa74ceffd026a4403dc2
0.3.10 — an event claim that is actually a claim
Webhook endpoint only. Nothing on the payment surfaces changed, but the shared lock underneath every reconciliation did, so all three were re-verified.
Duplicate deliveries could do the work twice. The event claim was built on add_option(), which reads through the object cache before writing and is therefore not a mutex; the stale-claim takeover was a second read-then-write; and the release deleted by name, so a delivery told "busy" freed the claim of the delivery that was doing the work. VezmoPay's retry then ran the whole reconciliation again for an event already handled. No order was ever completed twice — the reconcile lock stops that — but each replay cost a blocking call to VezmoPay, which is exactly what the claim exists to prevent.
The primitive is now one class used by both the reconcile lock and the event claim: an INSERT IGNORE claim on the options table's unique index, a compare-and-swap takeover of an abandoned claim, and a release that matches the claim's own token. It also fixes an edge the earlier copy still had — if the row disappears between a failed claim and the follow-up read, try once more rather than reporting busy over a lock that is free.
The log no longer fills with the same rejection. A store that has not saved its webhook secret wrote one error line per delivery, times four platform retries, indefinitely — burying the errors a merchant actually needs and making a correctly-behaving endpoint read as broken. Each kind of refusal is logged loudly once per five-minute window and quietly after that. A mismatched signature is never quietened: that one can mean a real misconfiguration or an attack. Statuses and reasons are unchanged.
Genuine deliveries can no longer be rate-limited. The throttle sat ahead of signature verification at 60 per five minutes per address, so a store above roughly twelve orders a minute would have started refusing real deliveries if VezmoPay delivers from one address. A valid signature is now never throttled; the cap applies only to traffic that fails authentication, behind a much higher unconditional ceiling whose only job is to stop the endpoint being used to make the store compute HMACs.
Claim rows no longer accumulate. They are kept for their ten-minute lifetime by design — that is what makes a replay a duplicate — so nothing removed them and a busy store grew one row per webhook event for the life of the install. The five-minute check now sweeps the expired ones.
Two comments that described code which no longer exists are rewritten, and the delivery-rate question is recorded in docs/VEZMOPAY-API-CONTRACT.md: the platform's delivery queue sets no limiter and no concurrency, and its egress addresses are not in the source — still an open question for whoever runs that infrastructure.
vezmopay-sha256: 45f1ca03b088ccac4f80cc56ecd0ef613a799527dfc6b898dc41d5bfb6a6241e
0.3.9 — the order's own currency, and a pay page that pays
Update promptly. This release fixes a way an order could be charged a hundred times over, and a payment page customers could not pay on at all.
The two that matter
A silent overcharge on any order whose currency is not the store's. VezmoPay multiplies every amount by 100 — right for dollars, catastrophic for yen — so the plugin refuses zero-decimal currencies. It only ever checked the store's currency, while the charge carries the order's. A multi-currency plugin, an order created in the admin, or a store whose currency changed after the order: ¥1,000 was sent, ¥100,000 was taken, and the plugin's own tamper check compared the figure it had sent, so the order completed as paid with nothing to show anything was wrong. The order's own currency is now checked before any money moves.
Customers could not pay from My Account, the invoice email, or the admin's payment link. All three open ?pay_for_order=true, a page the embedded payment form is deliberately not loaded on — so it showed "Loading secure payment fields…" for ever, and pressing Pay silently re-rendered the same dead page while moving the order back to Pending. That page now sends the customer to a payment form that works, in both embedded modes.
Money that could be taken twice, or lost
- A paid payment link is recognised before a retry starts a second one.
- A "try again" link can no longer mint a new payment over one that has already been captured.
- An order that was paid is no longer knocked back to On hold by a later check after the merchant edits its total — the mismatch is still recorded for review, but the order is left alone, and the customer is no longer blocked from retrying.
- The reconciliation lock is a real mutex now; it could previously be held by two passes at once, and a revived stale holder could free somebody else's claim mid-flight.
Honesty in the admin
- Apple Pay and Google Pay say plainly that they do not appear in the embedded payment form, instead of advertising a button no customer of that store will see.
- The payment-method list says VezmoPay decides what the embedded form offers — a method shown as unavailable can still be offered to shoppers, bank transfer included.
- Hosted checkout: once the override is ticked, the settings screen stops saying hosted is inactive and instructing you to tick it, and says what you have actually taken on.
- An order stranded on an unactivated account is given up on after 24 hours with one note saying what to check, instead of being re-read silently for a week. Bank debits in flight are never given up on.
Security and housekeeping
- Webhook deliveries that cannot be authenticated are refused rather than processed, with an admin notice when no secret is saved. Orders still complete via the five-minute check.
- Every URL VezmoPay returns that the plugin will act on — the SDK script, the payment frame, the redirect — is checked against the same host allow-list the settings URLs use.
- Billing details are no longer written to the debug log; a webhook with no event id can no longer be replayed indefinitely; the rate limit can no longer be reset by clearing cookies; the decline message no longer repeats itself.
For the repo
tests/sandbox/ now carries the WordPress + mock-API rig every [S] case in the test checklist is verified with, and CI enforces a syntax floor on every PHP and JS file. Coding-standards checking is wired up but advisory until the existing backlog is cleared — the workflow says so.
vezmopay-sha256: 3b0620ed12c3bd6211e4bb216423adbfb7ee03928382f01bc03473642cc32832
0.3.8 — the checklist catches up with 0.3.7
No change to how the plugin behaves — 0.3.8 is 0.3.7's code. It exists so the QA documentation ships with the build it describes.
The test checklist had split into two versions: one on main from the 0.3.7 pull requests, and a full rewrite where every case carries an id. They are now one document, current for 0.3.7 and 176 cases:
- Wallets. The old section expected the double charge, because that was the behaviour when it was written. PR #6 fixed it, so the section is now the handshake's own cases, all asking the one question worth asking: did money move without an order to put it on?
- Bank payments. Rewritten around what 0.3.7 does — billing travelling with the session, a submitted debit putting the order on hold and sending the customer to order-received, no second debit while the first is in flight, and the sixty-second bound never firing on a payment that is on its way.
- New cases for the rest of 0.3.7: the abandoned status-check claim being broken after two minutes, the order action no longer reporting
LOCKEDas though it were a payment status, the secure-payment token in the request path being redacted, the frame's referrer policy (and why a security plugin sendingno-referrerused to silence every event), the settings-fields recursion, and the Transaction label. - A slow confirmation must not read as a failure — the defect that put a "no result" note on an order seconds before its own "payment captured" note.
Every case has an id, so a failure is reported as T-22.4, and each is marked for how it can be proven: in a local sandbox, or only against the real platform.
vezmopay-sha256: 75f6211973b35c35cd407311c7126f6e63846271a48ae0654c643577f1701d58
0.3.7 — wallets, bank payments, and orders that unstick themselves
Wallets, bank payments, and three ways an order could get stuck. Everything below is on main from PRs #5–#13; this release tags and packages it for testing.
A wallet approval can no longer take money without an order. Apple Pay and Google Pay are available in the payment box again. A wallet sheet charges the instant the customer approves it — on their own tap, inside VezmoPay's form, before WooCommerce has an order for that money to belong to. The plugin now holds the approval, places the order, and only then lets the charge complete; a checkout WooCommerce refuses cancels the payment instead of taking it. The buttons stay greyed out until the required checkout fields are filled.
Bank (ACH) payments can be selected at checkout. The payment form is created for the cart before any billing field is filled, so it carried no customer — and an ACH debit mandate requires the payer's email, so choosing Bank simply failed. Billing details are now sent to VezmoPay as they are typed, on both checkouts, with the order's own billing attached as a backstop before anything is charged.
A submitted bank payment completes the order instead of stranding the customer: the order goes on hold with a note saying the debit is clearing, the customer reaches order-received, and the order updates itself when the debit settles. Pressing Place order again while that debit is on its way no longer starts a second payment.
The checkout no longer calls a payment failed while the store is completing it. Confirming an order sends its emails, which can take tens of seconds; everything asking about the payment in that window was told "still waiting", so the customer saw a failure and the order collected a "no result" note seconds before its own "payment captured" note.
A status check that never finished can no longer freeze an order permanently. An abandoned claim is broken after two minutes and logged, instead of turning away every later check, webhook and retry for that order forever.
Also: the "Check VezmoPay payment status" action stops reporting LOCKED as if it were a payment status; debug logging no longer writes a live payment credential (the secure-payment token travels in the request path, and only bodies were redacted); the payment frame pins a referrer policy; the settings form fields no longer recurse into init_settings; and a new Transaction label setting tells VezmoPay which store a payment came from.
Translation template regenerated (242 strings).
vezmopay-sha256: 889d5f30c77068077c75bc21a1ab2a7073be43084da3227390a947b79c6cc917
0.3.6 — a decline keeps the card details
A declined card no longer clears the payment form.
0.3.5 replaced the form with a fresh one after a decline, so a customer who mistyped one digit had to enter their card number, expiry and CVC again. The form now stays exactly as it is, with everything they typed still in it; the decline appears next to it; the Pay button comes straight back; and pressing it charges the same payment again without leaving the checkout.
Making that work needed a change on the store side too. The payment session is consumed the first time it is attached to an order, so a second attempt used to find nothing and redirect to the pay page. A declined card leaves its payment chargeable, so the store now hands the browser the same payment back — after checking against the VezmoPay API that it is still unpaid, still for this order's exact amount and currency, and not one already reported as failed. A captured payment is never offered for a second charge.
The form is still replaced where it genuinely cannot be charged again — VezmoPay reporting the payment as failed, or an expired payment session — and only then does the message ask for the card to be entered again.
The order-pay page and the checkout payment box now say the same thing in the same words.
vezmopay-sha256: ac92e262e640568a9dcdda6034220650f6a0e4979724749aecfb05c002754a85
0.3.5 — both payment surfaces, and a bounded attempt
Fixes the retest of 0.3.4, which showed no improvement because the fix landed in the wrong file.
The order-pay page has the same decline handling as the checkout payment box. They are different code — checkout-element.js / checkout-iframe.js versus checkout-inline.js — and 0.3.4 changed only the second, while the decline was entered on the pay page. Every payment session that cannot be bound routes there (token expired while the checkout sat open, cart total changed, JavaScript unavailable), so it is a first-class surface. The shared behaviour now lives in one module both drivers use, so they cannot drift again.
A payment attempt is bounded. VezmoPay reports no terminal state for a declined card — the payment stays INITIATED and the hosted form emits no error event — so waiting is the worst option: one test order polled for 100 seconds, another for 15 minutes, both showing a spinner and then moving the customer on without a word. Sixty seconds after Pay is pressed with no result, the customer is told plainly, the Pay button comes back, and they can correct their card and press Pay again on the same payment. No replacement payment is created on a timeout, so there is no path to two live payments for one order. A 3-D Secure challenge is never timed out — and making that reliable turned up a second defect: vezmo.js does not relay requires_action at all, so both element paths now listen for it directly (source- and origin-checked).
Failed attempts appear on the order. A declined card used to leave an order with no notes, indistinguishable from an abandoned cart. The browser reports the attempt to the store, the store asks the VezmoPay API before it believes the report — a payment that actually settled forwards the customer instead of showing a failure — and records what happened either way.
Webhooks are no longer rejected for want of a saved secret. VezmoPay signs every delivery now; refusing a signed delivery a store cannot verify only cost the merchant their fast order updates, while an unsigned delivery was accepted anyway. They are accepted with a logged reminder to paste the secret. Nothing in a webhook is trusted without re-reading the payment from the API.
One checkout script per page. The checkout-page script was also loading on the pay page, leaving two payment watchers there.
Still outstanding, and platform-side: a declined charge needs to produce a signal — either the payment transitioning to FAILED with a reason on GET /merchant/payment/:id, or vezmo:secure-payment:error from the hosted page. Until then the sixty-second bound is a bounded guess, marked TODO(platform) in the code and recorded in docs/VEZMOPAY-API-CONTRACT.md.
vezmopay-sha256: fe58c08f6321479d07d404021a0b6ac4428a4ea2ccec404b171ae5925a1a0a12